Merge "Fix responsive folders" into udc-qpr-dev
diff --git a/quickstep/res/drawable/bg_bubble_dismiss_circle.xml b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
new file mode 100644
index 0000000..b793eec
--- /dev/null
+++ b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <stroke
+        android:width="2dp"
+        android:color="@android:color/system_accent1_600" />
+
+    <solid android:color="@android:color/system_accent1_600" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
new file mode 100644
index 0000000..b15111b
--- /dev/null
+++ b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2023 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+        android:fillColor="@android:color/system_neutral1_50"/>
+</vector>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e4f6555..6140d14 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -367,6 +367,13 @@
     <dimen name="bubblebar_icon_spacing">3dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
 
+    <!-- Bubble bar dismiss view -->
+    <dimen name="bubblebar_dismiss_target_size">96dp</dimen>
+    <dimen name="bubblebar_dismiss_target_small_size">60dp</dimen>
+    <dimen name="bubblebar_dismiss_target_icon_size">24dp</dimen>
+    <dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
+    <dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
+
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
     <!--     starting_surface_exit_animation_window_shift_length -->
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 42cb290..cb9c329 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -90,6 +90,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
+import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
@@ -216,7 +218,9 @@
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView),
                     new BubbleStashController(this),
-                    new BubbleStashedHandleViewController(this, bubbleHandleView)));
+                    new BubbleStashedHandleViewController(this, bubbleHandleView),
+                    new BubbleDragController(this),
+                    new BubbleDismissController(this, mDragLayer)));
         }
 
         // Construct controllers.
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..e93d410 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -95,6 +95,8 @@
     private View.OnClickListener mOnClickListener;
 
     private final Rect mTempRect = new Rect();
+    private float mRelativePivotX = 1f;
+    private float mRelativePivotY = 1f;
 
     // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
     // collapsed state and 1 to the fully expanded state.
@@ -109,6 +111,9 @@
     @Nullable
     private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
 
+    @Nullable
+    private BubbleView mDraggedBubbleView;
+
     public BubbleBarView(Context context) {
         this(context, null);
     }
@@ -181,9 +186,10 @@
         mBubbleBarBounds.right = right;
         mBubbleBarBounds.bottom = bottom;
 
-        // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
-        setPivotX(getWidth());
-        setPivotY(getHeight());
+        // The bubble bar handle is aligned according to the relative pivot,
+        // by default it's aligned to the bottom edge of the screen so scale towards that
+        setPivotX(mRelativePivotX * getWidth());
+        setPivotY(mRelativePivotY * getHeight());
 
         // Position the views
         updateChildrenRenderNodeProperties();
@@ -198,6 +204,32 @@
         return mBubbleBarBounds;
     }
 
+    /**
+     * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
+     * respectively. If the value is not in range of 0 to 1 it will be normalized.
+     * @param x relative X pivot value in range 0..1
+     * @param y relative Y pivot value in range 0..1
+     */
+    public void setRelativePivot(float x, float y) {
+        mRelativePivotX = Float.max(Float.min(x, 1), 0);
+        mRelativePivotY = Float.max(Float.min(y, 1), 0);
+        requestLayout();
+    }
+
+    /**
+     * Get current relative pivot for X axis
+     */
+    public float getRelativePivotX() {
+        return mRelativePivotX;
+    }
+
+    /**
+     * Get current relative pivot for Y axis
+     */
+    public float getRelativePivotY() {
+        return mRelativePivotY;
+    }
+
     // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -223,6 +255,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 +272,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);
@@ -247,20 +286,18 @@
                 // where the bubble will end up when the animation ends
                 final float targetX = currentWidth - expandedWidth + expandedX;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
-                // if we're fully expanded, set the z level to 0
+                // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
                 if (widthState == 1f) {
-                    bv.setZ(0);
+                    bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 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);
             }
         }
 
