Check sysui state before swiping up
am: a876fe7846

Change-Id: I143e363cba7d11d3db004c666185f008e9e6d0df
diff --git a/go/quickstep/res/drawable/default_thumbnail.xml b/go/quickstep/res/drawable/default_thumbnail.xml
new file mode 100644
index 0000000..0a2dbf0
--- /dev/null
+++ b/go/quickstep/res/drawable/default_thumbnail.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/darker_gray"/>
+    <corners android:radius="2dp"/>
+</shape>
diff --git a/go/quickstep/res/drawable/empty_content_box.xml b/go/quickstep/res/drawable/empty_content_box.xml
new file mode 100644
index 0000000..a488388
--- /dev/null
+++ b/go/quickstep/res/drawable/empty_content_box.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/transparent"/>
+    <stroke android:color="@android:color/white" android:width="4px"/>
+    <corners android:radius="2dp"/>
+</shape>
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 674fcae..66c074b 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -94,7 +94,7 @@
             return;
         }
         Task task = tasks.get(position);
-        holder.bindTask(task);
+        holder.bindTask(task, false /* willAnimate */);
         mLoader.loadTaskIconAndLabel(task, () -> {
             // Ensure holder still has the same task.
             if (Objects.equals(task, holder.getTask())) {
@@ -110,6 +110,13 @@
     }
 
     @Override
+    public void onBindViewHolder(@NonNull TaskHolder holder, int position,
+            @NonNull List<Object> payloads) {
+        // TODO: Bind task in preparation for animation. For now, we apply UI changes immediately.
+        super.onBindViewHolder(holder, position, payloads);
+    }
+
+    @Override
     public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
         if (holder.getTask() == null) {
             return;
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index 98dc989..a3fa5c1 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep;
 
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
@@ -40,13 +43,28 @@
     }
 
     /**
-     * Bind a task to the holder, resetting the view and preparing it for content to load in.
+     * Bind the task model to the holder. This will take the current task content in the task
+     * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
+     * the content to animate to at a later time. If the task does not have all its content loaded,
+     * the view will prepare appropriate default placeholders and it is the callers responsibility
+     * to change them at a later time.
+     *
+     * Regardless of whether it is animating, input handlers will be bound immediately (see
+     * {@link TaskActionController}).
      *
      * @param task the task to bind to the view
+     * @param willAnimate true if UI should animate in later, false if it should apply immediately
      */
-    public void bindTask(Task task) {
+    public void bindTask(@NonNull Task task, boolean willAnimate) {
         mTask = task;
-        mTaskItemView.resetTaskItemView();
+        Bitmap thumbnail = (task.thumbnail != null) ? task.thumbnail.thumbnail : null;
+        if (willAnimate) {
+            mTaskItemView.startContentAnimation(task.icon, thumbnail, task.titleDescription);
+        } else {
+            mTaskItemView.setIcon(task.icon);
+            mTaskItemView.setThumbnail(thumbnail);
+            mTaskItemView.setLabel(task.titleDescription);
+        }
     }
 
     /**
@@ -55,10 +73,7 @@
      */
     public void bindEmptyUi() {
         mTask = null;
-        // TODO: Set the task view to a loading, empty UI.
-        // Temporarily using the one below for visual confirmation but should be swapped out to new
-        // UI later.
-        mTaskItemView.resetTaskItemView();
+        mTaskItemView.resetToEmptyUi();
     }
 
     /**
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index c06b6ec..7f77b6c 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -24,14 +24,12 @@
 import android.animation.PropertyValuesHolder;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewDebug;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.LayoutAnimationController;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
@@ -40,6 +38,7 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
 
 import com.android.launcher3.R;
 import com.android.quickstep.RecentsToActivityHelper;
@@ -90,7 +89,6 @@
     private final TaskListLoader mTaskLoader;
     private final TaskAdapter mTaskAdapter;
     private final TaskActionController mTaskActionController;
-    private final LayoutAnimationController mLayoutAnimation;
 
     private RecentsToActivityHelper mActivityHelper;
     private RecyclerView mTaskRecyclerView;
@@ -98,6 +96,9 @@
     private View mContentView;
     private View mClearAllView;
     private boolean mTransitionedFromApp;
+    private AnimatorSet mLayoutAnimation;
+    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
+
 
     public IconRecentsView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -106,7 +107,6 @@
         mTaskAdapter = new TaskAdapter(mTaskLoader);
         mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
         mTaskAdapter.setActionController(mTaskActionController);
-        mLayoutAnimation = createLayoutAnimation();
     }
 
     @Override
@@ -120,7 +120,20 @@
             ItemTouchHelper helper = new ItemTouchHelper(
                     new TaskSwipeCallback(mTaskActionController));
             helper.attachToRecyclerView(mTaskRecyclerView);
-            mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
+            mTaskRecyclerView.addOnChildAttachStateChangeListener(
+                    new OnChildAttachStateChangeListener() {
+                        @Override
+                        public void onChildViewAttachedToWindow(@NonNull View view) {
+                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
+                                // Child view was added that is not part of current layout animation
+                                // so restart the animation.
+                                animateFadeInLayoutAnimation();
+                            }
+                        }
+
+                        @Override
+                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
+                    });
 
             mEmptyView = findViewById(R.id.recent_task_empty_view);
             mContentView = findViewById(R.id.recent_task_content_view);
@@ -165,8 +178,7 @@
      * becomes visible.
      */
     public void onBeginTransitionToOverview() {
-        mTaskRecyclerView.scheduleLayoutAnimation();
-
+        scheduleFadeInLayoutAnimation();
         // Load any task changes
         if (!mTaskLoader.needsToLoad()) {
             return;
@@ -338,17 +350,56 @@
                 });
     }
 
-    private static LayoutAnimationController createLayoutAnimation() {
-        AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
+    /**
+     * Schedule a one-shot layout animation on the next layout. Separate from
+     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
+     * view properties themselves, allowing more controllable behavior and making it easier to
+     * manage when the animation conflicts with another animation.
+     */
+    private void scheduleFadeInLayoutAnimation() {
+        ViewTreeObserver viewTreeObserver = mTaskRecyclerView.getViewTreeObserver();
+        viewTreeObserver.addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        animateFadeInLayoutAnimation();
+                        viewTreeObserver.removeOnGlobalLayoutListener(this);
+                    }
+                });
+    }
 
