Merge "Adding some utility test rules" into udc-qpr-dev
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index bc97df1..32361a8 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -52,6 +52,7 @@
 import androidx.annotation.CallSuper;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.InvariantDeviceProfile;
@@ -93,11 +94,14 @@
     private static final boolean IS_DEBUG = false;
     private static final String TAG = "QuickstepModelDelegate";
 
-    private final PredictorState mAllAppsState =
+    @VisibleForTesting
+    final PredictorState mAllAppsState =
             new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
-    private final PredictorState mHotseatState =
+    @VisibleForTesting
+    final PredictorState mHotseatState =
             new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
-    private final PredictorState mWidgetsRecommendationState =
+    @VisibleForTesting
+    final PredictorState mWidgetsRecommendationState =
             new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");
 
     private final InvariantDeviceProfile mIDP;
@@ -348,12 +352,7 @@
                         .build()));
 
         // TODO: get bundle
-        registerPredictor(mHotseatState, apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(context)
-                        .setUiSurface("hotseat")
-                        .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
-                        .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
-                        .build()));
+        registerHotseatPredictor(apm, context);
 
         registerWidgetsPredictor(apm.createAppPredictionSession(
                 new AppPredictionContext.Builder(context)
@@ -363,6 +362,29 @@
                         .build()));
     }
 
+    @WorkerThread
+    private void recreateHotseatPredictor() {
+        mHotseatState.destroyPredictor();
+        if (!mActive) {
+            return;
+        }
+        Context context = mApp.getContext();
+        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+        registerHotseatPredictor(apm, context);
+    }
+
+    private void registerHotseatPredictor(AppPredictionManager apm, Context context) {
+        registerPredictor(mHotseatState, apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(context)
+                        .setUiSurface("hotseat")
+                        .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
+                        .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
+                        .build()));
+    }
+
     private void registerPredictor(PredictorState state, AppPredictor predictor) {
         state.setTargets(Collections.emptyList());
         state.predictor = predictor;
@@ -393,7 +415,8 @@
         mWidgetsRecommendationState.predictor.requestPredictionUpdate();
     }
 