@@ -324,6 +361,14 @@
     }
 
     /**
+     * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
+     */
+    public void setDraggedBubble(@Nullable BubbleView view) {
+        mDraggedBubbleView = view;
+        requestLayout();
+    }
+
+    /**
      * Update the arrow position to match the selected bubble.
      *
      * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 4e9f88a..2e3c701 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -24,6 +24,8 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -54,6 +56,7 @@
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
     private BubbleBarController mBubbleBarController;
+    private BubbleDragController mBubbleDragController;
     private TaskbarStashController mTaskbarStashController;
     private TaskbarInsetsController mTaskbarInsetsController;
     private View.OnClickListener mBubbleClickListener;
@@ -85,6 +88,7 @@
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
+        mBubbleDragController = bubbleControllers.bubbleDragController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
 
@@ -95,6 +99,7 @@
         mBubbleBarScale.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked(v);
         mBubbleBarClickListener = v -> setExpanded(true);
+        mBubbleDragController.setupBubbleBarView(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
                 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -112,9 +117,7 @@
             setExpanded(false);
             mBubbleStashController.stashBubbleBar();
         } else {
-            mBubbleBarController.setSelectedBubble(bubble);
-            mSystemUiProxy.showBubble(bubble.getKey(),
-                    mBubbleStashController.isBubblesShowingOnHome());
+            mBubbleBarController.showAndSelectBubble(bubble);
         }
     }
 
@@ -258,6 +261,7 @@
         if (b != null) {
             mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
             b.getView().setOnClickListener(mBubbleClickListener);
+            mBubbleDragController.setupBubbleView(b.getView());
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
         }
@@ -291,13 +295,7 @@
             if (!isExpanded) {
                 mSystemUiProxy.collapseBubbles();
             } else {
-                final String selectedKey = mBubbleBarController.getSelectedBubbleKey();
-                if (selectedKey != null) {
-                    mSystemUiProxy.showBubble(selectedKey,
-                            mBubbleStashController.isBubblesShowingOnHome());
-                } else {
-                    Log.w(TAG, "trying to expand bubbles when there isn't one selected");
-                }
+                mBubbleBarController.showSelectedBubble();
                 mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
                         false /* shouldBubblesFollow */);
             }
@@ -315,4 +313,41 @@
             mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
         }
     }
+
+    /**
+     * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
+     * to collapse the selected bubble while it's dragged.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragStart(@NonNull BubbleView bubbleView) {
+        mBarView.setDraggedBubble(bubbleView);
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.collapseWhileDragging(bubbleView.getBubble().getKey(), true /* collapse */);
+    }
+
+    /**
+     * Removes the dragged bubble view in the bubble bar view, and notifies SystemUI
+     * to expand the selected bubble when dragging finished.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragEnd(@NonNull BubbleView bubbleView) {
+        mBarView.setDraggedBubble(null);
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.collapseWhileDragging(bubbleView.getBubble().getKey(), false /* collapse */);
+    }
+
+    /**
+     * Called when bubble was dragged into the dismiss target. Notifies System
+     * @param bubble dismissed bubble item
+     */
+    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+        mSystemUiProxy.removeBubble(bubble.getKey());
+    }
+
+    /**
+     * Called when bubble stack was dragged into the dismiss target
+     */
+    public void onDismissAllBubblesWhileDragging() {
+        mSystemUiProxy.removeAllBubbles();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 6417f3c..c47427d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -27,6 +27,8 @@
     public final BubbleBarViewController bubbleBarViewController;
     public final BubbleStashController bubbleStashController;
     public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+    public final BubbleDragController bubbleDragController;
+    public final BubbleDismissController bubbleDismissController;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -39,11 +41,15 @@
             BubbleBarController bubbleBarController,
             BubbleBarViewController bubbleBarViewController,
             BubbleStashController bubbleStashController,
-            BubbleStashedHandleViewController bubbleStashedHandleViewController) {
+            BubbleStashedHandleViewController bubbleStashedHandleViewController,
+            BubbleDragController bubbleDragController,
+            BubbleDismissController bubbleDismissController) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
         this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
+        this.bubbleDragController = bubbleDragController;
+        this.bubbleDismissController = bubbleDismissController;
     }
 
     /**
@@ -56,6 +62,8 @@
         bubbleBarViewController.init(taskbarControllers, this);
         bubbleStashedHandleViewController.init(taskbarControllers, this);
         bubbleStashController.init(taskbarControllers, this);
+        bubbleDragController.init(/* bubbleControllers = */ this);
+        bubbleDismissController.init(/* bubbleControllers = */ this);
 
         mPostInitRunnables.executeAllAndDestroy();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