-        Animation alphaAnim = new AlphaAnimation(0, 1);
-        alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
-        anim.addAnimation(alphaAnim);
-
-        LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
-        layoutAnim.setDelay(
-                (float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
-
-        return layoutAnim;
+    /**
+     * Start animating the layout animation where items fade in.
+     */
+    private void animateFadeInLayoutAnimation() {
+        if (mLayoutAnimation != null) {
+            // If layout animation still in progress, cancel and restart.
+            mLayoutAnimation.cancel();
+        }
+        TaskItemView[] views = getTaskViews();
+        int delay = 0;
+        mLayoutAnimation = new AnimatorSet();
+        for (TaskItemView view : views) {
+            view.setAlpha(0.0f);
+            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
+            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
+            alphaAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    view.setAlpha(1.0f);
+                    mLayingOutViews.remove(view);
+                }
+            });
+            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
+            mLayoutAnimation.play(alphaAnim);
+            mLayingOutViews.add(view);
+        }
+        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLayoutAnimation = null;
+            }
+        });
+        mLayoutAnimation.start();
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index d831b20..572747b 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -16,15 +16,18 @@
 package com.android.quickstep.views;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
@@ -34,16 +37,41 @@
  */
 public final class TaskItemView extends LinearLayout {
 
+    private static final String EMPTY_LABEL = "";
     private static final String DEFAULT_LABEL = "...";
     private final Drawable mDefaultIcon;
+    private final Drawable mDefaultThumbnail;
+    private final TaskLayerDrawable mIconDrawable;
+    private final TaskLayerDrawable mThumbnailDrawable;
     private TextView mLabelView;
     private ImageView mIconView;
     private ImageView mThumbnailView;
+    private float mContentTransitionProgress;
+
+    /**
+     * Property representing the content transition progress of the view. 1.0f represents that the
+     * currently bound icon, thumbnail, and label are fully animated in and visible.
+     */
+    public static FloatProperty CONTENT_TRANSITION_PROGRESS =
+            new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
+                @Override
+                public void setValue(TaskItemView view, float progress) {
+                    view.setContentTransitionProgress(progress);
+                }
+
+                @Override
+                public Float get(TaskItemView view) {
+                    return view.mContentTransitionProgress;
+                }
+            };
 
     public TaskItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mDefaultIcon = context.getResources().getDrawable(
-                android.R.drawable.sym_def_app_icon, context.getTheme());
+        Resources res = context.getResources();
+        mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
+        mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
+        mIconDrawable = new TaskLayerDrawable(context);
+        mThumbnailDrawable = new TaskLayerDrawable(context);
     }
 
     @Override