-    private void onAppTargetEvent(AppTargetEvent event, int client) {
+    @VisibleForTesting
+    void onAppTargetEvent(AppTargetEvent event, int client) {
         PredictorState state;
         switch(client) {
             case CONTAINER_PREDICTION:
@@ -411,6 +434,13 @@
             state.predictor.notifyAppTargetEvent(event);
             Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction()
                     + " launchLocation=" + event.getLaunchLocation());
+            if (state == mHotseatState
+                    && (event.getAction() == AppTargetEvent.ACTION_PIN
+                            || event.getAction() == AppTargetEvent.ACTION_UNPIN)) {
+                // Recreate hot seat predictor when we need to query for hot seat due to pin or
+                // unpin app icons.
+                recreateHotseatPredictor();
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index b2d5940..6818db6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -31,8 +31,11 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
 
+import static java.lang.Math.abs;
+
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -118,6 +121,7 @@
     private final Executor mMainExecutor;
     private final LauncherApps mLauncherApps;
     private final BubbleIconFactory mIconFactory;
+    private final SystemUiProxy mSystemUiProxy;
 
     private BubbleBarItem mSelectedBubble;
     private BubbleBarOverflow mOverflowBubble;
@@ -159,8 +163,10 @@
         mContext = context;
         mBarView = bubbleView; // Need the view for inflating bubble views.
 
+        mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
+
         if (BUBBLE_BAR_ENABLED) {
-            SystemUiProxy.INSTANCE.get(context).setBubblesListener(this);
+            mSystemUiProxy.setBubblesListener(this);
         }
         mMainExecutor = MAIN_EXECUTOR;
         mLauncherApps = context.getSystemService(LauncherApps.class);
@@ -173,7 +179,7 @@
     }
 
     public void onDestroy() {
-        SystemUiProxy.INSTANCE.get(mContext).setBubblesListener(null);
+        mSystemUiProxy.setBubblesListener(null);
     }
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
@@ -240,17 +246,21 @@
             BUBBLE_STATE_EXECUTOR.execute(() -> {
                 createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView);
+                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                            null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
+                    BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(update.updatedBubble, mContext, mBarView);
+                            populateBubble(mContext, update.updatedBubble, mBarView,
+                                    existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
                         BubbleBarBubble b =
-                                populateBubble(update.currentBubbleList.get(i), mContext, mBarView);
+                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
+                                        null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -312,9 +322,11 @@
         mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
 
         if (update.updatedBubble != null) {
-            // TODO: (b/269670235) handle updates:
-            //  (1) if content / icons change -- requires reload & add back in place
-            //  (2) if showing update dot changes -- tell the view to hide / show the dot
+            // Updates mean the dot state may have changed; any other changes were updated in
+            // the populateBubble step.
+            BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
+            // If we're not stashed, we're visible so animate
+            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -331,8 +343,8 @@
             // TODO: (b/273316505) handle suppression
         }
         if (update.selectedBubbleKey != null) {
-            if (mSelectedBubble != null
-                    && !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
+            if (mSelectedBubble == null
+                    || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
                 BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey);
                 if (newlySelected != null) {
                     bubbleToSelect = newlySelected;
@@ -354,12 +366,38 @@
         }
     }
 
+    /** Tells WMShell to show the currently selected bubble. */
+    public void showSelectedBubble() {
+        if (getSelectedBubbleKey() != null) {
+            if (mSelectedBubble instanceof BubbleBarBubble) {
+                // Because we've visited this bubble, we should suppress the notification.
+                // This is updated on WMShell side when we show the bubble, but that update isn't
+                // passed to launcher, instead we apply it directly here.
+                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
+                info.setFlags(
+                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
+            }
+            mSystemUiProxy.showBubble(getSelectedBubbleKey(),
+                    getBubbleBarOffsetX(), getBubbleBarOffsetY());
+        } else {
+            Log.w(TAG, "Trying to show the selected bubble but it's null");
+        }
+    }
+
+    /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */
+    public void showAndSelectBubble(BubbleBarItem b) {
+        if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey());
+        setSelectedBubble(b);
+        showSelectedBubble();
+    }
+
     /**
      * Sets the bubble that should be selected. This notifies the views, it does not notify
-     * WMShell that the selection has changed, that should go through
-     * {@link SystemUiProxy#showBubble}.
+     * WMShell that the selection has changed, that should go through either
+     * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}.
      */
-    public void setSelectedBubble(BubbleBarItem b) {
+    private void setSelectedBubble(BubbleBarItem b) {
         if (!Objects.equals(b, mSelectedBubble)) {
             if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey());
             mSelectedBubble = b;
@@ -383,7 +421,8 @@
     //
 
     @Nullable
-    private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) {
+    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
+            @Nullable BubbleBarBubble existingBubble) {
         String appName;
         Bitmap badgeBitmap;
         Bitmap bubbleBitmap;
@@ -452,16 +491,27 @@
         iconPath.transform(matrix);
         dotPath = iconPath;
         dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA);
+                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
 
-        LayoutInflater inflater = LayoutInflater.from(context);
-        BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
+        if (existingBubble == null) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            BubbleView bubbleView = (BubbleView) inflater.inflate(
+                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
 
-        BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-        bubbleView.setBubble(bubble);
-        return bubble;
+            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(b);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            existingBubble.setDotColor(dotColor);
+            existingBubble.setDotPath(dotPath);
+            existingBubble.setAppName(appName);
+            return existingBubble;
+        }
     }
 
     private BubbleBarOverflow createOverflow(Context context) {
@@ -496,4 +546,13 @@
 
         return mIconFactory.createBadgedIconBitmap(drawable).icon;
     }
+
+    private int getBubbleBarOffsetY() {
+        final int translation = (int) abs(mBubbleStashController.getBubbleBarTranslationY());
+        return translation + mBarView.getHeight();
+    }
+
+    private int getBubbleBarOffsetX() {
+        return mBarView.getWidth() + mBarView.getHorizontalMargin();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 582dcc7..43e21f4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -20,18 +20,18 @@
 import com.android.wm.shell.common.bubbles.BubbleInfo
 
 /** An entity in the bubble bar. */
-sealed class BubbleBarItem(open val key: String, open val view: BubbleView)
+sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
 
 /** Contains state info about a bubble in the bubble bar as well as presentation information. */
 data class BubbleBarBubble(
-    val info: BubbleInfo,
-    override val view: BubbleView,
-    val badge: Bitmap,
-    val icon: Bitmap,
-    val dotColor: Int,
-    val dotPath: Path,
-    val appName: String
+    var info: BubbleInfo,
+    override var view: BubbleView,
+    var badge: Bitmap,
+    var icon: Bitmap,
+    var dotColor: Int,
+    var dotPath: Path,
+    var appName: String
 ) : BubbleBarItem(info.key, view)
 
 /** Represents the overflow bubble in the bubble bar. */
-data class BubbleBarOverflow(override val view: BubbleView) : BubbleBarItem("Overflow", view)
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 563ba02..eec334a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -223,6 +223,12 @@
         setLayoutParams(lp);
     }
 
+    /** @return the horizontal margin between the bubble bar and the edge of the screen. */
+    int getHorizontalMargin() {
+        LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
+        return lp.getMarginEnd();
+    }
+
     /**
      * Updates the z order, positions, and badge visibility of the bubble views in the bar based
      * on the expanded state.
@@ -234,6 +240,7 @@
         final float collapsedWidth = collapsedWidth();
         int bubbleCount = getChildCount();
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+        final boolean animate = getVisibility() == VISIBLE;
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             bv.setTranslationY(ty);
@@ -251,16 +258,14 @@
                 if (widthState == 1f) {
                     bv.setZ(0);
                 }
-                bv.showBadge();
+                // When we're expanded, we're not stacked so we're not behind the stack
+                bv.setBehindStack(false, animate);
             } else {
                 final float targetX = currentWidth - collapsedWidth + collapsedX;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                 bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
-                if (i > 0) {
-                    bv.hideBadge();
-                } else {
-                    bv.showBadge();
-                }
+                // If we're not the first bubble we're behind the stack
+                bv.setBehindStack(i > 0, animate);
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 725f948..8e7fda8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -112,9 +112,7 @@
             setExpanded(false);
             mBubbleStashController.stashBubbleBar();
         } else {
-            mBubbleBarController.setSelectedBubble(bubble);
-            int[] bubbleBarCoords = mBarView.getLocationOnScreen();
-            mSystemUiProxy.showBubble(bubble.getKey(), bubbleBarCoords[0], bubbleBarCoords[1]);
+            mBubbleBarController.showAndSelectBubble(bubble);
         }
     }
 
@@ -142,6 +140,11 @@
         return mBarView.getVisibility() == VISIBLE;
     }
 
+    /** Whether the bubble bar has bubbles. */
+    public boolean hasBubbles() {
+        return mBubbleBarController.getSelectedBubbleKey() != null;
+    }
+
     /**
      * The bounds of the bubble bar.
      */
@@ -291,13 +294,7 @@
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
-                final String selectedKey = mBubbleBarController.getSelectedBubbleKey();
-                if (selectedKey != null) {
-                    int[] bubbleBarCoords = mBarView.getLocationOnScreen();
-                    mSystemUiProxy.showBubble(selectedKey, bubbleBarCoords[0], bubbleBarCoords[1]);
-                } else {
-                    Log.w(TAG, "trying to expand bubbles when there isn't one selected");
-                }
+                mBubbleBarController.showSelectedBubble();
                 mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
                         false /* shouldBubblesFollow */);
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 5177d93..8af4ff9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -125,6 +125,12 @@
     public void setBubblesShowingOnHome(boolean onHome) {
         if (mBubblesShowingOnHome != onHome) {
             mBubblesShowingOnHome = onHome;
+
+            if (!mBarViewController.hasBubbles()) {
+                // if there are no bubbles, there's nothing to show, so just return.
+                return;
+            }
+
             if (mBubblesShowingOnHome) {
                 showBubbleBar(/* expanded= */ false);
                 // When transitioning from app to home the stash animator may already have been
@@ -309,4 +315,9 @@
         return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
                 hotseatCellHeight - mUnstashedHeight) / 2;
     }
+
+    float getBubbleBarTranslationY() {
+        return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
+                : getBubbleBarTranslationYForTaskbar();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 92b76a6..12cb8c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -18,7 +18,9 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Outline;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,10 +30,13 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
+import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
+import com.android.wm.shell.animation.Interpolators;
+
+import java.util.EnumSet;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
-// TODO: (b/269670235) currently this doesn't show the 'update dot'
 
 /**
  * View that displays a bubble icon, along with an app badge on either the left or
@@ -39,14 +44,42 @@
  */
 public class BubbleView extends ConstraintLayout {
 
-    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
     public static final int DEFAULT_PATH_SIZE = 100;
 
+    /**
+     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
+     * another. If any of these flags are set, the dot will not be shown.
+     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
+     */
+    enum SuppressionFlag {
+        // TODO: (b/277815200) implement flyout
+        // Suppressed because the flyout is visible - it will morph into the dot via animation.
+        FLYOUT_VISIBLE,
+        // Suppressed because this bubble is behind others in the collapsed stack.
+        BEHIND_STACK,
+    }
+
+    private final EnumSet<SuppressionFlag> mSuppressionFlags =
+            EnumSet.noneOf(SuppressionFlag.class);
+
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
     private final int mBubbleSize;
 
+    private DotRenderer mDotRenderer;
+    private DotRenderer.DrawParams mDrawParams;
+    private int mDotColor;
+    private Rect mTempBounds = new Rect();
+
+    // Whether the dot is animating
+    private boolean mDotIsAnimating;
+    // What scale value the dot is animating to
+    private float mAnimatingToDotScale;
+    // The current scale value of the dot
+    private float mDotScale;
+
     // TODO: (b/273310265) handle RTL
+    // Whether the bubbles are positioned on the left or right side of the screen
     private boolean mOnLeft = false;
 
     private BubbleBarItem mBubble;
@@ -75,6 +108,8 @@
         mBubbleIcon = findViewById(R.id.icon_view);
         mAppIcon = findViewById(R.id.app_icon_view);
 
+        mDrawParams = new DotRenderer.DrawParams();
+
         setFocusable(true);
         setClickable(true);
         setOutlineProvider(new ViewOutlineProvider() {
@@ -91,17 +126,43 @@
         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
     }
 
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (!shouldDrawDot()) {
+            return;
+        }
+
+        getDrawingRect(mTempBounds);
+
+        mDrawParams.dotColor = mDotColor;
+        mDrawParams.iconBounds = mTempBounds;
+        mDrawParams.leftAlign = mOnLeft;
+        mDrawParams.scale = mDotScale;
+
+        mDotRenderer.draw(canvas, mDrawParams);
+    }
+
     /** Sets the bubble being rendered in this view. */
     void setBubble(BubbleBarBubble bubble) {
         mBubble = bubble;
         mBubbleIcon.setImageBitmap(bubble.getIcon());
         mAppIcon.setImageBitmap(bubble.getBadge());
+        mDotColor = bubble.getDotColor();
+        mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
     }
 
+    /**
+     * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
+     * but does not represent app content, instead it shows recent bubbles that couldn't fit into
+     * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
+     * come from an app.
+     */
     void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
         mBubble = overflow;
         mBubbleIcon.setImageBitmap(bitmap);
-        hideBadge();
+        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
     }
 
     /** Returns the bubble being rendered in this view. */
@@ -110,38 +171,102 @@
         return mBubble;
     }
 
-    /** Shows the app badge on this bubble. */
-    void showBadge() {
+    void updateDotVisibility(boolean animate) {
+        final float targetScale = shouldDrawDot() ? 1f : 0f;
+        if (animate) {
+            animateDotScale();
+        } else {
+            mDotScale = targetScale;
+            mAnimatingToDotScale = targetScale;
+            invalidate();
+        }
+    }
+
+    void updateBadgeVisibility() {
         if (mBubble instanceof BubbleBarOverflow) {
             // The overflow bubble does not have a badge, so just bail.
             return;
         }
         BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
-
         Bitmap appBadgeBitmap = bubble.getBadge();
-        if (appBadgeBitmap == null) {
-            mAppIcon.setVisibility(GONE);
+        int translationX = mOnLeft
+                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
+                : 0;
+        mAppIcon.setTranslationX(translationX);
+        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
+    }
+
+    /** Sets whether this bubble is in the stack & not the first bubble. **/
+    void setBehindStack(boolean behindStack, boolean animate) {
+        if (behindStack) {
+            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
+        } else {
+            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
+        }
+        updateDotVisibility(animate);
+        updateBadgeVisibility();
+    }
+
+    /** Whether this bubble is in the stack & not the first bubble. **/
+    boolean isBehindStack() {
+        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+    }
+
+    /** Whether the dot indicating unseen content in a bubble should be shown. */
+    private boolean shouldDrawDot() {
+        boolean bubbleHasUnseenContent = mBubble != null
+                && mBubble instanceof BubbleBarBubble
+                && mSuppressionFlags.isEmpty()
+                && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
+
+        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
+        // it if the bubble wants to show it, and we aren't suppressing it.
+        return bubbleHasUnseenContent || mDotIsAnimating;
+    }
+
+    /** How big the dot should be, fraction from 0 to 1. */
+    private void setDotScale(float fraction) {
+        mDotScale = fraction;
+        invalidate();
+    }
+
+    /**
+     * Animates the dot to the given scale.
+     */
+    private void animateDotScale() {
+        float toScale = shouldDrawDot() ? 1f : 0f;
+        mDotIsAnimating = true;
+
+        // Don't restart the animation if we're already animating to the given value.
+        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
+            mDotIsAnimating = false;
             return;
         }
 
-        int translationX;
-        if (mOnLeft) {
-            translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
-        } else {
-            translationX = 0;
-        }
+        mAnimatingToDotScale = toScale;
 
-        mAppIcon.setTranslationX(translationX);
-        mAppIcon.setVisibility(VISIBLE);
+        final boolean showDot = toScale > 0f;
+
+        // Do NOT wait until after animation ends to setShowDot
+        // to avoid overriding more recent showDot states.
+        clearAnimation();
+        animate()
+                .setDuration(200)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setUpdateListener((valueAnimator) -> {
+                    float fraction = valueAnimator.getAnimatedFraction();
+                    fraction = showDot ? fraction : 1f - fraction;
+                    setDotScale(fraction);
+                }).withEndAction(() -> {
+                    setDotScale(showDot ? 1f : 0f);
+                    mDotIsAnimating = false;
+                }).start();
     }
 
-    /** Hides the app badge on this bubble. */
-    void hideBadge() {
-        mAppIcon.setVisibility(GONE);
-    }
 
     @Override
     public String toString() {
-        return "BubbleView{" + mBubble + "}";
+        String toString = mBubble != null ? mBubble.getKey() : "null";
+        return "BubbleView{" + toString + "}";
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 9fcadea..d78ca88 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -111,7 +111,7 @@
                 && !toState.overviewUi;
         if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
             setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
-                    .animateAwayPlaceholder(mLauncher));
+                    .createPlaceholderDismissAnim(mLauncher));
             setter.setViewAlpha(
                     mRecentsView.getSplitInstructionsView(),
                     0,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 3b53e8a..fbe0a8f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -33,7 +33,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE;
 import static com.android.launcher3.config.FeatureFlags.RECEIVE_UNFOLD_EVENTS_FROM_SYSUI;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
@@ -406,9 +405,7 @@
     }
 
     private List<SystemShortcut.Factory<QuickstepLauncher>> getSplitShortcuts() {
-
-        if (!ENABLE_SPLIT_FROM_WORKSPACE.get() || !mDeviceProfile.isTablet ||
-                mSplitSelectStateController.isSplitSelectActive()) {
+        if (!mDeviceProfile.isTablet || mSplitSelectStateController.isSplitSelectActive()) {
             return Collections.emptyList();
         }
         RecentsView recentsView = getOverviewPanel();
@@ -549,7 +546,7 @@
         list.add(getDragController());
         Consumer<AnimatorSet> splitAnimator = animatorSet -> {
             AnimatorSet anim = mSplitSelectStateController.getSplitAnimationController()
-                    .animateAwayPlaceholder(QuickstepLauncher.this);
+                    .createPlaceholderDismissAnim(QuickstepLauncher.this);
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -1000,6 +997,13 @@
         return mSplitToWorkspaceController;
     }
 
+    @Override
+    protected void handleSplitAnimationGoingToHome() {
+        super.handleSplitAnimationGoingToHome();
+        mSplitSelectStateController.getSplitAnimationController()
+                .playPlaceholderDismissAnim(this);
+    }
+
     public <T extends OverviewActionsView> T getActionsView() {
         return (T) mActionsView;
     }
@@ -1324,9 +1328,9 @@
                                 : groupTask.mSplitBounds.leftTaskPercent);
     }
 
-    public boolean isCommandQueueEmpty() {
+    public boolean canStartHomeSafely() {
         OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
-        return overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty();
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
     }
 
     private static final class LauncherTaskViewController extends
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 95672f3..25909ac 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -107,8 +107,6 @@
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarUIController;
-import com.android.launcher3.tracing.InputConsumerProto;
-import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
 import com.android.launcher3.util.DisplayController;
@@ -126,7 +124,6 @@
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.InputProxyHandlerFactory;
 import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -2426,7 +2423,6 @@
                 taskViewSimulator.apply(remoteHandle.getTransformParams());
             }
         }
-        ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
 
     // Scaling of RecentsView during quick switch based on amount of recents scroll
@@ -2496,26 +2492,6 @@
                 mRecentsAnimationTargets.nonApps, shown, null /* animatorHandler */);
     }
 
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param inputConsumerProto The parent of this proto message.
-     */
-    public void writeToProto(InputConsumerProto.Builder inputConsumerProto) {
-        SwipeHandlerProto.Builder swipeHandlerProto = SwipeHandlerProto.newBuilder();
-
-        mGestureState.writeToProto(swipeHandlerProto);
-
-        swipeHandlerProto.setIsRecentsAttachedToAppWindow(
-                mAnimationFactory.isRecentsAttachedToAppWindow());
-        swipeHandlerProto.setScrollOffset(mRecentsView == null
-                ? 0
-                : mRecentsView.getScrollOffset());
-        swipeHandlerProto.setAppToOverviewProgress(mCurrentShift.value);
-
-        inputConsumerProto.setSwipeHandler(swipeHandlerProto);
-    }
-
     public interface Factory {
         AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs);
     }
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 3d0f6d5..c7df18f 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -37,8 +37,6 @@
 
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.tracing.GestureStateProto;
-import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -62,24 +60,21 @@
      * Defines the end targets of a gesture and the associated state.
      */
     public enum GestureEndTarget {
-        HOME(true, LAUNCHER_STATE_HOME, false, GestureStateProto.GestureEndTarget.HOME),
+        HOME(true, LAUNCHER_STATE_HOME, false),
 
-        RECENTS(true, LAUNCHER_STATE_OVERVIEW, true, GestureStateProto.GestureEndTarget.RECENTS),
+        RECENTS(true, LAUNCHER_STATE_OVERVIEW, true),
 
-        NEW_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
-                GestureStateProto.GestureEndTarget.NEW_TASK),
+        NEW_TASK(false, LAUNCHER_STATE_BACKGROUND, true),
 
-        LAST_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
-                GestureStateProto.GestureEndTarget.LAST_TASK),
+        LAST_TASK(false, LAUNCHER_STATE_BACKGROUND, true),
 