new file mode 100644
index 0000000..6063376
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2023 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.launcher3.taskbar.bubbles;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.os.SystemProperties;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarDragLayer;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls dismiss view presentation for the bubble bar dismiss functionality.
+ * Provides the dragged view snapping to the target dismiss area and animates it.
+ * When the dragged bubble/bubble stack is realised inside of the target area, it gets dismissed.
+ *
+ * @see BubbleDragController
+ */
+public class BubbleDismissController {
+    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+    public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
+            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
+    private final TaskbarActivityContext mActivity;
+    private final TaskbarDragLayer mDragLayer;
+    @Nullable
+    private BubbleBarViewController mBubbleBarViewController;
+
+    // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
+    // dismiss view used as a dismiss target.
+    @Nullable
+    private DismissView mDismissView;
+
+    // The currently magnetized object, which is being dragged and will be attracted to the magnetic
+    // dismiss target. This is either the stack itself, or an individual bubble.
+    @Nullable
+    private MagnetizedObject<View> mMagnetizedObject;
+
+    // The MagneticTarget instance for our circular dismiss view. This is added to the
+    // MagnetizedObject instances for the stack and any dragged-out bubbles.
+    @Nullable
+    private MagnetizedObject.MagneticTarget mMagneticTarget;
+    @Nullable
+    private ValueAnimator mDismissAnimator;
+
+    public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+        mActivity = activity;
+        mDragLayer = dragLayer;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+    }
+
+    /**
+     * Setup the dismiss view and magnetized object that will be attracted to magnetic target.
+     * Should be called before handling events or showing/hiding dismiss view.
+     * @param magnetizedView the view to be pulled into target dismiss area
+     */
+    public void setupDismissView(@NonNull View magnetizedView) {
+        setupDismissView();
+        setupMagnetizedObject(magnetizedView);
+    }
+
+    /**
+     * Handle the touch event and pass it to the magnetized object.
+     * It should be called after {@code setupDismissView}
+     */
+    public boolean handleTouchEvent(@NonNull MotionEvent event) {
+        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    /**
+     * Show dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void showDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.show();
+    }
+
+    /**
+     * Hide dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void hideDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.hide();
+    }
+
+    /**
+     * Dismiss magnetized object when it's released in the dismiss target area
+     */
+    private void dismissMagnetizedObject() {
+        if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
+        if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
+            BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
+            if (bubbleView.getBubble() != null) {
+                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+            }
+        } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
+            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+        }
+        cleanUpAnimatedViews();
+    }
+
+    /**
+     * Animate dismiss view when magnetized object gets stuck in the magnetic target
+     * @param view captured view
+     */
+    private void animateDismissCaptured(@NonNull View view) {
+        cancelAnimations();
+        mDismissAnimator = createDismissAnimator(view);
+        mDismissAnimator.start();
+    }
+
+    /**
+     * Animate dismiss view when magnetized object gets unstuck from the magnetic target
+     */
+    private void animateDismissReleased() {
+        if (mDismissAnimator == null) return;
+        mDismissAnimator.removeAllListeners();
+        mDismissAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                cancelAnimations();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                cancelAnimations();
+            }
+        });
+        mDismissAnimator.reverse();
+    }
+
+    /**
+     * Cancel and clear dismiss animations and reset view states
+     */
+    private void cancelAnimations() {
+        if (mDismissAnimator == null) return;
+        ValueAnimator animator = mDismissAnimator;
+        mDismissAnimator = null;
+        animator.cancel();
+    }
+
+    /**
+     * Clean up views changed during animation
+     */
+    private void cleanUpAnimatedViews() {
+        // Cancel animations
+        cancelAnimations();
+        // Reset dismiss view
+        if (mDismissView != null) {
+            mDismissView.getCircle().setScaleX(1f);
+            mDismissView.getCircle().setScaleY(1f);
+        }
+        // Reset magnetized view
+        if (mMagnetizedObject != null) {
+            mMagnetizedObject.getUnderlyingObject().setAlpha(1f);
+            mMagnetizedObject.getUnderlyingObject().setScaleX(1f);
+            mMagnetizedObject.getUnderlyingObject().setScaleY(1f);
+        }
+    }
+
+    private void setupDismissView() {
+        if (mDismissView != null) return;
+        mDismissView = new DismissView(mActivity.getApplicationContext());
+        BubbleDismissViewUtils.setup(mDismissView);
+        mDragLayer.addView(mDismissView, /* index = */ 0,
+                new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+        setupMagneticTarget(mDismissView.getCircle());
+    }
+
+    private void setupMagneticTarget(@NonNull View view) {
+        int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_size);
+        mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
+    }
+
+    private void setupMagnetizedObject(@NonNull View magnetizedView) {
+        mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
+                magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+            @Override
+            public float getWidth(@NonNull View underlyingObject) {
+                return underlyingObject.getWidth();
+            }
+
+            @Override
+            public float getHeight(@NonNull View underlyingObject) {
+                return underlyingObject.getHeight();
+            }
+
+            @Override
+            public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+                underlyingObject.getLocationOnScreen(loc);
+            }
+        };
+
+        mMagnetizedObject.setHapticsEnabled(true);
+        mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
+        mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+        if (mMagneticTarget != null) {
+            mMagnetizedObject.addTarget(mMagneticTarget);
+        }
+        mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
+            @Override
+            public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                animateDismissCaptured(magnetizedView);
+            }
+
+            @Override
+            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                    float velX, float velY, boolean wasFlungOut) {
+                animateDismissReleased();
+            }
+
+            @Override
+            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                dismissMagnetizedObject();
+            }
+        });
+    }
+
+    private ValueAnimator createDismissAnimator(@NonNull View magnetizedView) {
+        Resources resources = mActivity.getResources();
+        int expandedSize = resources.getDimensionPixelSize(R.dimen.bubblebar_dismiss_target_size);
+        int collapsedSize = resources.getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_small_size);
+        float minScale = (float) collapsedSize / expandedSize;
+        ValueAnimator animator = ValueAnimator.ofFloat(1f, minScale);
+        animator.addUpdateListener(animation -> {
+            if (mDismissView == null) return;
+            final float animatedValue = (float) animation.getAnimatedValue();
+            mDismissView.getCircle().setScaleX(animatedValue);
+            mDismissView.getCircle().setScaleY(animatedValue);
+            magnetizedView.setAlpha(animatedValue);
+            if (magnetizedView instanceof BubbleBarView) {
+                magnetizedView.setScaleX(animatedValue);
+                magnetizedView.setScaleY(animatedValue);
+            }
+        });
+        return animator;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
new file mode 100644
index 0000000..4b235a9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+@file:JvmName("BubbleDismissViewUtils")
+
+package com.android.launcher3.taskbar.bubbles
+
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.DismissView
+
+/**
+ * Dismiss view is shared from WMShell. It requires setup with local resources.
+ *
+ * Usage:
+ * - Kotlin `dismissView.setup()`
+ * - Java `BubbleDismissViewUtils.setup(dismissView)`
+ */
+fun DismissView.setup() {
+    setup(
+        DismissView.Config(
+            targetSizeResId = R.dimen.bubblebar_dismiss_target_size,
+            iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
+            bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
+            floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height,
+            floatingGradientColorResId = android.R.color.system_neutral1_900,
+            backgroundResId = R.drawable.bg_bubble_dismiss_circle,
+            iconResId = R.drawable.ic_bubble_dismiss_white
+        )
+    )
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
new file mode 100644
index 0000000..28dc62c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 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.launcher3.taskbar.bubbles;
+
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.wm.shell.common.bubbles.RelativeTouchListener;
+
+/**
+ * Controls bubble bar drag to dismiss interaction.
+ * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
+ * Supported interactions:
+ * - Drag a single bubble view into dismiss target to remove it.
+ * - Drag the bubble stack into dismiss target to remove all.
+ * Restores initial position of dragged view if released outside of the dismiss target.
+ */
+public class BubbleDragController {
+    private final TaskbarActivityContext mActivity;
+    private BubbleBarViewController mBubbleBarViewController;
+    private BubbleDismissController mBubbleDismissController;
+
+    public BubbleDragController(TaskbarActivityContext activity) {
+        mActivity = activity;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+        mBubbleDismissController = bubbleControllers.bubbleDismissController;
+    }
+
+    /**
+     * Setup the bubble view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleView(@NonNull BubbleView bubbleView) {
+        // Don't setup dragging for overflow bubble view
+        if (bubbleView.getBubble() == null
+                || !(bubbleView.getBubble() instanceof BubbleBarBubble)) return;
+        bubbleView.setOnTouchListener(new BaseDragListener() {
+            @Override
+            protected void onDragStart() {
+                super.onDragStart();
+                mBubbleBarViewController.onDragStart(bubbleView);
+            }
+
+            @Override
+            protected void onDragEnd() {
+                super.onDragEnd();
+                mBubbleBarViewController.onDragEnd(bubbleView);
+            }
+        });
+    }
+
+    /**
+     * Setup the bubble bar view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
+        PointF initialRelativePivot = new PointF();
+        bubbleBarView.setOnTouchListener(new BaseDragListener() {
+            @Override
+            public boolean onDown(@NonNull View view, @NonNull MotionEvent event) {
+                if (bubbleBarView.isExpanded()) return false;
+                // Setup dragging only when bubble bar is collapsed
+                return super.onDown(view, event);
+            }
+
+            @Override
+            protected void onDragStart() {
+                super.onDragStart();
+                initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
+                        bubbleBarView.getRelativePivotY());
+                bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
+            }
+
+            @Override
+            protected void onDragEnd() {
+                super.onDragEnd();
+                bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
+            }
+        });
+    }
+
+    /**
+     * Base drag listener for handling a single bubble view or bubble bar view dragging.
+     * Controls dragging interaction and interacts with {@link BubbleDismissController}
+     * to coordinate dismiss view presentation.
+     * Lifecycle methods can be overridden do add extra setup/clean up steps
+     */
+    private class BaseDragListener extends RelativeTouchListener {
+        private boolean mHandling;
+        private boolean mDragging;
+
+        @Override
+        public boolean onDown(@NonNull View view, @NonNull MotionEvent event) {
+            mHandling = true;
+            mActivity.setTaskbarWindowFullscreen(true);
+            mBubbleDismissController.setupDismissView(view);
+            mBubbleDismissController.handleTouchEvent(event);
+            return true;
+        }
+
+        @Override
+        public void onMove(@NonNull View view, @NonNull MotionEvent event, float viewInitialX,
+                float viewInitialY, float dx, float dy) {
+            if (!mHandling) return;
+            if (!mDragging) {
+                // Start dragging
+                mDragging = true;
+                onDragStart();
+            }
+            if (!mBubbleDismissController.handleTouchEvent(event)) {
+                // Drag the view if not processed by dismiss controller
+                view.setTranslationX(viewInitialX + dx);
+                view.setTranslationY(viewInitialY + dy);
+            }
+        }
+
+        @Override
+        public void onUp(@NonNull View view, @NonNull MotionEvent event, float viewInitialX,
+                float viewInitialY, float dx, float dy, float velX, float velY) {
+            onComplete(view, event, viewInitialX, viewInitialY);
+        }
+
+        @Override
+        public void onCancel(@NonNull View view, @NonNull MotionEvent event, float viewInitialX,
+                float viewInitialY, float dx, float dy) {
+            onComplete(view, event, viewInitialX, viewInitialY);
+        }
+
+        /**
+         * Prepares dismiss view for dragging.
+         * This method can be overridden to add extra setup on drag start
+         */
+        protected void onDragStart() {
+            mBubbleDismissController.showDismissView();
+        }
+
+        /**
+         * Cleans up dismiss view after dragging.
+         * This method can be overridden to add extra setup on drag end
+         */
+        protected void onDragEnd() {
+            mBubbleDismissController.hideDismissView();
+        }
+
+        /**
+         * Complete drag handling and clean up dependencies
+         */
+        private void onComplete(@NonNull View view, @NonNull MotionEvent event,
+                float viewInitialX, float viewInitialY) {
+            if (!mHandling) return;
+            if (mDragging) {
+                // Stop dragging
+                mDragging = false;
+                view.setTranslationX(viewInitialX);
+                view.setTranslationY(viewInitialY);
+                onDragEnd();
+            }
+            mBubbleDismissController.handleTouchEvent(event);
+            mActivity.setTaskbarWindowFullscreen(false);
+            mHandling = false;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 5177d93..bd660be 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.isBubbleBarVisible()) {
+                // if the bubble bar is not visible, there are no bubbles, 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..7ce87a3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -549,7 +549,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 +1000,13 @@
         return mSplitToWorkspaceController;
     }
 
+    @Override
+    protected void handleSplitAnimationGoingToHome() {
+        super.handleSplitAnimationGoingToHome();
+        mSplitSelectStateController.getSplitAnimationController()
+                .playPlaceholderDismissAnim(this);
+    }
+
     public <T extends OverviewActionsView> T getActionsView() {
         return (T) mActionsView;
     }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 1de264a..3af9d5c 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 onLauncherHome whether the bubble is showing on launcher home or not (modifies where
-     *                       the expanded bubble view is placed).
+     * @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, boolean onLauncherHome) {
+    public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
         if (mBubbles != null) {
             try {
-                mBubbles.showBubble(key, onLauncherHome);
+                mBubbles.showBubble(key, bubbleBarOffsetX, bubbleBarOffsetY);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call showBubble");
             }
@@ -660,6 +662,31 @@
     }
 
     /**
+     * Tells SysUI to remove the bubble with the provided key.
+     * @param key the key of the bubble to show.
+     */
+    public void removeBubble(String key) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeBubble(key);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeBubble");
+        }
+    }
+
+    /**
+     * Tells SysUI to remove all bubbles.
+     */
+    public void removeAllBubbles() {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeAllBubbles();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeAllBubbles");
+        }
+    }
+
+    /**
      * Tells SysUI to collapse the bubbles.
      */
     public void collapseBubbles() {
@@ -672,6 +699,21 @@
         }
     }
 