@@ -52,15 +80,21 @@
         mLabelView = findViewById(R.id.task_label);
         mThumbnailView = findViewById(R.id.task_thumbnail);
         mIconView = findViewById(R.id.task_icon);
+
+        mThumbnailView.setImageDrawable(mThumbnailDrawable);
+        mIconView.setImageDrawable(mIconDrawable);
+
+        resetToEmptyUi();
+        CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
     }
 
     /**
-     * Resets task item view to default values.
+     * Resets task item view to empty, loading UI.
      */
-    public void resetTaskItemView() {
-        setLabel(DEFAULT_LABEL);
-        setIcon(null);
-        setThumbnail(null);
+    public void resetToEmptyUi() {
+        mIconDrawable.resetDrawable();
+        mThumbnailDrawable.resetDrawable();
+        setLabel(EMPTY_LABEL);
     }
 
     /**
@@ -69,11 +103,8 @@
      * @param label task label
      */
     public void setLabel(@Nullable String label) {
-        if (label == null) {
-            mLabelView.setText(DEFAULT_LABEL);
-            return;
-        }
-        mLabelView.setText(label);
+        mLabelView.setText(getSafeLabel(label));
+        // TODO: Animation for label
     }
 
     /**
@@ -86,11 +117,7 @@
         // The icon proper is actually smaller than the drawable and has "padding" on the side for
         // the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
         // view if we want the icon to be flush with the bottom of the thumbnail.
-        if (icon == null) {
-            mIconView.setImageDrawable(mDefaultIcon);
-            return;
-        }
-        mIconView.setImageDrawable(icon);
+        mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
     }
 
     /**
@@ -99,16 +126,48 @@
      * @param thumbnail task thumbnail for the task
      */
     public void setThumbnail(@Nullable Bitmap thumbnail) {
-        if (thumbnail == null) {
-            mThumbnailView.setImageBitmap(null);
-            mThumbnailView.setBackgroundColor(Color.GRAY);
-            return;
-        }
-        mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
-        mThumbnailView.setImageBitmap(thumbnail);
+        mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnail));
     }
 
     public View getThumbnailView() {
         return mThumbnailView;
     }
+
+    /**
+     * Start a new animation from the current task content to the specified new content. The caller
+     * is responsible for the actual animation control via the property
+     * {@link #CONTENT_TRANSITION_PROGRESS}.
+     *
+     * @param endIcon the icon to animate to
+     * @param endThumbnail the thumbnail to animate to
+     * @param endLabel the label to animate to
+     */
+    public void startContentAnimation(@Nullable Drawable endIcon, @Nullable Bitmap endThumbnail,
+            @Nullable String endLabel) {
+        mIconDrawable.startNewTransition(getSafeIcon(endIcon));
+        mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
+        // TODO: Animation for label
+
+        setContentTransitionProgress(0.0f);
+    }
+
+    private void setContentTransitionProgress(float progress) {
+        mContentTransitionProgress = progress;
+        mIconDrawable.setTransitionProgress(progress);
+        mThumbnailDrawable.setTransitionProgress(progress);
+        // TODO: Animation for label
+    }
+
+    private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
+        return (icon != null) ? icon : mDefaultIcon;
+    }
+
+    private @NonNull Drawable getSafeThumbnail(@Nullable Bitmap thumbnail) {
+        return (thumbnail != null) ? new BitmapDrawable(getResources(), thumbnail)
+                                   : mDefaultThumbnail;
+    }
+
+    private @NonNull String getSafeLabel(@Nullable String label) {
+        return (label != null) ? label : DEFAULT_LABEL;
+    }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
new file mode 100644
index 0000000..98b66b9
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+
+/**
+ * A layer drawable for task content that transitions between two drawables by crossfading. Similar
+ * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
+ * progress and provides a default, empty drawable.
+ */
+public final class TaskLayerDrawable extends LayerDrawable {
+    private final Drawable mEmptyDrawable;
+    private float mProgress;
+
+    public TaskLayerDrawable(Context context) {
+        super(new Drawable[0]);
+
+        // Use empty drawable for both layers initially.
+        mEmptyDrawable = context.getResources().getDrawable(
+                R.drawable.empty_content_box, context.getTheme());
+        addLayer(mEmptyDrawable);
+        addLayer(mEmptyDrawable);
+        setTransitionProgress(1.0f);
+    }
+
+    /**
+     * Immediately set the front-most drawable layer.
+     *
+     * @param drawable drawable to set
+     */
+    public void setCurrentDrawable(@NonNull Drawable drawable) {
+        setDrawable(0, drawable);
+        applyTransitionProgress(mProgress);
+    }
+
+    /**
+     * Immediately reset the drawable to showing the empty drawable.
+     */
+    public void resetDrawable() {
+        setCurrentDrawable(mEmptyDrawable);
+    }
+
+    /**
+     * Prepare to start animating the transition by pushing the current drawable to the back and
+     * setting a new drawable to the front layer and making it invisible.
+     *
+     * @param endDrawable drawable to animate to
+     */
+    public void startNewTransition(@NonNull Drawable endDrawable) {
+        Drawable oldDrawable = getDrawable(0);
+        setDrawable(1, oldDrawable);
+        setDrawable(0, endDrawable);
+        setTransitionProgress(0.0f);
+    }
+
+    /**
+     * Set the progress of the transition animation to crossfade the two drawables.
+     *
+     * @param progress current transition progress between 0 (front view invisible) and 1
+     *                 (front view visible)
+     */
+    public void setTransitionProgress(float progress) {
+        if (progress > 1 || progress < 0) {
+            throw new IllegalArgumentException("Transition progress should be between 0 and 1");
+        }
+        mProgress = progress;
+        applyTransitionProgress(progress);
+    }
+
+    private void applyTransitionProgress(float progress) {
+        int drawableAlpha = (int) (progress * 255);
+        getDrawable(0).setAlpha(drawableAlpha);
+        if (getDrawable(0) != getDrawable(1)) {
+            // Only do this if it's a different drawable so that it fades out.
+            // Otherwise, we'd just be overwriting the front drawable's alpha.
+            getDrawable(1).setAlpha(255 - drawableAlpha);
+        }
+        invalidateSelf();
+    }
+}
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 089d672..3686493 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -18,39 +18,39 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3.tests">
 
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
 
     <application android:debuggable="true">
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
             android:label="No Config">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/appwidget_no_config" />