-        ALL_APPS(true, LAUNCHER_STATE_ALLAPPS, false, GestureStateProto.GestureEndTarget.ALL_APPS);
+        ALL_APPS(true, LAUNCHER_STATE_ALLAPPS, false);
 
-        GestureEndTarget(boolean isLauncher, int containerType, boolean recentsAttachedToAppWindow,
-                GestureStateProto.GestureEndTarget protoEndTarget) {
+        GestureEndTarget(boolean isLauncher, int containerType,
+                boolean recentsAttachedToAppWindow) {
             this.isLauncher = isLauncher;
             this.containerType = containerType;
             this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
-            this.protoEndTarget = protoEndTarget;
         }
 
         /** Whether the target is in the launcher activity. Implicitly, if the end target is going
@@ -89,8 +84,6 @@
         public final int containerType;
         /** Whether RecentsView should be attached to the window as we animate to this target */
         public final boolean recentsAttachedToAppWindow;
-        /** The GestureStateProto enum value, used for winscope tracing. See launcher_trace.proto */
-        public final GestureStateProto.GestureEndTarget protoEndTarget;
     }
 
     private static final String TAG = "GestureState";
@@ -489,17 +482,4 @@
         pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
         pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
     }
-
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param swipeHandlerProto The parent of this proto message.
-     */
-    public void writeToProto(SwipeHandlerProto.Builder swipeHandlerProto) {
-        GestureStateProto.Builder gestureStateProto = GestureStateProto.newBuilder();
-        gestureStateProto.setEndTarget(mEndTarget == null
-                ? GestureStateProto.GestureEndTarget.UNSET
-                : mEndTarget.protoEndTarget);
-        swipeHandlerProto.setGestureState(gestureStateProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 2071103..23def14 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -21,9 +21,6 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-import com.android.launcher3.tracing.InputConsumerProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
-
 @TargetApi(Build.VERSION_CODES.O)
 public interface InputConsumer {
 
@@ -129,21 +126,4 @@
         }
         return name.toString();
     }
-
-    /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param serviceProto The parent of this proto message.
-     */
-    default void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
-        InputConsumerProto.Builder inputConsumerProto = InputConsumerProto.newBuilder();
-        inputConsumerProto.setName(getName());
-        writeToProtoInternal(inputConsumerProto);
-        serviceProto.setInputConsumer(inputConsumerProto);
-    }
-
-    /**
-     * @see #writeToProto - allows subclasses to write additional info to the proto.
-     */
-    default void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {}
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 4a60566..42bf1ac 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -141,8 +141,8 @@
     }
 
     @UiThread
-    public boolean isCommandQueueEmpty() {
-        return mPendingCommands.isEmpty();
+    public boolean canStartHomeSafely() {
+        return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME;
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index a8f3c3a..60713cf 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -38,8 +38,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
-import com.android.launcher3.tracing.OverviewComponentObserverProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
@@ -276,19 +274,6 @@
     }
 
     /**
-     * Used for winscope tracing, see launcher_trace.proto
-     * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
-     * @param serviceProto The parent of this proto message.
-     */
-    public void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
-        OverviewComponentObserverProto.Builder overviewComponentObserver =
-                OverviewComponentObserverProto.newBuilder();
-        overviewComponentObserver.setOverviewActivityStarted(mActivityInterface.isStarted());
-        overviewComponentObserver.setOverviewActivityResumed(mActivityInterface.isResumed());
-        serviceProto.setOverviewComponentObvserver(overviewComponentObserver);
-    }
-
-    /**
      * Starts the intent for the current home activity.
      */
     public static void startHomeIntentSafely(@NonNull Context context, @Nullable Bundle options) {
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index c58d6cc..33a8261 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -470,8 +470,8 @@
         };
     }
 
-    public boolean isCommandQueueEmpty() {
+    public boolean canStartHomeSafely() {
         OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
-        return overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty();
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index d2e7fb5..60784f5 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -646,13 +646,15 @@
     /**
      * Tells SysUI to show the bubble with the provided key.
      * @param key the key of the bubble to show.
-     * @param bubbleBarXCoordinate the X coordinate of the bubble bar on the screen.
-     * @param bubbleBarYCoordinate the Y coordinate of the bubble bar on the screen.
+     * @param bubbleBarOffsetX the offset of the bubble bar from the edge of the screen on the X
+     *                         axis.
+     * @param bubbleBarOffsetY the offset of the bubble bar from the edge of the screen on the Y
+     *                         axis.
      */
-    public void showBubble(String key, int bubbleBarXCoordinate, int bubbleBarYCoordinate) {
+    public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
         if (mBubbles != null) {
             try {
-                mBubbles.showBubble(key, bubbleBarXCoordinate, bubbleBarYCoordinate);
+                mBubbles.showBubble(key, bubbleBarOffsetX, bubbleBarOffsetY);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call showBubble");
             }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index b5c0f7d..a1ecaeb 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -92,8 +92,6 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.LauncherTraceProto;
-import com.android.launcher3.tracing.TouchInteractionServiceProto;
 import com.android.launcher3.uioverrides.flags.FlagsFactory;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.DisplayController;
@@ -117,7 +115,6 @@
 import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
-import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.ProxyScreenStatusProvider;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -126,7 +123,6 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
-import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.bubbles.IBubbles;
@@ -149,8 +145,7 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.R)
-public class TouchInteractionService extends Service
-        implements ProtoTraceable<LauncherTraceProto.Builder> {
+public class TouchInteractionService extends Service {
 
     private static final String SUBSTRING_PREFIX = "; ";
     private static final String NEWLINE_PREFIX = "\n\t\t\t-> ";
@@ -491,8 +486,6 @@
         LockedUserState.get(this).runOnUserUnlocked(this::onUserUnlocked);
         LockedUserState.get(this).runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
-
-        ProtoTracer.INSTANCE.get(this).add(this);
         sConnected = true;
     }
 
@@ -617,19 +610,6 @@
                 // overview.
                 mTaskAnimationManager.endLiveTile();
             }
-
-            if ((lastSysUIFlags & SYSUI_STATE_TRACING_ENABLED) !=
-                    (systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED)) {
-                // Update the tracing state
-                if ((systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED) != 0) {
-                    Log.d(TAG, "Starting tracing.");
-                    ProtoTracer.INSTANCE.get(this).start();
-                } else {
-                    Log.d(TAG, "Stopping tracing. Dumping to file="
-                            + ProtoTracer.INSTANCE.get(this).getTraceFile());
-                    ProtoTracer.INSTANCE.get(this).stop();
-                }
-            }
         }
     }
 
@@ -652,8 +632,6 @@
         disposeEventHandlers("TouchInteractionService onDestroy()");
         mDeviceState.destroy();
         SystemUiProxy.INSTANCE.get(this).clearProxy();
-        ProtoTracer.INSTANCE.get(this).stop();
-        ProtoTracer.INSTANCE.get(this).remove(this);
 
         getSystemService(AccessibilityManager.class)
                 .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
@@ -781,7 +759,6 @@
             reset();
         }
         traceToken.close();