+    /**
+     * Tells SysUI to collapse/expand selected bubble view while it's dragged.
+     * Should be called only when the bubble bar is expanded.
+     * @param bubbleKey the key of the bubble to collapse/expand
+     * @param collapse whether to collapse/expand selected bubble
+     */
+    public void collapseWhileDragging(@Nullable String bubbleKey, boolean collapse) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.collapseWhileDragging(bubbleKey, collapse);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call collapseWhileDragging");
+        }
+    }
+
     //
     // Splitscreen
     //
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..828d466 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -86,6 +86,10 @@
         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);
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 849db28..9efab36 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() {
@@ -2961,7 +2985,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..74b7fd3 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,
@@ -385,23 +390,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/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/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index b2c64b3..073e523 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -381,7 +381,9 @@
 
         // Draw all page indicators;
         float circleGap = mCircleGap;
-        float startX = (getWidth() - (mNumPages * circleGap) + mDotRadius) / 2;
+        float startX = ((float) getWidth() / 2)
+                - (mCircleGap * (((float) mNumPages - 1) / 2))
+                - mDotRadius;
 
         float x = startX + mDotRadius;
         float y = getHeight() / 2;
@@ -420,9 +422,9 @@
         float startCircle = (int) mCurrentPosition;
         float delta = mCurrentPosition - startCircle;
         float diameter = 2 * mDotRadius;
-        float startX;
-
-        startX = ((getWidth() - (mNumPages * mCircleGap) + mDotRadius) / 2);
+        float startX = ((float) getWidth() / 2)
+                - (mCircleGap * (((float) mNumPages - 1) / 2))
+                - mDotRadius;
         sTempRect.top = (getHeight() * 0.5f) - mDotRadius;
         sTempRect.bottom = (getHeight() * 0.5f) + mDotRadius;
         sTempRect.left = startX + (startCircle * mCircleGap);
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 08be026..e0f245f 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -673,10 +673,6 @@
         }
         mIsOpen = false;
 
-        mOpenCloseAnimator = getOpenCloseAnimator(false, mCloseDuration, mCloseFadeStartDelay,
-                mCloseFadeDuration, mCloseChildFadeStartDelay, mCloseChildFadeDuration,
-                ACCELERATED_EASE);
-
         mOpenCloseAnimator = ENABLE_MATERIAL_U_POPUP.get()
                 ? getMaterialUOpenCloseAnimator(
                         false,
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;
             }
         }