+                       android:resource="@xml/appwidget_no_config"/>
         </receiver>
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWdigetHidden"
             android:label="Hidden widget">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                android:resource="@xml/appwidget_hidden" />
+                       android:resource="@xml/appwidget_hidden"/>
         </receiver>
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
             android:label="With Config">
             <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
             </intent-filter>
             <meta-data android:name="android.appwidget.provider"
-                       android:resource="@xml/appwidget_with_config" />
+                       android:resource="@xml/appwidget_with_config"/>
         </receiver>
 
         <activity
@@ -61,8 +61,8 @@
         </activity>
         <activity
             android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
-            android:label="Test Pin Item"
-            android:icon="@drawable/test_drawable_pin_item">
+            android:icon="@drawable/test_drawable_pin_item"
+            android:label="Test Pin Item">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -73,28 +73,28 @@
         <provider
             android:name="com.android.launcher3.testcomponent.TestCommandReceiver"
             android:authorities="${packageName}.commands"
-            android:exported="true" />
+            android:exported="true"/>
 
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
-            android:launchMode="singleTask"
             android:clearTaskOnLaunch="true"
-            android:label="Test launcher"
-            android:stateNotNeeded="true"
-            android:theme="@android:style/Theme.DeviceDefault.Light"
-            android:windowSoftInputMode="adjustPan"
-            android:screenOrientation="unspecified"
             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
-            android:resizeableActivity="true"
-            android:taskAffinity=""
+            android:enabled="false"
+            android:label="Test launcher"
+            android:launchMode="singleTask"
             android:process=":testLauncherProcess"
-            android:enabled="false">
+            android:resizeableActivity="true"
+            android:screenOrientation="unspecified"
+            android:stateNotNeeded="true"
+            android:taskAffinity=""
+            android:theme="@android:style/Theme.DeviceDefault.Light"
+            android:windowSoftInputMode="adjustPan">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.HOME" />
-                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.HOME"/>
+                <category android:name="android.intent.category.DEFAULT"/>
                 <category android:name="android.intent.category.MONKEY"/>
-                <category android:name="android.intent.category.LAUNCHER_APP" />
+                <category android:name="android.intent.category.LAUNCHER_APP"/>
             </intent-filter>
         </activity>
         <activity