-        ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate();
     }
 
     private InputConsumer tryCreateAssistantInputConsumer(
@@ -1302,8 +1279,6 @@
         pw.println("  mConsumer=" + mConsumer.getName());
         ActiveGestureLog.INSTANCE.dump("", pw);
         RecentsModel.INSTANCE.get(this).dump("", pw);
-        pw.println("ProtoTrace:");
-        pw.println("  file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
         if (createdOverviewActivity != null) {
             createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
         }
@@ -1323,18 +1298,4 @@
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer);
     }
-
-    @Override
-    public void writeToProto(LauncherTraceProto.Builder proto) {
-        TouchInteractionServiceProto.Builder serviceProto =
-            TouchInteractionServiceProto.newBuilder();
-        serviceProto.setServiceConnected(true);
-
-        if (mOverviewComponentObserver != null) {
-            mOverviewComponentObserver.writeToProto(serviceProto);
-        }
-        mConsumer.writeToProto(serviceProto);
-
-        proto.setTouchInteractionService(serviceProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 7dc8347..95d88cd 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -86,8 +86,8 @@
     }
 
     @Override
-    protected boolean isCommandQueueEmpty() {
-        return mActivity.isCommandQueueEmpty();
+    protected boolean canStartHomeSafely() {
+        return mActivity.canStartHomeSafely();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 03f8eef..858999e 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -4,7 +4,6 @@
 
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.quickstep.InputConsumer;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -54,9 +53,4 @@
         mDelegate.onMotionEvent(event);
         event.recycle();
     }
-
-    @Override
-    public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
-        mDelegate.writeToProtoInternal(inputConsumerProto);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 10c6316..2816228 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -48,7 +48,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.AbsSwipeUpHandler;
@@ -509,13 +508,6 @@
         return !mPassedPilferInputSlop;
     }
 
-    @Override
-    public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
-        if (mInteractionHandler != null) {
-            mInteractionHandler.writeToProto(inputConsumerProto);
-        }
-    }
-
     /**
      * A listener which just finishes the animation immediately after starting. Replaces
      * AbsSwipeUpHandler if the gesture itself finishes before the animation even starts.
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 6288937..1f06f94 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -110,8 +110,6 @@
     public static final CopyOnWriteArrayList<StatsLogConsumer> LOGS_CONSUMER =
             new CopyOnWriteArrayList<>();
 
-    private final Context mContext;
-
     public StatsLogCompatManager(Context context) {
         mContext = context;
     }
diff --git a/quickstep/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/src/com/android/quickstep/util/ProtoTracer.java
deleted file mode 100644
index ef9586d..0000000
--- a/quickstep/src/com/android/quickstep/util/ProtoTracer.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.util;
-
-import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H_VALUE;
-import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L_VALUE;
-
-import android.content.Context;
-import android.os.SystemClock;
-
-import android.os.Trace;
-import com.android.launcher3.tracing.LauncherTraceProto;
-import com.android.launcher3.tracing.LauncherTraceEntryProto;
-import com.android.launcher3.tracing.LauncherTraceFileProto;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.tracing.FrameProtoTracer;
-import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
-import com.android.systemui.shared.tracing.ProtoTraceable;
-import com.google.protobuf.MessageLite;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Queue;
-
-
-/**
- * Controller for coordinating winscope proto tracing.
- */
-public class ProtoTracer implements ProtoTraceParams<MessageLite.Builder,
-        LauncherTraceFileProto.Builder, LauncherTraceEntryProto.Builder,
-                LauncherTraceProto.Builder> {
-
-    public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
-            new MainThreadInitializedObject<>(ProtoTracer::new);
-
-    private static final String TAG = "ProtoTracer";
-    private static final long MAGIC_NUMBER_VALUE =
-            ((long) MAGIC_NUMBER_H_VALUE << 32) | MAGIC_NUMBER_L_VALUE;
-
-    private final Context mContext;
-    private final FrameProtoTracer<MessageLite.Builder, LauncherTraceFileProto.Builder,
-        LauncherTraceEntryProto.Builder, LauncherTraceProto.Builder> mProtoTracer;
-
-    public ProtoTracer(Context context) {
-        mContext = context;
-        mProtoTracer = new FrameProtoTracer<>(this);
-    }
-
-    @Override
-    public File getTraceFile() {
-        return new File(mContext.getFilesDir(), "launcher_trace.pb");
-    }
-
-    @Override
-    public LauncherTraceFileProto.Builder getEncapsulatingTraceProto() {
-        return LauncherTraceFileProto.newBuilder();
-    }
-
-    @Override
-    public LauncherTraceEntryProto.Builder updateBufferProto(
-            LauncherTraceEntryProto.Builder reuseObj,
-            ArrayList<ProtoTraceable<LauncherTraceProto.Builder>> traceables) {
-        Trace.beginSection("ProtoTracer.updateBufferProto");
-        LauncherTraceEntryProto.Builder proto = LauncherTraceEntryProto.newBuilder();
-        proto.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
-        LauncherTraceProto.Builder launcherProto = LauncherTraceProto.newBuilder();
-        for (ProtoTraceable t : traceables) {
-            t.writeToProto(launcherProto);
-        }
-        proto.setLauncher(launcherProto);
-        Trace.endSection();
-        return proto;
-    }
-
-    @Override
-    public byte[] serializeEncapsulatingProto(LauncherTraceFileProto.Builder encapsulatingProto,
-            Queue<LauncherTraceEntryProto.Builder> buffer) {
-        Trace.beginSection("ProtoTracer.serializeEncapsulatingProto");
-        encapsulatingProto.setMagicNumber(MAGIC_NUMBER_VALUE);
-        for (LauncherTraceEntryProto.Builder entry : buffer) {
-            encapsulatingProto.addEntry(entry);
-        }
-        byte[] bytes = encapsulatingProto.build().toByteArray();
-        Trace.endSection();
-        return bytes;
-    }
-
-    @Override
-    public byte[] getProtoBytes(MessageLite.Builder proto) {
-        return proto.build().toByteArray();
-    }
-
-    @Override
-    public int getProtoSize(MessageLite.Builder proto) {
-        return proto.build().getSerializedSize();
-    }
-
-    public void start() {
-        mProtoTracer.start();
-    }
-
-    public void stop() {
-        mProtoTracer.stop();
-    }
-
-    public void add(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
-        mProtoTracer.add(traceable);
-    }
-
-    public void remove(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
-        mProtoTracer.remove(traceable);
-    }
-
-    public void scheduleFrameUpdate() {
-        mProtoTracer.scheduleFrameUpdate();
-    }
-
-    public void update() {
-        mProtoTracer.update();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 2e8af4c..5740991 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -17,6 +17,8 @@
 
 package com.android.quickstep.util
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
 import android.graphics.Bitmap
@@ -185,17 +187,32 @@
         }
     }
 
+    /** Does not play any animation if user is not currently in split selection state. */
+    fun playPlaceholderDismissAnim(launcher: Launcher) {
+        if (!splitSelectStateController.isSplitSelectActive) {
+            return
+        }
 
-    fun animateAwayPlaceholder(mLauncher: Launcher) : AnimatorSet {
+        val anim = createPlaceholderDismissAnim(launcher)
+        anim.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                splitSelectStateController.resetState()
+            }
+        })
+        anim.start()
+    }
+
+    /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
+    fun createPlaceholderDismissAnim(launcher: Launcher) : AnimatorSet {
         val animatorSet = AnimatorSet()
-        val recentsView : RecentsView<*, *> = mLauncher.getOverviewPanel()
+        val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
         val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
                 ?: return animatorSet
 
         // We are in split selection state currently, transitioning to another state
-        val dragLayer: DragLayer = mLauncher.dragLayer
+        val dragLayer: DragLayer = launcher.dragLayer
         val onScreenRectF = RectF()
-        Utilities.getBoundsForViewInDragLayer(mLauncher.dragLayer, floatingTask,
+        Utilities.getBoundsForViewInDragLayer(launcher.dragLayer, floatingTask,
                 Rect(0, 0, floatingTask.width, floatingTask.height),
                 false, null, onScreenRectF)
         // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
@@ -214,7 +231,7 @@
                                 floatingTask,
                                 onScreenRectF,
                                 floatingTask.stagePosition,
-                                mLauncher.deviceProfile
+                                launcher.deviceProfile
                         )))
         return animatorSet
     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 24d8326..f3fa86a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -27,21 +27,15 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityManager;
-import android.content.Intent;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.SystemClock;
-import android.os.UserHandle;
-import android.view.View;
 
 import androidx.annotation.BinderThread;
 
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -143,11 +137,7 @@
                     .updateIconInBackground(
                             Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
                                     false /* isLocked */),
-                            (task) -> {
-                                if (task.thumbnail != null) {
-                                    floatingTaskView.setIcon(task.thumbnail.thumbnail);
-                                }
-                            });
+                            (task) -> floatingTaskView.setIcon(task.icon));
             floatingTaskView.setAlpha(1);
             floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
                     false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index a5652dc..f250b8c 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -11,7 +11,6 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -213,8 +212,8 @@
         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
     }
 
-    public void setIcon(Bitmap icon) {
-        mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize);
+    public void setIcon(Drawable drawable) {
+        mSplitPlaceholderView.setIcon(drawable, mSplitHolderSize);
     }
 
     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 3ee9009..80e5a54 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -86,12 +86,16 @@
         StateManager stateManager = mActivity.getStateManager();
         animated &= stateManager.shouldAnimateStateChange();
         stateManager.goToState(NORMAL, animated);
+        if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+            mSplitSelectStateController.getSplitAnimationController()
+                    .playPlaceholderDismissAnim(mActivity);
+        }
         AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
 
     @Override
-    protected boolean isCommandQueueEmpty() {
-        return mActivity.isCommandQueueEmpty();
+    protected boolean canStartHomeSafely() {
+        return mActivity.canStartHomeSafely();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index b8ec5f0..6f16f41 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -165,6 +165,7 @@
 import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.GestureState;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsFilterState;
@@ -2367,14 +2368,14 @@
     }
 
     public void startHome(boolean animated) {
-        if (!isCommandQueueEmpty()) return;
+        if (!canStartHomeSafely()) return;
         handleStartHome(animated);
     }
 
     protected abstract void handleStartHome(boolean animated);
 
-    /** Returns whether the overview command helper queue is empty. */
-    protected abstract boolean isCommandQueueEmpty();
+    /** Returns whether user can start home based on state in {@link OverviewCommandHelper}. */
+    protected abstract boolean canStartHomeSafely();
 
     public void reset() {
         setCurrentTask(-1);
diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
new file mode 100644
index 0000000..a532762
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -0,0 +1,127 @@
+package com.android.launcher3.model
+
+import android.app.prediction.AppPredictor
+import android.app.prediction.AppTarget
+import android.app.prediction.AppTargetEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+import com.android.launcher3.util.LauncherModelHelper
+import org.junit.After
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+/** Unit tests for [QuickstepModelDelegate]. */
+@RunWith(AndroidJUnit4::class)
+class QuickstepModelDelegateTest {
+
+    private lateinit var underTest: QuickstepModelDelegate
+    private lateinit var modelHelper: LauncherModelHelper
+
+    @Mock private lateinit var target: AppTarget
+    @Mock private lateinit var mockedAppTargetEvent: AppTargetEvent
+    @Mock private lateinit var allAppsPredictor: AppPredictor
+    @Mock private lateinit var hotseatPredictor: AppPredictor
+    @Mock private lateinit var widgetRecommendationPredictor: AppPredictor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        modelHelper = LauncherModelHelper()
+        underTest = QuickstepModelDelegate(modelHelper.sandboxContext)
+        underTest.mAllAppsState.predictor = allAppsPredictor
+        underTest.mHotseatState.predictor = hotseatPredictor
+        underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor
+        underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext)
+        underTest.mDataModel = BgDataModel()
+    }
+
+    @After
+    fun tearDown() {
+        modelHelper.destroy()
+    }
+
+    @Test
+    fun onAppTargetEvent_notifyTarget() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION)
+
+        verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+        verifyZeroInteractions(hotseatPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+    }
+
+    @Test
+    fun onWidgetPrediction_notifyWidgetRecommendationPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+        verifyZeroInteractions(hotseatPredictor)
+    }
+
+    @Test
+    fun onHotseatPrediction_notifyHotseatPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+        verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+    }
+
+    @Test
+    fun onOtherClient_notifyHotseatPredictor() {
+        underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS)
+
+        verifyZeroInteractions(allAppsPredictor)
+        verifyZeroInteractions(widgetRecommendationPredictor)
+        verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
+    }
+
+    @Test
+    fun hotseatActionPin_recreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_PIN).build()
+        underTest.markActive()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verify(hotseatPredictor).destroy()
+        assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+
+    @Test
+    fun hotseatActionUnpin_recreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        underTest.markActive()
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
+
+        verify(hotseatPredictor).destroy()
+        assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+
+    @Test
+    fun container_actionPin_notRecreateHotSeat() {
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+        val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
+        underTest.markActive()
+
+        underTest.onAppTargetEvent(appTargetEvent, CONTAINER_PREDICTION)
+
+        verify(allAppsPredictor, never()).destroy()
+        verify(hotseatPredictor, never()).destroy()
+        assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index e92dc8f..4c6874e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -16,11 +16,13 @@
 
 package com.android.quickstep;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -38,12 +40,11 @@
     public void setUp() throws Exception {
         super.setUp();
         TaplTestsLauncher3.initialize(this);
-        mLauncher.setSwipeFromTrackpad(true);
     }
 
     @After
     public void tearDown() {
-        mLauncher.setSwipeFromTrackpad(false);
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.NONE);
     }
 
     @Test
@@ -52,7 +53,30 @@
     public void goHome() throws Exception {
         assumeTrue(mLauncher.isTablet());
 
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
         startTestActivity(2);
         mLauncher.goHome();
     }
+
+    @Test
+    @PortraitLandscape
+    @NavigationModeSwitch
+    public void switchToOverview() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+        startTestActivity(2);
+        mLauncher.goHome().switchToOverview();
+    }
+
+    @Test
+    @PortraitLandscape
+    @NavigationModeSwitch
+    public void testAllAppsFromHome() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+
+        mLauncher.setTrackpadGestureType(TrackpadGestureType.TWO_FINGER);
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToAllApps());
+    }
 }
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 786088e..6796d4b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -438,4 +438,8 @@
     <!--  Folder spaces  -->
     <dimen name="folder_top_padding_default">24dp</dimen>
     <dimen name="folder_footer_horiz_padding">20dp</dimen>