@@ -104,6 +104,12 @@
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.launcher3.intent.action.test_shortcut"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+            <meta-data android:name="android.app.shortcuts"
+                       android:resource="@xml/shortcuts"/>
         </activity>
     </application>
 </manifest>
diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml
new file mode 100644
index 0000000..0ad87fb
--- /dev/null
+++ b/tests/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="shortcut1" translatable="false">Shortcut 1</string>
+    <string name="shortcut2" translatable="false">Shortcut 2</string>
+    <string name="shortcut3" translatable="false">Shortcut 3</string>
+</resources>
diff --git a/tests/res/xml/shortcuts.xml b/tests/res/xml/shortcuts.xml
new file mode 100644
index 0000000..bdc22f9
--- /dev/null
+++ b/tests/res/xml/shortcuts.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+    <shortcut
+        android:shortcutId="shortcut1"
+        android:shortcutShortLabel="@string/shortcut1">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut"/>
+    </shortcut>
+    <shortcut
+        android:shortcutId="shortcut2"
+        android:shortcutShortLabel="@string/shortcut2">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut"/>
+    </shortcut>
+    <shortcut
+        android:shortcutId="shortcut3"
+        android:shortcutShortLabel="@string/shortcut3">
+        <intent android:action="com.android.launcher3.intent.action.test_shortcut"/>
+    </shortcut>
+</shortcuts>
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index e11e62e..dfb0edf 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -25,7 +25,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Intent;
-import android.content.pm.LauncherActivityInfo;
 import android.util.Log;
 
 import androidx.test.filters.LargeTest;
@@ -62,6 +61,7 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
     private static final String TAG = "TaplTestsAosp";
+    private static final String APP_NAME = "LauncherTestApp";
 
     private static int sScreenshotCount = 0;
 