+
+    <!-- Default Ime height. Used only for logging purposes.
+    Assume this is default keyboard height in EN locale in case the keyboard height is not known when queried.-->
+    <dimen name="default_ime_height">300dp</dimen>
 </resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 814a0f9..f3b5155 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -1255,7 +1255,7 @@
             folderFooterHeightPx = mResponsiveFolderHeightSpec.getEndPaddingPx();
 
             folderCellLayoutBorderSpacePx = new Point(mResponsiveFolderWidthSpec.getGutterPx(),
-                    mResponsiveHeightSpec.getGutterPx());
+                    mResponsiveFolderHeightSpec.getGutterPx());
 
             folderContentPaddingLeftRight = mResponsiveFolderWidthSpec.getStartPaddingPx();
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 849db28..d85c384 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -424,6 +424,7 @@
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onCreate 1");
         mStartupLatencyLogger = createStartupLatencyLogger(
                 sIsNewProcess
                         ? LockedUserState.get(this).isUserUnlockedAtLauncherStartup()
@@ -582,6 +583,7 @@
         }
         setTitle(R.string.home_screen);
         mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onCreate 2");
     }
 
     /**
@@ -1056,6 +1058,7 @@
 
     @Override
     protected void onStop() {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStop 1");
         super.onStop();
         if (mDeferOverlayCallbacks) {
             checkIfOverlayStillDeferred();
@@ -1067,10 +1070,12 @@
         mAppWidgetHolder.setActivityStarted(false);
         NotificationListener.removeNotificationsChangedListener(getPopupDataProvider());
         FloatingIconView.resetIconLoadResult();
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStop 2");
     }
 
     @Override
     protected void onStart() {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStart 1");
         TraceHelper.INSTANCE.beginSection(ON_START_EVT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
@@ -1079,6 +1084,7 @@
 
         mAppWidgetHolder.setActivityStarted(true);
         TraceHelper.INSTANCE.endSection();
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onStart 2");
     }
 
     @Override
@@ -1249,6 +1255,7 @@
 
     @Override
     protected void onResume() {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onResume 1");
         TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
         super.onResume();
 
@@ -1260,10 +1267,12 @@
 
         DragView.removeAllViews(this);
         TraceHelper.INSTANCE.endSection();
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onResume 2");
     }
 
     @Override
     protected void onPause() {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onPause 1");
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
@@ -1276,6 +1285,7 @@
             mOverlayManager.onActivityPaused(this);
         }
         mAppWidgetHolder.setActivityResumed(false);
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onPause 2");
     }
 
     /**
@@ -1683,6 +1693,9 @@
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
+            if (FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                handleSplitAnimationGoingToHome();
+            }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
             handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
@@ -1696,6 +1709,11 @@
         TraceHelper.INSTANCE.endSection();
     }
 
+    /** Handle animating away split placeholder view when user taps on home button */
+    protected void handleSplitAnimationGoingToHome() {
+        // Overridden
+    }
+
     protected void toggleAllAppsFromIntent(boolean alreadyOnHome) {
         if (getStateManager().isInStableState(ALL_APPS)) {
             getStateManager().goToState(NORMAL, alreadyOnHome);
@@ -1740,6 +1758,8 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
+        TestProtocol.testLogD(
+                TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onSaveInstanceState 1");
         outState.putIntArray(RUNTIME_STATE_CURRENT_SCREEN_IDS,
                 mWorkspace.getCurrentPageScreenIds().getArray().toArray());
         outState.putInt(RUNTIME_STATE, mStateManager.getState().ordinal);
@@ -1771,10 +1791,13 @@
 
         super.onSaveInstanceState(outState);
         mOverlayManager.onActivitySaveInstanceState(this, outState);
+        TestProtocol.testLogD(
+                TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onSaveInstanceState 2");
     }
 
     @Override
     public void onDestroy() {
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onDestroy 1");
         super.onDestroy();
         ACTIVITY_TRACKER.onActivityDestroyed(this);
 
@@ -1797,6 +1820,7 @@
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
 
         mOverlayManager.onActivityDestroyed(this);
+        TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE, "Launcher.onDestroy 2");
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -2176,6 +2200,8 @@
 
     /**
      * Returns the CellLayout of the specified container at the specified screen.
+     *
+     * @param screenId must be presenterPos and not modelPos.
      */
     public CellLayout getCellLayout(int container, int screenId) {
         return (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
@@ -2961,7 +2987,7 @@
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
         boolean hadWorkApps = mAppsView.shouldShowTabs();
-        AllAppsStore appsStore = mAppsView.getAppsStore();
+        AllAppsStore<Launcher> appsStore = mAppsView.getAppsStore();
         appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
         if (hadWorkApps != mAppsView.shouldShowTabs()) {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index da1bcd7..9b7a05f 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -139,7 +139,7 @@
     private final SearchTransitionController mSearchTransitionController;
     private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private final Rect mInsets = new Rect();
-    private final AllAppsStore mAllAppsStore;
+    private final AllAppsStore<T> mAllAppsStore;
     private final RecyclerView.OnScrollListener mScrollListener =
             new RecyclerView.OnScrollListener() {
                 @Override
@@ -192,7 +192,7 @@
     public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mActivityContext = ActivityContext.lookupContext(context);
-        mAllAppsStore = new AllAppsStore(mActivityContext);
+        mAllAppsStore = new AllAppsStore<>(mActivityContext);
 
         mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
         mHeaderThreshold = getResources().getDimensionPixelSize(
@@ -892,7 +892,7 @@
         container.put(R.id.work_tab_state_id, state);
     }
 
-    public AllAppsStore getAppsStore() {
+    public AllAppsStore<T> getAppsStore() {
         return mAllAppsStore;
     }
 
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 29767bf..0657178 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -69,7 +69,7 @@
     // The set of apps from the system
     private final List<AppInfo> mApps = new ArrayList<>();
     @Nullable
-    private final AllAppsStore mAllAppsStore;
+    private final AllAppsStore<T> mAllAppsStore;
 
     // The number of results in current adapter
     private int mAccessibilityResultsCount = 0;
@@ -86,7 +86,7 @@
     private int mNumAppRowsInAdapter;
     private Predicate<ItemInfo> mItemFilter;
 
-    public AlphabeticalAppsList(Context context, @Nullable AllAppsStore appsStore,
+    public AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore,
             WorkProfileManager workProfileManager) {
         mAllAppsStore = appsStore;
         mActivityContext = ActivityContext.lookupContext(context);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index a070284..5426532 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -182,6 +182,11 @@
             "ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION", DISABLED,
             "Enables predictive back animation from all apps and widgets to home");
 
+    // TODO(Block 11): Clean up flags
+    public static final BooleanFlag ENABLE_PARAMETRIZE_REORDER = getDebugFlag(289420844,
+            "ENABLE_PARAMETRIZE_REORDER", DISABLED,
+            "Enables generating the reorder using a set of parameters");
+
     // TODO(Block 12): Clean up flags
     public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680,
             "ENABLE_MULTI_INSTANCE", DISABLED,
@@ -353,10 +358,6 @@
             "Use inbuilt monochrome icons if app doesn't provide one");
 
     // TODO(Block 28): Clean up flags
-    public static final BooleanFlag ENABLE_SPLIT_FROM_WORKSPACE = getDebugFlag(270393906,
-            "ENABLE_SPLIT_FROM_WORKSPACE", ENABLED,
-            "Enable initiating split screen from workspace.");
-
     public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
             getDebugFlag(270394122, "ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", DISABLED,
                     "Enable splitting from fullscreen app with keyboard shortcuts");
@@ -385,23 +386,23 @@
             "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", DISABLED,
             "Use local overrides for search request timeout");
 
-    // TODO(Block 31)
+    // TODO(Block 31): Clean up flags
     public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
             "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", ENABLED,
             "Use refactored split launching code path");
 
-    // TODO(Block 32): Empty block
-
+    // TODO(Block 32): Clean up flags
     public static final BooleanFlag ENABLE_RESPONSIVE_WORKSPACE = getDebugFlag(241386436,
             "ENABLE_RESPONSIVE_WORKSPACE", DISABLED,
             "Enables new workspace grid calculations method.");
 
     // TODO(Block 33): Clean up flags
-
     public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
             "ENABLE_ALL_APPS_RV_PREINFLATION", DISABLED,
             "Enables preinflating all apps icons to avoid scrolling jank.");
 
+    // TODO(Block 34): Empty block
+
     public static class BooleanFlag {
 
         private final boolean mCurrentValue;
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index f15dc83..7ac2472 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -93,7 +93,7 @@
                         // Move the item from the folder to the workspace, in the position of the
                         // folder
                         CellLayout cellLayout = mLauncher.getCellLayout(info.container,
-                                info.screenId);
+                                mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
                         if (cellLayout == null) {
                             return;
                         }
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index a572a60..66001d8 100644
--- a/src/com/android/launcher3/graphics/SysUiScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -32,6 +32,7 @@
 import android.view.View;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
@@ -87,6 +88,7 @@
     private final View mRoot;
     private final BaseDraggingActivity mActivity;
     private final boolean mHideSysUiScrim;
+    private boolean mSkipScrimAnimationForTest = false;
 
     private boolean mAnimateScrimOnNextDraw = false;
     private final AnimatedFloat mSysUiAnimMultiplier = new AnimatedFloat(this::reapplySysUiAlpha);
@@ -189,6 +191,15 @@
         mBottomMaskRect.set(0, h - mBottomMaskHeight, w, h);
     }
 
+    /**
+     * Sets whether the SysUiScrim should hide for testing.
+     */
+    @VisibleForTesting
+    public void skipScrimAnimation() {
+        mSkipScrimAnimationForTest = true;
+        reapplySysUiAlpha();
+    }
+
     private void reapplySysUiAlpha() {
         reapplySysUiAlphaNoInvalidate();
         if (!mHideSysUiScrim) {
@@ -198,6 +209,7 @@
 
     private void reapplySysUiAlphaNoInvalidate() {
         float factor = mSysUiProgress.value * mSysUiAnimMultiplier.value;
+        if (mSkipScrimAnimationForTest) factor = 1f;
         mBottomMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
         mTopMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
     }
diff --git a/src/com/android/launcher3/logging/KeyboardStateManager.java b/src/com/android/launcher3/logging/KeyboardStateManager.java
index 6dc0a0b..d0f9c74 100644
--- a/src/com/android/launcher3/logging/KeyboardStateManager.java
+++ b/src/com/android/launcher3/logging/KeyboardStateManager.java
@@ -24,7 +24,10 @@
  */
 public class KeyboardStateManager {
     private long mUpdatedTime;
-    private int mImeHeight;
+    private int mImeHeightPx;
+    // Height of the keyboard when it's shown.
+    // mImeShownHeightPx>=mImeHeightPx always.
+    private int mImeShownHeightPx;
 
     public enum KeyboardState {
         NO_IME_ACTION,
@@ -34,8 +37,9 @@
 
     private KeyboardState mKeyboardState;
 
-    public KeyboardStateManager() {
+    public KeyboardStateManager(int defaultImeShownHeightPx) {
         mKeyboardState = NO_IME_ACTION;
+        mImeShownHeightPx = defaultImeShownHeightPx;
     }
 
     /**
@@ -64,13 +68,25 @@
      * Returns keyboard's current height.
      */
     public int getImeHeight() {
-        return mImeHeight;
+        return mImeHeightPx;
     }
 
     /**
-     * Setter method to set keyboard height.
+     * Returns keyboard's height in pixels when shown.
      */
-    public void setImeHeight(int imeHeight) {
-        mImeHeight = imeHeight;
+    public int getImeShownHeight() {
+        return mImeShownHeightPx;
+    }
+
+    /**
+     * Setter method to set keyboard height in pixels.
+     */
+    public void setImeHeight(int imeHeightPx) {
+        mImeHeightPx = imeHeightPx;
+        if (mImeHeightPx > 0) {
+            // Update the mImeShownHeightPx with the actual ime height when shown and store it
+            // for future sessions.
+            mImeShownHeightPx = mImeHeightPx;
+        }
     }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 7e065a9..3e9731382 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -59,6 +59,7 @@
     private InstanceId mInstanceId;
 
     protected @Nullable ActivityContext mActivityContext = null;
+    protected @Nullable Context mContext = null;
     private KeyboardStateManager mKeyboardStateManager;
 
     /**
@@ -1035,7 +1036,9 @@
      */
     public KeyboardStateManager keyboardStateManager() {
         if (mKeyboardStateManager == null) {
-            mKeyboardStateManager = new KeyboardStateManager();
+            mKeyboardStateManager = new KeyboardStateManager(
+                    mContext != null ? mContext.getResources().getDimensionPixelSize(
+                            R.dimen.default_ime_height) : 0);
         }
         return mKeyboardStateManager;
     }
@@ -1071,6 +1074,7 @@
         StatsLogManager manager = Overrides.getObject(StatsLogManager.class,
                 context.getApplicationContext(), R.string.stats_log_manager_class);
         manager.mActivityContext = ActivityContext.lookupContextNoThrow(context);
+        manager.mContext = context;
         return manager;
     }
 }
diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
index f03c62a..2d69bfa 100644
--- a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
+++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
@@ -60,13 +60,15 @@
     private final OnClickListener mOnClickListener;
     private final OnLongClickListener mOnLongClickListener;
     private final SharedPreferences mPrefs;
-    private final AllAppsStore mAllAppsList;
+    private final AllAppsStore<SecondaryDisplayLauncher> mAllAppsList;
     private final AppInfoComparator mAppNameComparator;
 
     private final Set<ComponentKey> mPinnedApps = new HashSet<>();
     private final ArrayList<AppInfo> mItems = new ArrayList<>();
 
-    public PinnedAppsAdapter(SecondaryDisplayLauncher launcher, AllAppsStore allAppsStore,
+    public PinnedAppsAdapter(
+            SecondaryDisplayLauncher launcher,
+            AllAppsStore<SecondaryDisplayLauncher> allAppsStore,
             OnLongClickListener onLongClickListener) {
         mLauncher = launcher;
         mOnClickListener = launcher.getItemOnClickListener();
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index e751730..a10c0ad 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -302,7 +302,7 @@
     public void bindAllApplications(AppInfo[] apps, int flags,
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
-        AllAppsStore appsStore = mAppsView.getAppsStore();
+        AllAppsStore<SecondaryDisplayLauncher> appsStore = mAppsView.getAppsStore();
         appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
     }
@@ -335,7 +335,7 @@
 
     @Override
     public View.OnLongClickListener getAllAppsItemLongClickListener() {
-        return mDragLayer::onIconLongClicked;
+        return v -> mDragLayer.onIconLongClicked(v);
     }
 
     private void onIconClicked(View v) {
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index ad812f0..447d22b 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.app.animation.Interpolators.DECELERATED_EASE;
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
@@ -211,8 +212,8 @@
             if (!config.userControlled) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, EMPHASIZED);
-            config.setInterpolator(ANIM_DEPTH, EMPHASIZED);
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
+            config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
             if (config.userControlled) {
                 config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
@@ -252,8 +253,8 @@
             if (!config.userControlled) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, EMPHASIZED);
-            config.setInterpolator(ANIM_DEPTH, EMPHASIZED);
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
+            config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
             config.setInterpolator(ANIM_DEPTH, config.userControlled ? BLUR_MANUAL : BLUR_ATOMIC);
             config.setInterpolator(ANIM_WORKSPACE_FADE,
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 4073517..75cee2f 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -157,6 +157,7 @@
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
     public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
     public static final String ICON_MISSING = "b/282963545";
+    public static final String ACTIVITY_LIFECYCLE_RULE = "b/289161193";
 
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index c1f2bb6..0d63a68 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -530,7 +530,6 @@
     }
 
     @Test
-    @ScreenRecord // b/258071914
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
     public void testUninstallFromAllApps() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 435649b..b05ebf8 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.platform.test.annotations.PlatinumTest;
+import android.platform.test.rule.ScreenRecordRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -53,7 +54,9 @@
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/289161193
     public void testDragIcon() throws Throwable {
+        mLauncher.enableDebugTracing(); // b/289161193
         new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
@@ -79,6 +82,7 @@
                 DEFAULT_UI_TIMEOUT);
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
+        mLauncher.disableDebugTracing(); // b/289161193
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
index 2eedec3..b5d8193 100644
--- a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
@@ -22,6 +22,8 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.launcher3.testing.shared.TestProtocol;
+
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
@@ -71,33 +73,57 @@
         @Override
         public void onActivityCreated(Activity activity, Bundle bundle) {
             if (activity != null && mClass.isInstance(activity)) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityCreated");
                 mActivity = (T) activity;
             }
         }
 
         @Override
         public void onActivityStarted(Activity activity) {
+            if (activity == mActivity) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityStarted");
+            }
         }
 
         @Override
         public void onActivityResumed(Activity activity) {
+            if (activity == mActivity) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityResumed");
+            }
         }
 
         @Override
         public void onActivityPaused(Activity activity) {
+            if (activity == mActivity) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityPaused");
+            }
         }
 
         @Override
         public void onActivityStopped(Activity activity) {
+            if (activity == mActivity) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onAcgtivityStopped");
+            }
         }
 
         @Override
         public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
+            if (activity == mActivity) {
+                TestProtocol.testLogD(TestProtocol.ACTIVITY_LIFECYCLE_RULE,
+                        "MyStatement.onActivitySaveInstanceState");
+            }
         }
 
         @Override
         public void onActivityDestroyed(Activity activity) {
             if (activity == mActivity) {
+                TestProtocol.testLogD(
+                        TestProtocol.ACTIVITY_LIFECYCLE_RULE, "MyStatement.onActivityDestroyed");
                 mActivity = null;
             }
         }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 31e9aa2..3dab9a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -30,6 +30,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
+import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.util.List;
@@ -75,80 +77,76 @@
     }
 
     protected void goToOverviewUnchecked() {
-        switch (mLauncher.getNavigationModel()) {
-            case ZERO_BUTTON: {
-                final long downTime = SystemClock.uptimeMillis();
-                sendDownPointerToEnterOverviewToLauncher(downTime);
-                String swipeAndHoldToEnterOverviewActionName =
-                        "swiping and holding to enter overview";
-                // If swiping from an app (e.g. Overview is in Background), we pause and hold on
-                // swipe up to make overview appear, or else swiping without holding would take
-                // us to the Home state. If swiping up from Home (e.g. Overview in Home or
-                // Workspace state where the below condition is true), there is no need to pause,
-                // and we will not test for an intermediate carousel as one will not exist.
-                if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) {
-                    mLauncher.runToState(
-                            () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
-                            OVERVIEW_STATE_ORDINAL, swipeAndHoldToEnterOverviewActionName);
-                    sendUpPointerToEnterOverviewToLauncher(downTime);
-                } else {
-                    // If swiping up from an app to overview, pause on intermediate carousel
-                    // until snapshots are visible. No intermediate carousel when swiping from
-                    // Home. The task swiped up is not a snapshot but the TaskViewSimulator. If
-                    // only a single task exists, no snapshots will be available during swipe up.
-                    mLauncher.executeAndWaitForLauncherEvent(
-                            () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
-                            event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(
-                                    event.getClassName().toString()),
-                            () -> "Pause wasn't detected",
-                            swipeAndHoldToEnterOverviewActionName);
-                    try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                            "paused on swipe up to overview")) {
-                        if (mLauncher.getRecentTasks().size() > 1) {
-                            // When swiping up to grid-overview for tablets, the swiped tab will be
-                            // in the middle of the screen (TaskViewSimulator, not a snapshot), and
-                            // all remaining snapshots will be to the left of that task. In
-                            // non-tablet overview, snapshots can be on either side of the swiped
-                            // task, but we still check that they become visible after swiping and
-                            // pausing.
-                            mLauncher.waitForOverviewObject("snapshot");
-                            if (mLauncher.isTablet()) {
-                                List<UiObject2> tasks = mLauncher.getDevice().findObjects(
-                                        mLauncher.getOverviewObjectSelector("snapshot"));
-                                final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
-                                mLauncher.assertTrue(
-                                        "All tasks not to the left of the swiped task",
-                                        tasks.stream()
-                                                .allMatch(
-                                                        t -> t.getVisibleBounds().right < centerX));
-                            }
-
-                        }
-                        String upPointerToEnterOverviewActionName =
-                                "sending UP pointer to enter overview";
-                        mLauncher.runToState(() -> sendUpPointerToEnterOverviewToLauncher(downTime),
-                                OVERVIEW_STATE_ORDINAL, upPointerToEnterOverviewActionName);
-                    }
-                }
-                break;
-            }
-
-            case THREE_BUTTON:
-                if (mLauncher.isTablet()) {
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_TOUCH_DOWN);
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_TOUCH_UP);
-                }
-                if (mLauncher.isTrackpadGestureEnabled()) {
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
-                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
-                }
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+        if (mLauncher.getNavigationModel() == NavigationModel.ZERO_BUTTON
+                || mLauncher.getTrackpadGestureType() == TrackpadGestureType.THREE_FINGER) {
+            final long downTime = SystemClock.uptimeMillis();
+            sendDownPointerToEnterOverviewToLauncher(downTime);
+            String swipeAndHoldToEnterOverviewActionName =
+                    "swiping and holding to enter overview";
+            // If swiping from an app (e.g. Overview is in Background), we pause and hold on
+            // swipe up to make overview appear, or else swiping without holding would take
+            // us to the Home state. If swiping up from Home (e.g. Overview in Home or
+            // Workspace state where the below condition is true), there is no need to pause,
+            // and we will not test for an intermediate carousel as one will not exist.
+            if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) {
                 mLauncher.runToState(
-                        () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
-                        OVERVIEW_STATE_ORDINAL, "clicking Recents button");
-                break;
+                        () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
+                        OVERVIEW_STATE_ORDINAL, swipeAndHoldToEnterOverviewActionName);
+                sendUpPointerToEnterOverviewToLauncher(downTime);
+            } else {
+                // If swiping up from an app to overview, pause on intermediate carousel
+                // until snapshots are visible. No intermediate carousel when swiping from
+                // Home. The task swiped up is not a snapshot but the TaskViewSimulator. If
+                // only a single task exists, no snapshots will be available during swipe up.
+                mLauncher.executeAndWaitForLauncherEvent(
+                        () -> sendSwipeUpAndHoldToEnterOverviewGestureToLauncher(downTime),
+                        event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(
+                                event.getClassName().toString()),
+                        () -> "Pause wasn't detected",
+                        swipeAndHoldToEnterOverviewActionName);
+                try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                        "paused on swipe up to overview")) {
+                    if (mLauncher.getRecentTasks().size() > 1) {
+                        // When swiping up to grid-overview for tablets, the swiped tab will be
+                        // in the middle of the screen (TaskViewSimulator, not a snapshot), and
+                        // all remaining snapshots will be to the left of that task. In
+                        // non-tablet overview, snapshots can be on either side of the swiped
+                        // task, but we still check that they become visible after swiping and
+                        // pausing.
+                        mLauncher.waitForOverviewObject("snapshot");
+                        if (mLauncher.isTablet()) {
+                            List<UiObject2> tasks = mLauncher.getDevice().findObjects(
+                                    mLauncher.getOverviewObjectSelector("snapshot"));
+                            final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
+                            mLauncher.assertTrue(
+                                    "All tasks not to the left of the swiped task",
+                                    tasks.stream()
+                                            .allMatch(
+                                                    t -> t.getVisibleBounds().right < centerX));
+                        }
+
+                    }
+                    String upPointerToEnterOverviewActionName =
+                            "sending UP pointer to enter overview";
+                    mLauncher.runToState(() -> sendUpPointerToEnterOverviewToLauncher(downTime),
+                            OVERVIEW_STATE_ORDINAL, upPointerToEnterOverviewActionName);
+                }
+            }
+        } else {
+            if (mLauncher.isTablet()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+                        LauncherInstrumentation.EVENT_TOUCH_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+                        LauncherInstrumentation.EVENT_TOUCH_UP);
+            }
+            if (mLauncher.isTrackpadGestureEnabled()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
+            }
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+            mLauncher.runToState(
+                    () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
+                    OVERVIEW_STATE_ORDINAL, "clicking Recents button");
         }
         expectSwitchToOverviewEvents();
     }
@@ -263,10 +261,8 @@
                         endY = startY;
                     }
 
-                    final boolean isZeroButton = mLauncher.getNavigationModel()
-                            == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
                     LauncherInstrumentation.GestureScope gestureScope =
-                            launcherWasVisible && isZeroButton
+                            launcherWasVisible
                                     ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
                                     : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
                     mLauncher.executeAndWaitForEvent(
@@ -326,6 +322,8 @@
     }
 
     protected int getSwipeStartY() {
-        return mLauncher.getRealDisplaySize().y - 1;
+        return mLauncher.getTrackpadGestureType() == TrackpadGestureType.THREE_FINGER
+                ? mLauncher.getDevice().getDisplayHeight() * 3 / 4
+                : mLauncher.getRealDisplaySize().y - 1;
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index ce50e22..5b67d73 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -139,6 +139,13 @@
         OUTSIDE_WITH_KEYCODE,
     }
 
+    public enum TrackpadGestureType {
+        NONE,
+        TWO_FINGER,
+        THREE_FINGER,
+        FOUR_FINGER
+    }
+
     // Base class for launcher containers.
     abstract static class VisibleContainer {
         protected final LauncherInstrumentation mLauncher;
@@ -198,7 +205,7 @@
     private boolean mCheckEventsForSuccessfulGestures = false;
     private Runnable mOnLauncherCrashed;
 
-    private boolean mSwipeFromTrackpad = false;
+    private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
     private int mPointerCount = 0;
 
     private static Pattern getTouchEventPattern(String prefix, String action) {
@@ -715,8 +722,17 @@
         mIgnoreTaskbarVisibility = ignoreTaskbarVisibility;
     }
 
-    public void setSwipeFromTrackpad(boolean swipeFromTrackpad) {
-        mSwipeFromTrackpad = swipeFromTrackpad;
+    /**
+     * Set the trackpad gesture type of the interaction.
+     * @param trackpadGestureType whether it's not from trackpad, two-finger, three-finger, or
+     *                            four-finger gesture.
+     */
+    public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
+        mTrackpadGestureType = trackpadGestureType;
+    }
+
+    TrackpadGestureType getTrackpadGestureType() {
+        return mTrackpadGestureType;
     }
 
     /**
@@ -1008,8 +1024,11 @@
             // We need waiting for any accessibility event generated after pressing Home because
             // otherwise waitForIdle may return immediately in case when there was a big enough
             // pause in accessibility events prior to pressing Home.
+            boolean isThreeFingerTrackpadGesture =
+                    mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
             final String action;
-            if (getNavigationModel() == NavigationModel.ZERO_BUTTON || mSwipeFromTrackpad) {
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+                    || isThreeFingerTrackpadGesture) {
                 checkForAnomaly(false, true);
 
                 final Point displaySize = getRealDisplaySize();
@@ -1025,8 +1044,9 @@
                 } else {
                     action = "swiping up to home";
 
-                    int startY = mSwipeFromTrackpad ? displaySize.y * 3 / 4 : displaySize.y - 1;
-                    int endY = mSwipeFromTrackpad ? displaySize.y / 4 : displaySize.y / 2;
+                    int startY = isThreeFingerTrackpadGesture ? displaySize.y * 3 / 4
+                            : displaySize.y - 1;
+                    int endY = isThreeFingerTrackpadGesture ? displaySize.y / 4 : displaySize.y / 2;
                     swipeToState(
                             displaySize.x / 2, startY,
                             displaySize.x / 2, endY,
@@ -1070,15 +1090,18 @@
             waitForLauncherInitialized();
             final boolean launcherVisible =
                     isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
-            if (getNavigationModel() == NavigationModel.ZERO_BUTTON || mSwipeFromTrackpad) {
+            boolean isThreeFingerTrackpadGesture =
+                    mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+                    || isThreeFingerTrackpadGesture) {
                 final Point displaySize = getRealDisplaySize();
                 final GestureScope gestureScope =
                         launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
                                 : GestureScope.OUTSIDE_WITH_KEYCODE;
                 // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
                 //  issue is solved.
-                int startX = mSwipeFromTrackpad ? displaySize.x / 4 : 0;
-                int endX = mSwipeFromTrackpad ? displaySize.x * 3 / 4 : displaySize.x / 2;
+                int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
+                int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
                 linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
                         10, false, gestureScope);
             } else {
@@ -1615,19 +1638,29 @@
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
-        if (mSwipeFromTrackpad) {
+        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
             sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
                     start, gestureScope);
-            sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
-                    start, gestureScope);
+            if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
+                    || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                sendPointer(downTime, downTime,
+                        getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
+                        start, gestureScope);
+                if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                    sendPointer(downTime, downTime,
+                            getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
+                            start, gestureScope);
+                }
+            }
         }
         final long endTime = movePointer(
                 start, end, steps, false, downTime, downTime, slowDown, gestureScope);
-        if (mSwipeFromTrackpad) {
-            sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_UP, 2),
-                    start, gestureScope);
-            sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_UP, 1),
-                    start, gestureScope);
+        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
+            for (int i = mPointerCount; i >= 2; i--) {
+                sendPointer(downTime, downTime,
+                        getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
+                        start, gestureScope);
+            }
         }
         sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
     }
@@ -1659,20 +1692,25 @@
         return getContext().getResources();
     }
 
-    private static MotionEvent getTrackpadThreeFingerMotionEvent(long downTime, long eventTime,
-            int action, float x, float y, int pointerCount) {
+    private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime,
+            int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) {
         MotionEvent.PointerProperties[] pointerProperties =
                 new MotionEvent.PointerProperties[pointerCount];
         MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+        boolean isMultiFingerGesture = gestureType != TrackpadGestureType.TWO_FINGER;
         for (int i = 0; i < pointerCount; i++) {
             pointerProperties[i] = getPointerProperties(i);
             pointerCoords[i] = getPointerCoords(x, y);
-            pointerCoords[i].setAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT, 3);
+            if (isMultiFingerGesture) {
+                pointerCoords[i].setAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT,
+                        gestureType == TrackpadGestureType.THREE_FINGER ? 3 : 4);
+            }
         }
         return MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties,
                 pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
                 InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_CLASS_POINTER, 0, 0,
-                MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE);
+                isMultiFingerGesture ? MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+                        : MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE);
     }
 
     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
@@ -1712,22 +1750,25 @@
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope) {
         final boolean hasTIS = hasTIS();
-        int pointerCount = 1;
+        int pointerCount = mPointerCount;
 
+        boolean isTrackpadGesture = mTrackpadGestureType != TrackpadGestureType.NONE;
+        boolean isTwoFingerTrackpadGesture = mTrackpadGestureType == TrackpadGestureType.TWO_FINGER;
         switch (action & MotionEvent.ACTION_MASK) {
             case MotionEvent.ACTION_DOWN:
                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
-                        && !mSwipeFromTrackpad) {
+                        && (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
                 }
                 if (hasTIS && (isTrackpadGestureEnabled()
                         || getNavigationModel() != NavigationModel.THREE_BUTTON)) {
                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
                 }
-                if (mSwipeFromTrackpad) {
+                if (isTrackpadGesture) {
                     mPointerCount = 1;
+                    pointerCount = mPointerCount;
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -1740,7 +1781,7 @@
                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
-                        && !mSwipeFromTrackpad) {
+                        && (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN,
                             gestureScope == GestureScope.INSIDE
                                     || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
@@ -1767,8 +1808,6 @@
                 pointerCount = mPointerCount;
                 break;
             case MotionEvent.ACTION_POINTER_UP:
-                pointerCount = mPointerCount;
-
                 // When the gesture is handled outside, it's cancelled within launcher.
                 if (gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
@@ -1779,9 +1818,10 @@
                 break;
         }
 
-        final MotionEvent event = mSwipeFromTrackpad
-                ? getTrackpadThreeFingerMotionEvent(
-                        downTime, currentTime, action, point.x, point.y, pointerCount)
+        final MotionEvent event = isTrackpadGesture
+                ? getTrackpadMotionEvent(
+                        downTime, currentTime, action, point.x, point.y, pointerCount,
+                        mTrackpadGestureType)
                 : getMotionEvent(downTime, currentTime, action, point.x, point.y);
         assertTrue("injectInputEvent failed",
                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));