@@ -307,12 +307,11 @@
     @PortraitLandscape
     public void testLaunchMenuItem() throws Exception {
         if (!TestHelpers.isInLauncherProcess()) return;
-        final LauncherActivityInfo testApp = getSettingsApp();
 
         final AppIconMenu menu = mLauncher.
                 getWorkspace().
                 switchToAllApps().
-                getAppIcon(testApp.getLabel().toString()).
+                getAppIcon(APP_NAME).
                 openMenu();
 
         executeOnLauncher(
@@ -322,7 +321,7 @@
         final AppIconMenuItem menuItem = menu.getMenuItem(1);
         final String itemName = menuItem.getText();
 
-        menuItem.launch(testApp.getComponentName().getPackageName(), itemName);
+        menuItem.launch(getAppPackageName());
     }
 
     @Test
@@ -330,16 +329,15 @@
     public void testDragAppIcon() throws Throwable {
         try {
             TestProtocol.sDebugTracing = true;
-            final String appName = "LauncherTestApp";
             // 1. Open all apps and wait for load complete.
             // 2. Drag icon to homescreen.
             // 3. Verify that the icon works on homescreen.
             mLauncher.getWorkspace().
                     switchToAllApps().
-                    getAppIcon(appName).
+                    getAppIcon(APP_NAME).
                     dragToWorkspace().
-                    getWorkspaceAppIcon(appName).
-                    launch(getInstrumentation().getContext().getPackageName());
+                    getWorkspaceAppIcon(APP_NAME).
+                    launch(getAppPackageName());
         } finally {
             TestProtocol.sDebugTracing = false;
         }
@@ -349,7 +347,6 @@
     @PortraitLandscape
     public void testDragShortcut() throws Throwable {
         if (!TestHelpers.isInLauncherProcess()) return;
-        LauncherActivityInfo testApp = getSettingsApp();
 
         // 1. Open all apps and wait for load complete.
         // 2. Find the app and long press it to show shortcuts.
@@ -357,7 +354,7 @@
         final AppIconMenuItem menuItem = mLauncher.
                 getWorkspace().
                 switchToAllApps().
-                getAppIcon(testApp.getLabel().toString()).
+                getAppIcon(APP_NAME).
                 openMenu().
                 getMenuItem(0);
         final String shortcutName = menuItem.getText();
@@ -368,6 +365,10 @@
         menuItem.
                 dragToWorkspace().
                 getWorkspaceAppIcon(shortcutName).
-                launch(testApp.getComponentName().getPackageName(), shortcutName);
+                launch(getAppPackageName());
+    }
+
+    private static String getAppPackageName() {
+        return getInstrumentation().getContext().getPackageName();
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index fbeb3a2..44fc3f7 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -50,4 +50,9 @@
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
         return new AppIconMenu(mLauncher, deepShortcutsContainer);
     }
+
+    @Override
+    protected String getLongPressIndicator() {
+        return "deep_shortcuts_container";
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index c39f8d1..ba9c10e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -32,4 +32,9 @@
     public String getText() {
         return mObject.getText();
     }
+
+    @Override
+    protected String getLongPressIndicator() {
+        return "drop_target_bar";
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 16ddba8..1b372ec 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -27,7 +27,7 @@
 /**
  * Ancestor for AppIcon and AppMenuItem.
  */
-class Launchable {
+abstract class Launchable {
     protected final LauncherInstrumentation mLauncher;
 
     protected final UiObject2 mObject;
@@ -45,24 +45,17 @@
      * Clicks the object to launch its app.
      */
     public Background launch(String expectedPackageName) {
-        return launch(expectedPackageName, By.pkg(expectedPackageName).depth(0));
+        return launch(By.pkg(expectedPackageName));
     }
 
-    /**
-     * Clicks the object to launch its app.
-     */
-    public Background launch(String expectedPackageName, String expectedAppText) {
-        return launch(expectedPackageName, By.pkg(expectedPackageName).text(expectedAppText));
-    }
-
-    private Background launch(String errorMessage, BySelector selector) {
+    private Background launch(BySelector selector) {
         LauncherInstrumentation.log("Launchable.launch before click " +
                 mObject.getVisibleCenter());
         mLauncher.assertTrue(
                 "Launching an app didn't open a new window: " + mObject.getText(),
                 mObject.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
         mLauncher.assertTrue(
-                "App didn't start: " + errorMessage,
+                "App didn't start: " + selector,
                 mLauncher.getDevice().wait(Until.hasObject(selector),
                         LauncherInstrumentation.WAIT_TIME_MS));
         return new Background(mLauncher);
@@ -76,10 +69,13 @@
         Workspace.dragIconToWorkspace(
                 mLauncher,
                 this,
-                new Point(device.getDisplayWidth() / 2, device.getDisplayHeight() / 2));
+                new Point(device.getDisplayWidth() / 2, device.getDisplayHeight() / 2),
+                getLongPressIndicator());
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "dragged launchable to workspace")) {
             return new Workspace(mLauncher);
         }
     }
+
+    protected abstract String getLongPressIndicator();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 3a45e93..08f2681 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -201,13 +201,19 @@
 
     Closable addContextLayer(String piece) {
         mDiagnosticContext.addLast(piece);
-        return () -> mDiagnosticContext.removeLast();
+        log("Added context: " + getContextDescription());
+        return () -> {
+            log("Removing context: " + getContextDescription());
+            mDiagnosticContext.removeLast();
+        };
     }
 
     private void fail(String message) {
-        final String ctxt = mDiagnosticContext.isEmpty() ? "" : String.join(", ",
-                mDiagnosticContext) + "; ";
-        Assert.fail("http://go/tapl : " + ctxt + message);
+        Assert.fail("http://go/tapl : " + getContextDescription() + message);
+    }
+
+    private String getContextDescription() {
+        return mDiagnosticContext.isEmpty() ? "" : String.join(", ", mDiagnosticContext) + "; ";
     }
 
     void assertTrue(String message, boolean condition) {
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 943332e..46562ce 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -118,7 +118,7 @@
                     mLauncher,
                     getHotseatAppIcon("Chrome"),
                     new Point(mLauncher.getDevice().getDisplayWidth(),
-                            workspace.getVisibleBounds().centerY()));
+                            workspace.getVisibleBounds().centerY()), "deep_shortcuts_container");
             verifyActiveContainer();
         }
         assertTrue("Home screen workspace didn't become scrollable",
@@ -136,12 +136,13 @@
     }
 
     static void dragIconToWorkspace(
-            LauncherInstrumentation launcher, Launchable launchable, Point dest) {
+            LauncherInstrumentation launcher, Launchable launchable, Point dest,
+            String longPressIndicator) {
         LauncherInstrumentation.log("dragIconToWorkspace: begin");
         final Point launchableCenter = launchable.getObject().getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
         launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, launchableCenter);
-        launcher.waitForLauncherObject("deep_shortcuts_container");
+        launcher.waitForLauncherObject(longPressIndicator);
         launcher.movePointer(downTime, DRAG_DURACTION, launchableCenter, dest);
         launcher.sendPointer(
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest);