Merge "Fixing UI leaks when using inline-subclass" into main
diff --git a/Android.bp b/Android.bp
index b205d0c..a0187e3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -69,6 +69,15 @@
     ],
 }
 
+// Source code for quickstep dagger
+filegroup {
+    name: "launcher-quickstep-dagger",
+    srcs: [
+        "quickstep/dagger/**/*.java",
+        "quickstep/dagger/**/*.kt",
+    ],
+}
+
 // Source code for quickstep build with compose enabled, on top of launcher-src
 filegroup {
     name: "launcher-quickstep-compose-enabled-src",
@@ -221,7 +230,7 @@
 android_library {
     name: "launcher-aosp-tapl",
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
         "androidx.annotation_annotation",
@@ -388,7 +397,7 @@
         "quickstep/res",
     ],
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     static_libs: [
         "Launcher3ResLib",
@@ -411,6 +420,7 @@
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
+        ":launcher-quickstep-dagger",
         "go/quickstep/src/**/*.java",
         "go/quickstep/src/**/*.kt",
     ],
@@ -449,11 +459,12 @@
     srcs: [
         ":launcher-src",
         ":launcher-quickstep-src",
+        ":launcher-quickstep-dagger",
         ":launcher-build-config",
     ],
     resource_dirs: [],
     libs: [
-        "framework-statsd",
+        "framework-statsd.stubs.module_lib",
     ],
     // Note the ordering here is important when it comes to resource
     // overriding. We want the most specific resource overrides defined
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index 1b9c661..4c724dc 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -52,6 +52,7 @@
         "tests/src/com/android/quickstep/TaplOverviewIconTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
         "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
+        "tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt",
         "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java",
     ],
 }
diff --git a/quickstep/src/com/android/launcher3/dagger/LauncherAppComponent.java b/quickstep/dagger/LauncherAppComponent.java
similarity index 100%
rename from quickstep/src/com/android/launcher3/dagger/LauncherAppComponent.java
rename to quickstep/dagger/LauncherAppComponent.java
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 6a99a3b..3973e56 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<TextView
+<com.android.quickstep.views.DigitalWellBeingToast
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     style="@style/TextTitle"
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 34193d3..bdfd241 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -47,4 +47,8 @@
         android:inflatedId="@id/icon"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content" />
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/digital_wellbeing_toast"
+        android:visibility="invisible"/>
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index cb4b98f..00a990b 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -73,4 +73,12 @@
         android:inflatedId="@id/bottomRight_icon"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content" />
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/digital_wellbeing_toast"
+        android:visibility="invisible"/>
+
+    <include layout="@layout/digital_wellbeing_toast"
+        android:id="@+id/bottomRight_digital_wellbeing_toast"
+        android:visibility="invisible"/>
 </com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index c5c7145..735f6c4 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -93,7 +93,7 @@
     <string name="allset_button_hint" msgid="2395219947744706291">"כדי לעבור אל מסך הבית צריך להקיש על הלחצן הראשי"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"הכול מוכן ואפשר להתחיל להשתמש ב<xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"מכשיר"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"הגדרות הניווט של המערכת"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"הגדרות הניווט במערכת"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"שיתוף"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"צילום מסך"</string>
     <string name="action_split" msgid="2098009717623550676">"פיצול"</string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 7424092..ce3f3ac 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -437,6 +437,7 @@
     <dimen name="bubblebar_stashed_handle_width">55dp</dimen>
     <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
     <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
+    <dimen name="bubblebar_stashed_handle_spring_velocity_dp_per_s">@dimen/transient_taskbar_stash_spring_velocity_dp_per_s</dimen>
     <!-- this is a pointer height minus 1dp to ensure the pointer overlaps with the bubblebar
     background appropriately when close to the rounded corner -->
     <dimen name="bubblebar_pointer_visible_size">9dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index e19042e..47ae741 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -283,7 +283,7 @@
             TaskbarHotseatDimensionsProvider dimensionsProvider =
                     new DeviceProfileDimensionsProviderAdapter(this);
             BubbleStashController bubbleStashController = isTransientTaskbar
-                    ? new TransientBubbleStashController(dimensionsProvider, getResources())
+                    ? new TransientBubbleStashController(dimensionsProvider, this)
                     : new PersistentBubbleStashController(dimensionsProvider);
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index acf976f..2845cee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -171,6 +171,10 @@
     }
 
     private void updateBackgroundAlpha() {
+        if (mActivity.isPhoneMode()) {
+            return;
+        }
+
         final float bgNavbar = mBgNavbar.value;
         final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
                 * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 4df0223..2370dfd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -21,7 +21,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
-import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
+import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 
 import android.animation.ObjectAnimator;
 import android.view.animation.Interpolator;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 56f88d1..60e65b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -152,12 +152,12 @@
     /**
      * How long to delay the icon/stash handle alpha.
      */
-    private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
+    public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
 
     /**
      * How long the icon/stash handle alpha animation plays.
      */
-    private static final long TASKBAR_STASH_ALPHA_DURATION = 50;
+    public static final long TASKBAR_STASH_ALPHA_DURATION = 50;
 
     /**
      * How long to delay the icon/stash handle alpha for the home to app taskbar animation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 8b8b4da..32d6561 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -66,7 +66,7 @@
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.util.List;
 import java.util.function.Predicate;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 296d379..5ec00ac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -28,7 +28,7 @@
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 /**
  * Callbacks for {@link TaskbarView} to interact with its controller.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index cdd3e13..d70a317 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -41,10 +41,10 @@
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.bubbles.IBubblesListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
-import com.android.wm.shell.common.bubbles.BubbleInfo;
-import com.android.wm.shell.common.bubbles.RemovedBubble;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
+import com.android.wm.shell.shared.bubbles.RemovedBubble;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 39d1ed7..7a32ef1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -17,7 +17,7 @@
 
 import android.graphics.Bitmap
 import android.graphics.Path
-import com.android.wm.shell.common.bubbles.BubbleInfo
+import com.android.wm.shell.shared.bubbles.BubbleInfo
 
 /** An entity in the bubble bar. */
 sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
index a6b0860..9c34307 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
@@ -28,8 +28,8 @@
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
-import com.android.wm.shell.common.bubbles.BaseBubblePinController
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 /**
  * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 71867fe..06301c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -48,7 +48,7 @@
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
 import com.android.launcher3.util.DisplayController;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -1303,7 +1303,10 @@
         return totalIconSize + totalSpace + horizontalPadding;
     }
 
-    private float collapsedWidth() {
+    /**
+     * Get width of the bubble bar if it is collapsed
+     */
+    float collapsedWidth() {
         final int bubbleChildCount = getBubbleChildCount();
         final float horizontalPadding = 2 * mBubbleBarPadding;
         // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 65f857e..d9e3406 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -43,7 +43,7 @@
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -80,12 +80,15 @@
 
     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
     private final MultiValueAlpha mBubbleBarAlpha;
-    private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
+    private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX);
+    private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY);
     private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
             this::updateTranslationY);
 
     // Modified when swipe up is happening on the bubble bar or task bar.
     private float mBubbleBarSwipeUpTranslationY;
+    // Modified when bubble bar is springing back into the stash handle.
+    private float mBubbleBarStashTranslationY;
 
     // Whether the bar is hidden for a sysui state.
     private boolean mHiddenForSysui;
@@ -125,7 +128,7 @@
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
                 dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
-        mBubbleBarScale.updateValue(1f);
+        mBubbleBarScaleY.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
@@ -255,19 +258,48 @@
         return mBubbleBarAlpha;
     }
 
-    public AnimatedFloat getBubbleBarScale() {
-        return mBubbleBarScale;
+    public AnimatedFloat getBubbleBarScaleX() {
+        return mBubbleBarScaleX;
+    }
+
+    public AnimatedFloat getBubbleBarScaleY() {
+        return mBubbleBarScaleY;
     }
 
     public AnimatedFloat getBubbleBarTranslationY() {
         return mBubbleBarTranslationY;
     }
 
+    public float getBubbleBarCollapsedWidth() {
+        return mBarView.collapsedWidth();
+    }
+
     public float getBubbleBarCollapsedHeight() {
         return mBarView.getBubbleBarCollapsedHeight();
     }
 
     /**
+     * @see BubbleBarView#getRelativePivotX()
+     */
+    public float getBubbleBarRelativePivotX() {
+        return mBarView.getRelativePivotX();
+    }
+
+    /**
+     * @see BubbleBarView#getRelativePivotY()
+     */
+    public float getBubbleBarRelativePivotY() {
+        return mBarView.getRelativePivotY();
+    }
+
+    /**
+     * @see BubbleBarView#setRelativePivot(float, float)
+     */
+    public void setBubbleBarRelativePivot(float x, float y) {
+        mBarView.setRelativePivot(x, y);
+    }
+
+    /**
      * Whether the bubble bar is visible or not.
      */
     public boolean isBubbleBarVisible() {
@@ -287,6 +319,14 @@
     }
 
     /**
+     * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
+     * the right
+     */
+    public boolean isBubbleBarOnLeft() {
+        return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+    }
+
+    /**
      * Update bar {@link BubbleBarLocation}
      */
     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
@@ -474,17 +514,24 @@
         updateTranslationY();
     }
 
-    private void updateTranslationY() {
-        mBarView.setTranslationY(mBubbleBarTranslationY.value
-                + mBubbleBarSwipeUpTranslationY);
+    /**
+     * Sets the translation of the bubble bar during the stash animation.
+     */
+    public void setTranslationYForStash(float transY) {
+        mBubbleBarStashTranslationY = transY;
+        updateTranslationY();
     }
 
-    /**
-     * Applies scale properties for the entire bubble bar.
-     */
-    private void updateScale() {
-        float scale = mBubbleBarScale.value;
+    private void updateTranslationY() {
+        mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY
+                + mBubbleBarStashTranslationY);
+    }
+
+    private void updateScaleX(float scale) {
         mBarView.setScaleX(scale);
+    }
+
+    private void updateScaleY(float scale) {
         mBarView.setScaleY(scale);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
index 8e9a2f6..12b1487 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -51,7 +51,7 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.wm.shell.common.bubbles.BubbleInfo;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
 
 /**
  * Loads the necessary info to populate / present a bubble (name, icon, shortcut).
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index f554fcd..a459dd9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -29,7 +29,7 @@
 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.shared.bubbles.DismissView;
 import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
 
 /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
index 6c3f0d8..a8002a5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -18,7 +18,7 @@
 package com.android.launcher3.taskbar.bubbles
 
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.DismissView
+import com.android.wm.shell.shared.bubbles.DismissView
 
 /**
  * Dismiss view is shared from WMShell. It requires setup with local resources.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 87f466f..adaba7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -29,8 +29,8 @@
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 
 import com.android.launcher3.R;
-import com.android.wm.shell.common.bubbles.DismissCircleView;
-import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.shared.bubbles.DismissCircleView;
+import com.android.wm.shell.shared.bubbles.DismissView;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 54b883c..42bd197 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -27,8 +27,8 @@
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 
 import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 /**
  * Controls bubble bar drag interactions.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
index 1341b53..af1666f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
@@ -28,8 +28,8 @@
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
-import com.android.wm.shell.common.bubbles.BaseBubblePinController
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.BaseBubblePinController
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 /** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */
 class BubblePinController(
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 00cc269..fdd385a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -21,6 +21,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.Rect;
@@ -28,6 +29,7 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
@@ -37,8 +39,8 @@
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.handles.RegionSamplingHelper;
 
 /**
@@ -49,23 +51,29 @@
     private final TaskbarActivityContext mActivity;
     private final StashedHandleView mStashedHandleView;
     private final MultiValueAlpha mStashedHandleAlpha;
+    private float mTranslationForSwipeY;
+    private float mTranslationForStashY;
 
     // Initialized in init.
     private BubbleBarViewController mBarViewController;
     private BubbleStashController mBubbleStashController;
     private RegionSamplingHelper mRegionSamplingHelper;
-    private int mBarSize;
-    private int mStashedTaskbarHeight;
+    // Height of the area for the stash handle. Handle will be drawn in the center of this.
+    // This is also the area where touch is handled on the handle.
+    private int mStashedBubbleBarHeight;
     private int mStashedHandleWidth;
     private int mStashedHandleHeight;
 
-    // The bounds we want to clip to in the settled state when showing the stashed handle.
+    // The bounds of the stashed handle in settled state.
     private final Rect mStashedHandleBounds = new Rect();
+    private float mStashedHandleRadius;
 
     // When the reveal animation is cancelled, we can assume it's about to create a new animation,
     // which should start off at the same point the cancelled one left off.
     private float mStartProgressForNextRevealAnim;
-    private boolean mWasLastRevealAnimReversed;
+    // Use a nullable boolean to handle initial case where the last animation direction is not known
+    @Nullable
+    private Boolean mWasLastRevealAnimReversed = null;
 
     // XXX: if there are more of these maybe do state flags instead
     private boolean mHiddenForSysui;
@@ -77,6 +85,7 @@
         mActivity = activity;
         mStashedHandleView = stashedHandleView;
         mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1);
+        mStashedHandleAlpha.setUpdateVisibility(true);
     }
 
     /** Initialize controller. */
@@ -84,26 +93,31 @@
         mBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
 
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
         mStashedHandleHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_handle_height);
         mStashedHandleWidth = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_handle_width);
-        mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
 
-        final int bottomMargin = resources.getDimensionPixelSize(
-                R.dimen.transient_taskbar_bottom_margin);
-        mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin;
+        int barSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
+        // Use the max translation for bubble bar whether it is on the home screen or in app.
+        // Use values directly from device profile to avoid referencing other bubble controllers
+        // during init flow.
+        int maxTy = Math.max(deviceProfile.hotseatBarBottomSpacePx,
+                deviceProfile.taskbarBottomMargin);
+        // Adjust handle view size to accommodate the handle morphing into the bubble bar
+        mStashedHandleView.getLayoutParams().height = barSize + maxTy;
 
         mStashedHandleAlpha.get(0).setValue(0);
 
-        mStashedTaskbarHeight = resources.getDimensionPixelSize(
+        mStashedBubbleBarHeight = resources.getDimensionPixelSize(
                 R.dimen.bubblebar_stashed_size);
         mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
             @Override
             public void getOutline(View view, Outline outline) {
-                float stashedHandleRadius = view.getHeight() / 2f;
-                outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius);
+                mStashedHandleRadius = view.getHeight() / 2f;
+                outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
             }
         });
 
@@ -132,28 +146,25 @@
     private void updateBounds(BubbleBarLocation bubbleBarLocation) {
         // As more bubbles get added, the icon bounds become larger. To ensure a consistent
         // handle bar position, we pin it to the edge of the screen.
-        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
+        final int stashedCenterY = mStashedHandleView.getHeight() - mStashedBubbleBarHeight / 2;
+        final int stashedCenterX;
         if (bubbleBarLocation.isOnLeft(mStashedHandleView.isLayoutRtl())) {
             final int left = mBarViewController.getHorizontalMargin();
-            mStashedHandleBounds.set(
-                    left,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    left + mStashedHandleWidth,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.setPivotX(0);
+            stashedCenterX = left + mStashedHandleWidth / 2;
         } else {
             final int right =
-                    mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin();
-            mStashedHandleBounds.set(
-                    right - mStashedHandleWidth,
-                    stashedCenterY - mStashedHandleHeight / 2,
-                    right,
-                    stashedCenterY + mStashedHandleHeight / 2);
-            mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
+                    mStashedHandleView.getRight() - mBarViewController.getHorizontalMargin();
+            stashedCenterX = right - mStashedHandleWidth / 2;
         }
-
+        mStashedHandleBounds.set(
+                stashedCenterX - mStashedHandleWidth / 2,
+                stashedCenterY - mStashedHandleHeight / 2,
+                stashedCenterX + mStashedHandleWidth / 2,
+                stashedCenterY + mStashedHandleHeight / 2
+        );
         mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
-        mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
+        mStashedHandleView.setPivotX(stashedCenterX);
+        mStashedHandleView.setPivotY(stashedCenterY);
     }
 
     public void onDestroy() {
@@ -162,6 +173,13 @@
     }
 
     /**
+     * Returns the width of the stashed handle.
+     */
+    public int getStashedWidth() {
+        return mStashedHandleWidth;
+    }
+
+    /**
      * Returns the height of the stashed handle.
      */
     public int getStashedHeight() {
@@ -169,13 +187,6 @@
     }
 
     /**
-     * Returns the height when the bubble bar is unstashed (so the height of the bubble bar).
-     */
-    public int getUnstashedHeight() {
-        return mBarSize;
-    }
-
-    /**
      * Called when system ui state changes. Bubbles don't show when the device is locked.
      */
     public void setHiddenForSysui(boolean hidden) {
@@ -242,7 +253,20 @@
      * Sets the translation of the stashed handle during the swipe up gesture.
      */
     public void setTranslationYForSwipe(float transY) {
-        mStashedHandleView.setTranslationY(transY);
+        mTranslationForSwipeY = transY;
+        updateTranslationY();
+    }
+
+    /**
+     * Sets the translation of the stashed handle during the spring on stash animation.
+     */
+    public void setTranslationYForStash(float transY) {
+        mTranslationForStashY = transY;
+        updateTranslationY();
+    }
+
+    private void updateTranslationY() {
+        mStashedHandleView.setTranslationY(mTranslationForSwipeY + mTranslationForStashY);
     }
 
     /** Returns the translation of the stashed handle. */
@@ -263,18 +287,17 @@
      * the size of where the bubble bar icons will be.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
-        Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+        Rect bubbleBarBounds = getLocalBubbleBarBounds();
 
-        // Account for the full visual height of the bubble bar
-        int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2;
-        bubbleBarBounds.top -= heightDiff;
-        bubbleBarBounds.bottom += heightDiff;
-        float stashedHandleRadius = mStashedHandleView.getHeight() / 2f;
+        float bubbleBarRadius = bubbleBarBounds.height() / 2f;
         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
-                stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
+                bubbleBarRadius, mStashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
 
         boolean isReversed = !isStashed;
-        boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
+        // We are only changing direction when mWasLastRevealAnimReversed is set at least once
+        boolean changingDirection =
+                mWasLastRevealAnimReversed != null && mWasLastRevealAnimReversed != isReversed;
+
         mWasLastRevealAnimReversed = isReversed;
         if (changingDirection) {
             mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
@@ -291,6 +314,21 @@
         return revealAnim;
     }
 
+    /**
+     * Get bounds for the bubble bar in the space of the handle view
+     */
+    private Rect getLocalBubbleBarBounds() {
+        // Position the bubble bar bounds to the space of handle view
+        Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+        // Start by moving bubble bar bounds to the bottom of handle view
+        int height = bubbleBarBounds.height();
+        bubbleBarBounds.bottom = mStashedHandleView.getHeight();
+        bubbleBarBounds.top = bubbleBarBounds.bottom - height;
+        // Then apply translation that is applied to the bubble bar
+        bubbleBarBounds.offset(0, (int) mBubbleStashController.getBubbleBarTranslationY());
+        return bubbleBarBounds;
+    }
+
     /** Checks that the stash handle is visible and that the motion event is within bounds. */
     public boolean isEventOverHandle(MotionEvent ev) {
         if (mStashedHandleView.getVisibility() != VISIBLE) {
@@ -299,7 +337,7 @@
 
         // the bounds of the handle only include the visible part, so we check that the Y coordinate
         // is anywhere within the stashed height of bubble bar (same as taskbar stashed height).
-        final int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight;
+        final int top = mActivity.getDeviceProfile().heightPx - mStashedBubbleBarHeight;
         final float x = ev.getRawX();
         return ev.getRawY() >= top && x >= mStashedHandleBounds.left
                 && x <= mStashedHandleBounds.right;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 6265ec3..591a9da 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -34,9 +34,9 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
-import com.android.wm.shell.common.bubbles.BubbleInfo;
 import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
 
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index 48eb7de..8d63217 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -23,8 +23,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
 import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 import java.io.PrintWriter
 
 /** StashController that defines stashing behaviour for the taskbar modes. */
@@ -181,8 +181,5 @@
 
         /** How long to translate Y coordinate of the BubbleBar. */
         const val BAR_TRANSLATION_DURATION = 300L
-
-        /** The scale bubble bar animates to when being stashed. */
-        const val STASHED_BAR_SCALE = 0.5f
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 1b65019..eaf4bf9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -30,8 +30,8 @@
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
 import com.android.launcher3.util.MultiPropertyFactory
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
 import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 class PersistentBubbleStashController(
     private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
@@ -116,7 +116,7 @@
         bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
         // bubble bar has only alpha property, getting it at index 0
         bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
-        bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScale
+        bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScaleY
     }
 
     private fun animateAfterUnlock() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 1a4b982..1157305 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -17,30 +17,36 @@
 package com.android.launcher3.taskbar.bubbles.stashing
 
 import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
-import android.content.res.Resources
+import android.content.Context
 import android.view.MotionEvent
 import android.view.View
 import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.app.animation.Interpolators.EMPHASIZED
+import com.android.app.animation.Interpolators.LINEAR
 import com.android.launcher3.R
 import com.android.launcher3.anim.AnimatedFloat
-import com.android.launcher3.taskbar.StashedHandleViewController
+import com.android.launcher3.anim.SpringAnimationBuilder
 import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_DURATION
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
-import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.STASHED_BAR_SCALE
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
 import com.android.launcher3.util.MultiPropertyFactory
-import com.android.wm.shell.common.bubbles.BubbleBarLocation
 import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import kotlin.math.max
 
 class TransientBubbleStashController(
     private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
-    resources: Resources
+    private val context: Context
 ) : BubbleStashController {
 
     private lateinit var bubbleBarViewController: BubbleBarViewController
@@ -50,14 +56,21 @@
     // stash view properties
     private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
     private var stashHandleViewAlpha: MultiPropertyFactory<View>.MultiProperty? = null
+    private var translationYDuringStash = AnimatedFloat { transY ->
+        bubbleStashedHandleViewController?.setTranslationYForStash(transY)
+        bubbleBarViewController.setTranslationYForStash(transY)
+    }
+    private val stashHandleStashVelocity =
+        context.resources.getDimension(R.dimen.bubblebar_stashed_handle_spring_velocity_dp_per_s)
     private var stashedHeight: Int = 0
 
     // bubble bar properties
     private lateinit var bubbleBarAlpha: MultiPropertyFactory<View>.MultiProperty
     private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
-    private lateinit var bubbleBarScale: AnimatedFloat
-    private val mHandleCenterFromScreenBottom =
-        resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
+    private lateinit var bubbleBarScaleX: AnimatedFloat
+    private lateinit var bubbleBarScaleY: AnimatedFloat
+    private val handleCenterFromScreenBottom =
+        context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
 
     private var animator: AnimatorSet? = null
 
@@ -136,12 +149,10 @@
         bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
         // bubble bar has only alpha property, getting it at index 0
         bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
-        bubbleBarScale = bubbleBarViewController.bubbleBarScale
+        bubbleBarScaleX = bubbleBarViewController.bubbleBarScaleX
+        bubbleBarScaleY = bubbleBarViewController.bubbleBarScaleY
         stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
-        stashHandleViewAlpha =
-            bubbleStashedHandleViewController
-                ?.stashedHandleAlpha
-                ?.get(StashedHandleViewController.ALPHA_INDEX_STASHED)
+        stashHandleViewAlpha = bubbleStashedHandleViewController?.stashedHandleAlpha?.get(0)
     }
 
     private fun animateAfterUnlock() {
@@ -149,7 +160,8 @@
         if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
             isStashed = false
             animatorSet.playTogether(
-                bubbleBarScale.animateToValue(1f),
+                bubbleBarScaleX.animateToValue(1f),
+                bubbleBarScaleY.animateToValue(1f),
                 bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
                 bubbleBarAlpha.animateToValue(1f)
             )
@@ -169,7 +181,8 @@
         stashHandleViewAlpha?.value = 0f
         this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
         bubbleBarAlpha.setValue(1f)
-        bubbleBarScale.updateValue(1f)
+        bubbleBarScaleX.updateValue(1f)
+        bubbleBarScaleY.updateValue(1f)
         isStashed = false
         onIsStashedChanged()
     }
@@ -179,7 +192,8 @@
         stashHandleViewAlpha?.value = 1f
         this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation())
         bubbleBarAlpha.setValue(0f)
-        bubbleBarScale.updateValue(STASHED_BAR_SCALE)
+        bubbleBarScaleX.updateValue(getStashScaleX())
+        bubbleBarScaleY.updateValue(getStashScaleY())
         isStashed = true
         onIsStashedChanged()
     }
@@ -223,11 +237,11 @@
         // the difference between the centers of the handle and the bubble bar is the difference
         // between their distance from the bottom of the screen.
         val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f
-        return mHandleCenterFromScreenBottom - barCenter
+        return handleCenterFromScreenBottom - barCenter
     }
 
     override fun getStashedHandleTranslationForNewBubbleAnimation(): Float {
-        return -mHandleCenterFromScreenBottom
+        return -handleCenterFromScreenBottom
     }
 
     override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? {
@@ -245,7 +259,19 @@
     override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
 
     private fun getStashTranslation(): Float {
-        return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f
+        return bubbleBarTranslationY / 2f
+    }
+
+    @VisibleForTesting
+    fun getStashScaleX(): Float {
+        val handleWidth = bubbleStashedHandleViewController?.stashedWidth ?: 0
+        return handleWidth / bubbleBarViewController.bubbleBarCollapsedWidth
+    }
+
+    @VisibleForTesting
+    fun getStashScaleY(): Float {
+        val handleHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
+        return handleHeight / bubbleBarViewController.bubbleBarCollapsedHeight
     }
 
     /**
@@ -258,61 +284,93 @@
     @Suppress("SameParameterValue")
     private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
         val animatorSet = AnimatorSet()
-        val fullLengthAnimatorSet = AnimatorSet()
-        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
-        val firstHalfAnimatorSet = AnimatorSet()
-        val secondHalfAnimatorSet = AnimatorSet()
-        val firstHalfDurationScale: Float
-        val secondHalfDurationScale: Float
-        val stashHandleAlphaValue: Float
-        if (isStashed) {
-            firstHalfDurationScale = 0.75f
-            secondHalfDurationScale = 0.5f
-            stashHandleAlphaValue = 1f
-            fullLengthAnimatorSet.play(
-                bubbleBarTranslationYAnimator.animateToValue(getStashTranslation())
-            )
-            firstHalfAnimatorSet.playTogether(
-                bubbleBarAlpha.animateToValue(0f),
-                bubbleBarScale.animateToValue(STASHED_BAR_SCALE)
-            )
-        } else {
-            firstHalfDurationScale = 0.5f
-            secondHalfDurationScale = 0.75f
-            stashHandleAlphaValue = 0f
-            fullLengthAnimatorSet.playTogether(
-                bubbleBarScale.animateToValue(1f),
-                bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY)
-            )
-            secondHalfAnimatorSet.playTogether(bubbleBarAlpha.animateToValue(1f))
-        }
-        stashHandleViewAlpha?.let {
-            secondHalfAnimatorSet.playTogether(it.animateToValue(stashHandleAlphaValue))
-        }
-        bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.let {
-            fullLengthAnimatorSet.play(it)
-        }
-        fullLengthAnimatorSet.setDuration(duration)
-        firstHalfAnimatorSet.setDuration((duration * firstHalfDurationScale).toLong())
-        secondHalfAnimatorSet.setDuration((duration * secondHalfDurationScale).toLong())
-        secondHalfAnimatorSet.startDelay = (duration * (1 - secondHalfDurationScale)).toLong()
-        animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, secondHalfAnimatorSet)
-        animatorSet.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    animator = null
-                    controllersAfterInitAction.runAfterInit {
-                        if (isStashed) {
-                            bubbleBarViewController.isExpanded = false
-                        }
-                        taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
-                    }
-                }
+
+        val alphaDuration = if (isStashed) duration else TASKBAR_STASH_ALPHA_DURATION
+        val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L
+        animatorSet.play(
+            createStashAlphaAnimator(isStashed).apply {
+                this.duration = max(0L, alphaDuration - alphaDelay)
+                this.startDelay = alphaDelay
+                this.interpolator = LINEAR
             }
         )
+
+        animatorSet.play(
+            createSpringOnStashAnimator(isStashed).apply {
+                this.duration = duration
+                this.interpolator = LINEAR
+            }
+        )
+
+        animatorSet.play(
+            bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        val pivotX = if (bubbleBarViewController.isBubbleBarOnLeft) 0f else 1f
+        animatorSet.play(
+            createScaleAnimator(isStashed).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+                this.setBubbleBarPivotDuringAnim(pivotX, 1f)
+            }
+        )
+
+        val translationYTarget = if (isStashed) getStashTranslation() else bubbleBarTranslationY
+        animatorSet.play(
+            bubbleBarTranslationYAnimator.animateToValue(translationYTarget).apply {
+                this.duration = duration
+                this.interpolator = EMPHASIZED
+            }
+        )
+
+        animatorSet.doOnEnd {
+            animator = null
+            controllersAfterInitAction.runAfterInit {
+                if (isStashed) {
+                    bubbleBarViewController.isExpanded = false
+                }
+                taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+            }
+        }
         return animatorSet
     }
 
+    private fun createStashAlphaAnimator(isStashed: Boolean): AnimatorSet {
+        val stashHandleAlphaTarget = if (isStashed) 1f else 0f
+        val barAlphaTarget = if (isStashed) 0f else 1f
+        return AnimatorSet().apply {
+            play(bubbleBarAlpha.animateToValue(barAlphaTarget))
+            play(stashHandleViewAlpha?.animateToValue(stashHandleAlphaTarget))
+        }
+    }
+
+    private fun createSpringOnStashAnimator(isStashed: Boolean): Animator {
+        if (!isStashed) {
+            // Animate the stash translation back to 0
+            return translationYDuringStash.animateToValue(0f)
+        }
+        // Apply a spring to the handle
+        return SpringAnimationBuilder(context)
+            .setStartValue(translationYDuringStash.value)
+            .setEndValue(0f)
+            .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+            .setStiffness(SpringForce.STIFFNESS_LOW)
+            .setStartVelocity(stashHandleStashVelocity)
+            .build(translationYDuringStash, AnimatedFloat.VALUE)
+    }
+
+    private fun createScaleAnimator(isStashed: Boolean): AnimatorSet {
+        val scaleXTarget = if (isStashed) getStashScaleX() else 1f
+        val scaleYTarget = if (isStashed) getStashScaleY() else 1f
+        return AnimatorSet().apply {
+            play(bubbleBarScaleX.animateToValue(scaleXTarget))
+            play(bubbleBarScaleY.animateToValue(scaleYTarget))
+        }
+    }
+
     private fun onIsStashedChanged() {
         controllersAfterInitAction.runAfterInit {
             taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -363,13 +421,23 @@
     }
 
     private fun Animator.updateTouchRegionOnAnimationEnd(): Animator {
-        this.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    onIsStashedChanged()
-                }
+        doOnEnd { onIsStashedChanged() }
+        return this
+    }
+
+    private fun Animator.setBubbleBarPivotDuringAnim(pivotX: Float, pivotY: Float): Animator {
+        var initialPivotX = Float.NaN
+        var initialPivotY = Float.NaN
+        doOnStart {
+            initialPivotX = bubbleBarViewController.bubbleBarRelativePivotX
+            initialPivotY = bubbleBarViewController.bubbleBarRelativePivotY
+            bubbleBarViewController.setBubbleBarRelativePivot(pivotX, pivotY)
+        }
+        doOnEnd {
+            if (!initialPivotX.isNaN() && !initialPivotY.isNaN()) {
+                bubbleBarViewController.setBubbleBarRelativePivot(initialPivotX, initialPivotY)
             }
-        )
+        }
         return this
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 05627be..38d08e0 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -97,6 +97,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.Cuj;
 import com.android.internal.util.LatencyTracker;
@@ -170,10 +171,12 @@
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
-public abstract class AbsSwipeUpHandler<T extends RecentsViewContainer,
-        Q extends RecentsView, S extends BaseState<S>>
-        extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
-        RecentsAnimationCallbacks.RecentsAnimationListener {
+public abstract class AbsSwipeUpHandler<
+        RECENTS_CONTAINER extends Context & RecentsViewContainer,
+        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
+        STATE extends BaseState<STATE>>
+        extends SwipeUpAnimationLogic
+        implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener {
     private static final String TAG = "AbsSwipeUpHandler";
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
@@ -181,7 +184,7 @@
     // Fraction of the scroll and transform animation in which the current task fades out
     private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
 
-    protected final BaseContainerInterface<S, T> mContainerInterface;
+    protected final BaseContainerInterface<STATE, RECENTS_CONTAINER> mContainerInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ActivityInitListener mActivityInitListener;
     // Callbacks to be made once the recents animation starts
@@ -192,8 +195,8 @@
     protected @Nullable RecentsAnimationController mRecentsAnimationController;
     protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
-    protected @Nullable T mContainer;
-    protected @Nullable Q mRecentsView;
+    protected @Nullable RECENTS_CONTAINER mContainer;
+    protected @Nullable RECENTS_VIEW mRecentsView;
     protected Runnable mGestureEndCallback;
     protected MultiStateCallback mStateCallback;
     protected boolean mCanceled;
@@ -486,11 +489,11 @@
             return false;
         }
 
-        T createdContainer = (T) mContainerInterface.getCreatedContainer();
+        RECENTS_CONTAINER createdContainer = mContainerInterface.getCreatedContainer();
         if (createdContainer != null) {
             initTransitionEndpoints(createdContainer.getDeviceProfile());
         }
-        final T container = (T) mContainerInterface.getCreatedContainer();
+        final RECENTS_CONTAINER container = mContainerInterface.getCreatedContainer();
         if (mContainer == container) {
             return true;
         }
@@ -564,7 +567,7 @@
     }
 
     private void onLauncherStart() {
-        final T container = (T) mContainerInterface.getCreatedContainer();
+        final RECENTS_CONTAINER container = mContainerInterface.getCreatedContainer();
         if (container == null || mContainer != container) {
             return;
         }
@@ -1096,7 +1099,7 @@
      */
     @UiThread
     private void notifyGestureStarted() {
-        final T curActivity = mContainer;
+        final RECENTS_CONTAINER curActivity = mContainer;
         if (curActivity != null) {
             // Once the gesture starts, we can no longer transition home through the button, so
             // reset the force override of the activity visibility
@@ -1167,7 +1170,8 @@
     /**
      * Called if the end target has been set and the recents animation is started.
      */
-    private void onCalculateEndTarget() {
+    @VisibleForTesting
+    protected void onCalculateEndTarget() {
         final GestureEndTarget endTarget = mGestureState.getEndTarget();
 
         switch (endTarget) {
@@ -1180,7 +1184,8 @@
         }
     }
 
-    private void onSettledOnEndTarget() {
+    @VisibleForTesting
+    protected void onSettledOnEndTarget() {
         // Fast-finish the attaching animation if it's still running.
         maybeUpdateRecentsAttachedState(false);
         final GestureEndTarget endTarget = mGestureState.getEndTarget();
@@ -1416,7 +1421,7 @@
             }
         }
         Interpolator interpolator;
-        S state = mContainerInterface.stateFromGestureEndTarget(endTarget);
+        STATE state = mContainerInterface.stateFromGestureEndTarget(endTarget);
         if (isKeyboardTaskFocusPending()) {
             interpolator = EMPHASIZED;
         } else if (state.displayOverviewTasksAsGrid(mDp)) {
@@ -2497,7 +2502,7 @@
     }
 
     private void animateSplashScreenExit(
-            @NonNull T activity,
+            @NonNull RECENTS_CONTAINER activity,
             @NonNull RemoteAnimationTarget[] appearedTaskTargets,
             @NonNull RemoteAnimationTarget[] animatingTargets) {
         ViewGroup splashView = activity.getDragLayer();
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 485fbaa..f653e60 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -62,8 +62,8 @@
 /**
  * Temporary class to allow easier refactoring
  */
-public class LauncherSwipeHandlerV2 extends
-        AbsSwipeUpHandler<QuickstepLauncher, RecentsView, LauncherState> {
+public class LauncherSwipeHandlerV2 extends AbsSwipeUpHandler<
+        QuickstepLauncher, RecentsView<QuickstepLauncher, LauncherState>, LauncherState> {
 
     public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 5e29139..8873275 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -96,10 +96,10 @@
 
     fun canStartHomeSafely(): Boolean = commandQueue.isEmpty() || commandQueue.first().type == HOME
 
-    /** Clear pending commands from the queue */
+    /** Clear pending or completed commands from the queue */
     fun clearPendingCommands() {
         Log.d(TAG, "clearing pending commands: $commandQueue")
-        commandQueue.clear()
+        commandQueue.removeAll { it.status != CommandStatus.PROCESSING }
     }
 
     /**
@@ -338,7 +338,7 @@
             taskAnimationManager.notifyRecentsAnimationState(recentAnimListener)
         } else {
             val intent =
-                Intent(interactionHandler.launchIntent)
+                Intent(interactionHandler.getLaunchIntent())
                     .putExtra(ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID, gestureState.gestureId)
             command.setAnimationCallbacks(
                 taskAnimationManager.startRecentsAnimation(gestureState, intent, interactionHandler)
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 4392255..dde16c8 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -81,7 +81,6 @@
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.bubbles.IBubblesListener;
-import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 import com.android.wm.shell.common.pip.IPip;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.desktopmode.IDesktopMode;
@@ -92,6 +91,7 @@
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.shared.GroupedRecentTaskInfo;
 import com.android.wm.shell.shared.IShellTransitions;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index ec04cb7..658975c 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -37,6 +37,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.component1
 import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAnimUtils
@@ -242,7 +243,30 @@
         lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
     }
 
-    override fun getDwbLayoutTranslations(
+    override fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    ) {
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START
+            width =
+                if (isGroupedTaskView) {
+                    snapshotViewHeight
+                } else {
+                    taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+        }
+    }
+
+    override fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitBounds?,
@@ -252,39 +276,25 @@
         banner: View
     ): Pair<Float, Float> {
         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
-        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
         val translationX = banner.height.toFloat()
-
-        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
-        bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START
-        banner.pivotX = 0f
-        banner.pivotY = 0f
-        banner.rotation = degreesRotated
-
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin
-            return Pair(translationX, snapshotParams.topMargin.toFloat())
-        }
-
-        // Set correct width and translations
         val translationY: Float
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].measuredHeight
+        if (splitBounds == null) {
             translationY = snapshotParams.topMargin.toFloat()
         } else {
-            bannerParams.width = thumbnailViews[1].measuredHeight
-            val topLeftTaskPlusDividerPercent =
-                if (splitBounds.appsStackedVertically) {
-                    splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
-                } else {
-                    splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
-                }
-            translationY =
-                snapshotParams.topMargin +
-                    (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
+            if (desiredTaskId == splitBounds.leftTopTaskId) {
+                translationY = snapshotParams.topMargin.toFloat()
+            } else {
+                val topLeftTaskPlusDividerPercent =
+                    if (splitBounds.appsStackedVertically) {
+                        splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
+                    } else {
+                        splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
+                    }
+                translationY =
+                    snapshotParams.topMargin +
+                        (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
+            }
         }
-
         return Pair(translationX, translationY)
     }
 
@@ -300,6 +310,7 @@
         if (isRtl) displacement < 0 else displacement > 0
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
+
     /* -------------------- */
 
     override fun getChildBounds(
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index eeacee1..32d9052 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -243,54 +243,54 @@
     }
 
     @Override
-    public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner) {
-        float translationX = 0;
-        float translationY = 0;
+    public void updateDwbBannerLayout(int taskViewWidth, int taskViewHeight,
+            boolean isGroupedTaskView, @NonNull DeviceProfile deviceProfile,
+            int snapshotViewWidth, int snapshotViewHeight, @NonNull View banner) {
         FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
         banner.setPivotX(0);
         banner.setPivotY(0);
         banner.setRotation(getDegreesRotated());
-        if (splitBounds == null) {
-            // Single, fullscreen case
+        if (isGroupedTaskView) {
+            bannerParams.gravity =
+                    BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
+            bannerParams.width = snapshotViewWidth;
+        } else {
             bannerParams.width = MATCH_PARENT;
             bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL;
-            return new Pair<>(translationX, translationY);
         }
+        banner.setLayoutParams(bannerParams);
+    }
 
-        bannerParams.gravity =
-                BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
-
-        // Set correct width
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].getMeasuredWidth();
-        } else {
-            bannerParams.width = thumbnailViews[1].getMeasuredWidth();
-        }
-
-        // Set translations
-        if (deviceProfile.isLeftRightSplit) {
-            if (desiredTaskId == splitBounds.rightBottomTaskId) {
-                float leftTopTaskPercent = splitBounds.appsStackedVertically
-                        ? splitBounds.topTaskPercent
-                        : splitBounds.leftTaskPercent;
-                float dividerThicknessPercent = splitBounds.appsStackedVertically
-                        ? splitBounds.dividerHeightPercent
-                        : splitBounds.dividerWidthPercent;
-                translationX = ((taskViewWidth * leftTopTaskPercent)
-                        + (taskViewWidth * dividerThicknessPercent));
-            }
-        } else {
-            if (desiredTaskId == splitBounds.leftTopTaskId) {
-                FrameLayout.LayoutParams snapshotParams =
-                        (FrameLayout.LayoutParams) thumbnailViews[0]
-                                .getLayoutParams();
-                float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
-                        ? (1f - splitBounds.topTaskPercent)
-                        : (1f - splitBounds.leftTaskPercent);
-                translationY = -((taskViewHeight - snapshotParams.topMargin)
-                        * bottomRightTaskPlusDividerPercent);
+    @NonNull
+    @Override
+    public Pair<Float, Float> getDwbBannerTranslations(int taskViewWidth,
+            int taskViewHeight, SplitBounds splitBounds, @NonNull DeviceProfile deviceProfile,
+            @NonNull View[] thumbnailViews, int desiredTaskId, @NonNull View banner) {
+        float translationX = 0;
+        float translationY = 0;
+        if (splitBounds != null) {
+            if (deviceProfile.isLeftRightSplit) {
+                if (desiredTaskId == splitBounds.rightBottomTaskId) {
+                    float leftTopTaskPercent = splitBounds.appsStackedVertically
+                            ? splitBounds.topTaskPercent
+                            : splitBounds.leftTaskPercent;
+                    float dividerThicknessPercent = splitBounds.appsStackedVertically
+                            ? splitBounds.dividerHeightPercent
+                            : splitBounds.dividerWidthPercent;
+                    translationX = ((taskViewWidth * leftTopTaskPercent)
+                            + (taskViewWidth * dividerThicknessPercent));
+                }
+            } else {
+                if (desiredTaskId == splitBounds.leftTopTaskId) {
+                    FrameLayout.LayoutParams snapshotParams =
+                            (FrameLayout.LayoutParams) thumbnailViews[0]
+                                    .getLayoutParams();
+                    float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
+                            ? (1f - splitBounds.topTaskPercent)
+                            : (1f - splitBounds.leftTaskPercent);
+                    translationY = -((taskViewHeight - snapshotParams.topMargin)
+                            * bottomRightTaskPlusDividerPercent);
+                }
             }
         }
         return new Pair<>(translationX, translationY);
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index df4b030..06a0685 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -199,6 +199,7 @@
         parentWidth: Int,
         parentHeight: Int
     ): Pair<Point, Point>
+
     // Overview TaskMenuView methods
     /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
     fun setTaskIconParams(
@@ -294,13 +295,24 @@
         deviceProfile: DeviceProfile
     )
 
+    /** Layout a Digital Wellbeing Banner on its parent. TaskView. */
+    fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    )
+
     /**
-     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
+     * Calculates the translations where a Digital Wellbeing Banner should be apply on its parent
      * TaskView.
      *
      * @return A Pair of Floats representing the proper x and y translations.
      */
-    fun getDwbLayoutTranslations(
+    fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitConfigurationOptions.SplitBounds?,
@@ -309,6 +321,7 @@
         desiredTaskId: Int,
         banner: View
     ): Pair<Float, Float>
+
     // The following are only used by TaskViewTouchHandler.
 
     /** @return Either VERTICAL or HORIZONTAL. */
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 333359f..a972e8c 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -28,6 +28,7 @@
 import android.widget.FrameLayout
 import androidx.core.util.component1
 import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
 import com.android.launcher3.R
@@ -125,7 +126,30 @@
         }
     }
 
-    override fun getDwbLayoutTranslations(
+    override fun updateDwbBannerLayout(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isGroupedTaskView: Boolean,
+        deviceProfile: DeviceProfile,
+        snapshotViewWidth: Int,
+        snapshotViewHeight: Int,
+        banner: View
+    ) {
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START
+            width =
+                if (isGroupedTaskView) {
+                    snapshotViewHeight
+                } else {
+                    taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+        }
+    }
+
+    override fun getDwbBannerTranslations(
         taskViewWidth: Int,
         taskViewHeight: Int,
         splitBounds: SplitBounds?,
@@ -135,39 +159,26 @@
         banner: View
     ): Pair<Float, Float> {
         val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
-        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
-
-        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
-        bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
-        banner.pivotX = 0f
-        banner.pivotY = 0f
-        banner.rotation = degreesRotated
-
         val translationX: Float = (taskViewWidth - banner.height).toFloat()
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin
-            return Pair(translationX, banner.height.toFloat())
-        }
-
-        // Set correct width and translations
         val translationY: Float
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].measuredHeight
-            val bottomRightTaskPlusDividerPercent =
-                if (splitBounds.appsStackedVertically) {
-                    1f - splitBounds.topTaskPercent
-                } else {
-                    1f - splitBounds.leftTaskPercent
-                }
-            translationY =
-                banner.height -
-                    (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
-        } else {
-            bannerParams.width = thumbnailViews[1].measuredHeight
+        if (splitBounds == null) {
             translationY = banner.height.toFloat()
+        } else {
+            if (desiredTaskId == splitBounds.leftTopTaskId) {
+                val bottomRightTaskPlusDividerPercent =
+                    if (splitBounds.appsStackedVertically) {
+                        1f - splitBounds.topTaskPercent
+                    } else {
+                        1f - splitBounds.leftTaskPercent
+                    }
+                translationY =
+                    banner.height -
+                        (taskViewHeight - snapshotParams.topMargin) *
+                            bottomRightTaskPlusDividerPercent
+            } else {
+                translationY = banner.height.toFloat()
+            }
         }
-
         return Pair(translationX, translationY)
     }
 
@@ -339,6 +350,7 @@
         if (isRtl) displacement > 0 else displacement < 0
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
+
     /* -------------------- */
 
     override fun getSplitIconsPosition(
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index 0ab36c9..f0fdd81 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -15,8 +15,10 @@
  */
 package com.android.quickstep.views
 
+import android.annotation.SuppressLint
 import android.app.ActivityOptions
 import android.content.ActivityNotFoundException
+import android.content.Context
 import android.content.Intent
 import android.content.pm.LauncherApps
 import android.content.pm.LauncherApps.AppUsageLimit
@@ -27,46 +29,58 @@
 import android.icu.util.MeasureUnit
 import android.os.UserHandle
 import android.provider.Settings
+import android.util.AttributeSet
 import android.util.Log
 import android.view.View
-import android.view.ViewGroup.MarginLayoutParams
 import android.view.ViewOutlineProvider
 import android.view.accessibility.AccessibilityNodeInfo
-import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
 import androidx.core.util.component1
 import androidx.core.util.component2
-import androidx.core.view.updateLayoutParams
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
 import com.android.quickstep.TaskUtils
 import com.android.systemui.shared.recents.model.Task
 import java.time.Duration
 import java.util.Locale
 
-class DigitalWellBeingToast(
-    private val container: RecentsViewContainer,
-    private val taskView: TaskView
-) {
-    private val launcherApps: LauncherApps? =
-        container.asContext().getSystemService(LauncherApps::class.java)
+@SuppressLint("AppCompatCustomView")
+class DigitalWellBeingToast
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : TextView(context, attrs, defStyleAttr, defStyleRes) {
+    private val recentsViewContainer =
+        RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
+
+    private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
 
     private val bannerHeight =
-        container
-            .asContext()
-            .resources
-            .getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
+        context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
 
     private lateinit var task: Task
+    private lateinit var taskView: TaskView
+    private lateinit var snapshotView: View
+    @StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED
 
     private var appRemainingTimeMs: Long = 0
-    private var banner: View? = null
-    private var oldBannerOutlineProvider: ViewOutlineProvider? = null
     private var splitOffsetTranslationY = 0f
-    private var splitOffsetTranslationX = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateTranslationY()
+            }
+        }
 
     private var isDestroyed = false
 
@@ -76,65 +90,63 @@
         set(value) {
             if (field != value) {
                 field = value
-                banner?.let {
-                    updateTranslationY()
-                    it.invalidateOutline()
-                }
+                updateTranslationY()
             }
         }
 
+    init {
+        setOnClickListener(::openAppUsageSettings)
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    BACKGROUND.getOutline(view, outline)
+                    val verticalTranslation = splitOffsetTranslationY - translationY
+                    outline.offset(0, Math.round(verticalTranslation))
+                }
+            }
+        clipToOutline = true
+    }
+
     private fun setNoLimit() {
         hasLimit = false
-        taskView.contentDescription = task.titleDescription
-        replaceBanner(null)
+        setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
+        visibility = INVISIBLE
         appRemainingTimeMs = -1
     }
 
     private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
         this.appRemainingTimeMs = appRemainingTimeMs
         hasLimit = true
-        val toast =
-            container.viewCache
-                .getView<TextView>(
-                    R.layout.digital_wellbeing_toast,
-                    container.asContext(),
-                    taskView
-                )
-                .apply {
-                    text =
-                        Utilities.prefixTextWithIcon(
-                            container.asContext(),
-                            R.drawable.ic_hourglass_top,
-                            getBannerText()
-                        )
-                    setOnClickListener(::openAppUsageSettings)
-                }
-        replaceBanner(toast)
-
-        taskView.contentDescription =
-            getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+        text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
+        visibility = VISIBLE
+        setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
     }
 
-    fun initialize(task: Task) {
+    private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+        val contentDescription =
+            getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+        snapshotView.contentDescription = contentDescription
+    }
+
+    fun initialize() {
         check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
-        this.task = task
+        setupTranslations()
         Executors.ORDERED_BG_EXECUTOR.execute {
             var usageLimit: AppUsageLimit? = null
             try {
                 usageLimit =
                     launcherApps?.getAppUsageLimit(
-                        this.task.topComponent.packageName,
-                        UserHandle.of(this.task.key.userId)
+                        task.topComponent.packageName,
+                        UserHandle.of(task.key.userId)
                     )
             } catch (e: Exception) {
                 Log.e(TAG, "Error initializing digital well being toast", e)
             }
             val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
             val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
+
             taskView.post {
-                if (isDestroyed) {
-                    return@post
-                }
+                if (isDestroyed) return@post
                 if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
                     setNoLimit()
                 } else {
@@ -144,20 +156,36 @@
         }
     }
 
-    /** Mark the DWB toast as destroyed and remove banner from TaskView. */
+    /** Bind the DWB toast to its dependencies. */
+    fun bind(
+        task: Task,
+        taskView: TaskView,
+        snapshotView: View,
+        @StagePosition stagePosition: Int
+    ) {
+        this.task = task
+        this.taskView = taskView
+        this.snapshotView = snapshotView
+        this.stagePosition = stagePosition
+        isDestroyed = false
+    }
+
+    /** Mark the DWB toast as destroyed and hide it. */
     fun destroy() {
+        visibility = INVISIBLE
         isDestroyed = true
-        taskView.post { replaceBanner(null) }
     }
 
     private fun getSplitBannerConfig(): SplitBannerConfig {
         val splitBounds = splitBounds
         return when {
-            splitBounds == null || !container.deviceProfile.isTablet || taskView.isLargeTile ->
-                SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
+            splitBounds == null ||
+                !recentsViewContainer.deviceProfile.isTablet ||
+                taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
             // For portrait grid only height of task changes, not width. So we keep the text the
             // same
-            !container.deviceProfile.isLeftRightSplit -> SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            !recentsViewContainer.deviceProfile.isLeftRightSplit ->
+                SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
             // For landscape grid, for 30% width we only show icon, otherwise show icon and time
             task.key.id == splitBounds.leftTopTaskId ->
                 if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
@@ -193,8 +221,7 @@
                 MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
                     .formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
             // Use a specific string for usage less than one minute but non-zero.
-            duration > Duration.ZERO ->
-                container.asContext().getString(durationLessThanOneMinuteStringId)
+            duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId)
             // Otherwise, return 0-minute string.
             else ->
                 MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
@@ -208,6 +235,7 @@
      * [.SPLIT_BANNER_FULLSCREEN]
      */
     @JvmOverloads
+    @VisibleForTesting
     fun getBannerText(
         remainingTime: Long = appRemainingTimeMs,
         forContentDesc: Boolean = false
@@ -226,7 +254,7 @@
         val splitBannerConfig = getSplitBannerConfig()
         return when {
             forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
-                container.asContext().getString(R.string.time_left_for_app, readableDuration)
+                context.getString(R.string.time_left_for_app, readableDuration)
             // show no text
             splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
             // SPLIT_GRID_BANNER_LARGE only show time
@@ -241,7 +269,7 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
         try {
             val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
-            container.asContext().startActivity(intent, options.toBundle())
+            context.startActivity(intent, options.toBundle())
 
             // TODO: add WW logging on the app usage settings click.
         } catch (e: ActivityNotFoundException) {
@@ -259,99 +287,77 @@
         appRemainingTimeMs: Long
     ): String? =
         if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
-            container
-                .asContext()
-                .getString(
-                    R.string.task_contents_description_with_remaining_time,
-                    task.titleDescription,
-                    getBannerText(appRemainingTimeMs, true /* forContentDesc */)
-                )
+            context.getString(
+                R.string.task_contents_description_with_remaining_time,
+                task.titleDescription,
+                getBannerText(appRemainingTimeMs, true /* forContentDesc */)
+            )
         else task.titleDescription
 
-    private fun replaceBanner(view: View?) {
-        resetOldBanner()
-        setBanner(view)
-    }
-
-    private fun resetOldBanner() {
-        val banner = banner ?: return
-        banner.outlineProvider = oldBannerOutlineProvider
-        taskView.removeView(banner)
-        banner.setOnClickListener(null)
-        container.viewCache.recycleView(R.layout.digital_wellbeing_toast, banner)
-    }
-
-    private fun setBanner(banner: View?) {
-        this.banner = banner
-        if (banner != null && taskView.recentsView != null) {
-            setupAndAddBanner()
-            setBannerOutline()
+    fun setupLayout() {
+        val snapshotWidth: Int
+        val snapshotHeight: Int
+        val splitBounds = splitBounds
+        if (splitBounds == null) {
+            snapshotWidth = taskView.layoutParams.width
+            snapshotHeight =
+                taskView.layoutParams.height -
+                    recentsViewContainer.deviceProfile.overviewTaskThumbnailTopMarginPx
+        } else {
+            val groupedTaskSize =
+                taskView.pagedOrientationHandler.getGroupedTaskViewSizes(
+                    recentsViewContainer.deviceProfile,
+                    splitBounds,
+                    taskView.layoutParams.width,
+                    taskView.layoutParams.height
+                )
+            if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+                snapshotWidth = groupedTaskSize.first.x
+                snapshotHeight = groupedTaskSize.first.y
+            } else {
+                snapshotWidth = groupedTaskSize.second.x
+                snapshotHeight = groupedTaskSize.second.y
+            }
         }
+        taskView.pagedOrientationHandler.updateDwbBannerLayout(
+            taskView.layoutParams.width,
+            taskView.layoutParams.height,
+            taskView is GroupedTaskView,
+            recentsViewContainer.deviceProfile,
+            snapshotWidth,
+            snapshotHeight,
+            this
+        )
     }
 
-    private fun setupAndAddBanner() {
-        val banner = banner ?: return
-        banner.updateLayoutParams<FrameLayout.LayoutParams> {
-            bottomMargin =
-                (taskView.firstSnapshotView.layoutParams as MarginLayoutParams).bottomMargin
-        }
+    private fun setupTranslations() {
         val (translationX, translationY) =
-            taskView.pagedOrientationHandler.getDwbLayoutTranslations(
-                taskView.measuredWidth,
-                taskView.measuredHeight,
+            taskView.pagedOrientationHandler.getDwbBannerTranslations(
+                taskView.layoutParams.width,
+                taskView.layoutParams.height,
                 splitBounds,
-                container.deviceProfile,
+                recentsViewContainer.deviceProfile,
                 taskView.snapshotViews,
                 task.key.id,
-                banner
+                this
             )
-        splitOffsetTranslationX = translationX
-        splitOffsetTranslationY = translationY
-        updateTranslationY()
-        updateTranslationX()
-        taskView.addView(banner)
-    }
-
-    private fun setBannerOutline() {
-        val banner = banner ?: return
-        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
-        val oldBannerOutlineProvider =
-            if (banner.outlineProvider != null) banner.outlineProvider
-            else ViewOutlineProvider.BACKGROUND
-        this.oldBannerOutlineProvider = oldBannerOutlineProvider
-
-        banner.outlineProvider =
-            object : ViewOutlineProvider() {
-                override fun getOutline(view: View, outline: Outline) {
-                    oldBannerOutlineProvider.getOutline(view, outline)
-                    val verticalTranslation = -view.translationY + splitOffsetTranslationY
-                    outline.offset(0, Math.round(verticalTranslation))
-                }
-            }
-        banner.clipToOutline = true
+        this.translationX = translationX
+        this.splitOffsetTranslationY = translationY
     }
 
     private fun updateTranslationY() {
-        banner?.translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+        translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+        invalidateOutline()
     }
 
-    private fun updateTranslationX() {
-        banner?.translationX = splitOffsetTranslationX
-    }
-
-    fun setBannerColorTint(color: Int, amount: Float) {
-        val banner = banner ?: return
+    fun setColorTint(color: Int, amount: Float) {
         if (amount == 0f) {
-            banner.setLayerType(View.LAYER_TYPE_NONE, null)
+            setLayerType(View.LAYER_TYPE_NONE, null)
         }
         val layerPaint = Paint()
         layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
-        banner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
-        banner.setLayerPaint(layerPaint)
-    }
-
-    fun setBannerVisibility(visibility: Int) {
-        banner?.visibility = visibility
+        setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
+        setLayerPaint(layerPaint)
     }
 
     private fun getAccessibilityActionId(): Int =
@@ -361,9 +367,8 @@
 
     fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
         if (!hasLimit) return null
-        val context = container.asContext()
         val label =
-            if ((taskView.containsMultipleTasks()))
+            if (taskView.containsMultipleTasks())
                 context.getString(
                     R.string.split_app_usage_settings,
                     TaskUtils.getTitle(context, task)
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index ba4d786..4fae01e 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -115,6 +115,7 @@
                     R.id.snapshot,
                     R.id.icon,
                     R.id.show_windows,
+                    R.id.digital_wellbeing_toast,
                     STAGE_POSITION_TOP_OR_LEFT,
                     taskOverlayFactory
                 ),
@@ -123,6 +124,7 @@
                     R.id.bottomright_snapshot,
                     R.id.bottomRight_icon,
                     R.id.show_windows_right,
+                    R.id.bottomRight_digital_wellbeing_toast,
                     STAGE_POSITION_BOTTOM_OR_RIGHT,
                     taskOverlayFactory
                 )
@@ -211,7 +213,7 @@
         splitBoundsConfig = splitBounds
         taskContainers.forEach {
             it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
-            it.digitalWellBeingToast?.initialize(it.task)
+            it.digitalWellBeingToast?.initialize()
         }
         invalidate()
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index af32ba2..d34a93b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -146,6 +146,7 @@
             }
 
     fun bind() {
+        digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
         if (enableRefactorTaskThumbnail()) {
             bindThumbnailView()
         } else {
@@ -171,4 +172,18 @@
             thumbnailViewDeprecated.setOverlayEnabled(enabled)
         }
     }
+
+    fun addChildForAccessibility(outChildren: ArrayList<View>) {
+        addAccessibleChildToList(iconView.asView(), outChildren)
+        addAccessibleChildToList(snapshotView, outChildren)
+        showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
+    }
+
+    private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
+        if (view.includeForAccessibility()) {
+            outChildren.add(view)
+        } else {
+            view.addChildrenForAccessibility(outChildren)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index d2cdfa2..5614af6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -702,6 +702,7 @@
                     R.id.snapshot,
                     R.id.icon,
                     R.id.show_windows,
+                    R.id.digital_wellbeing_toast,
                     STAGE_POSITION_UNDEFINED,
                     taskOverlayFactory
                 )
@@ -715,6 +716,7 @@
         @IdRes thumbnailViewId: Int,
         @IdRes iconViewId: Int,
         @IdRes showWindowViewId: Int,
+        @IdRes digitalWellbeingBannerId: Int,
         @StagePosition stagePosition: Int,
         taskOverlayFactory: TaskOverlayFactory,
     ): TaskContainer {
@@ -730,6 +732,7 @@
                 thumbnailViewDeprecated
             }
         val iconView = getOrInflateIconView(iconViewId)
+        val digitalWellBeingToast = findViewById<DigitalWellBeingToast>(digitalWellbeingBannerId)!!
         return TaskContainer(
             this,
             task,
@@ -737,7 +740,7 @@
             iconView,
             TransformingTouchDelegate(iconView.asView()),
             stagePosition,
-            DigitalWellBeingToast(container, this),
+            digitalWellBeingToast,
             findViewById(showWindowViewId)!!,
             taskOverlayFactory
         )
@@ -775,7 +778,7 @@
     protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) {
         taskContainers.forEach {
             it.overlay.updateOrientationState(orientationState)
-            it.digitalWellBeingToast?.initialize(it.task)
+            it.digitalWellBeingToast?.initialize()
         }
     }
 
@@ -827,10 +830,8 @@
         } else {
             nonGridScale = 1f
             boxTranslationY = 0f
-            expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT
-            expectedHeight =
-                if (enableOverviewIconMenu()) taskHeight + thumbnailPadding
-                else LayoutParams.MATCH_PARENT
+            expectedWidth = taskWidth
+            expectedHeight = taskHeight + thumbnailPadding
         }
         this.nonGridScale = nonGridScale
         this.boxTranslationY = boxTranslationY
@@ -847,6 +848,7 @@
         taskContainers[0].snapshotView.updateLayoutParams<LayoutParams> {
             topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
         }
+        taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
     }
 
     /** Returns the thumbnail's bounds, optionally relative to the screen. */
@@ -939,7 +941,7 @@
         if (enableOverviewIconMenu()) {
             setText(taskContainer.iconView, taskContainer.task.title)
         }
-        taskContainer.digitalWellBeingToast?.initialize(taskContainer.task)
+        taskContainer.digitalWellBeingToast?.initialize()
     }
 
     protected open fun onIconUnloaded(taskContainer: TaskContainer) {
@@ -1040,7 +1042,10 @@
                 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED
             )
             val recentsView = recentsView ?: return null
-            if (recentsView.runningTaskViewId != -1) {
+            if (
+                recentsView.runningTaskViewId != -1 &&
+                    recentsView.mRecentsAnimationController != null
+            ) {
                 recentsView.onTaskLaunchedInLiveTileMode()
 
                 // Return a fresh callback in the live tile case, so that it's not accidentally
@@ -1444,7 +1449,7 @@
                 it.thumbnailViewDeprecated.dimAlpha = amount
             }
             it.iconView.setIconColorTint(tintColor, amount)
-            it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount)
+            it.digitalWellBeingToast?.setColorTint(tintColor, amount)
         }
     }
 
@@ -1458,7 +1463,7 @@
         taskContainers.forEach {
             if (visibility == VISIBLE || it.task.key.id == taskId) {
                 it.snapshotView.visibility = visibility
-                it.digitalWellBeingToast?.setBannerVisibility(visibility)
+                it.digitalWellBeingToast?.visibility = visibility
                 it.showWindowsView?.visibility = visibility
                 it.overlay.setVisibility(visibility)
             }
@@ -1662,6 +1667,12 @@
         return thumbnailBounds.contains(x.toInt(), y.toInt())
     }
 
+    override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
+        (if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach {
+            it.addChildForAccessibility(outChildren)
+        }
+    }
+
     companion object {
         private const val TAG = "TaskView"
         const val FLAG_UPDATE_ICON = 1
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
index 1d92d7e..eb459ff 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt
@@ -25,7 +25,7 @@
 import android.view.LayoutInflater
 import androidx.test.core.app.ApplicationProvider
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.BubbleInfo
+import com.android.wm.shell.shared.bubbles.BubbleInfo
 import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
 import org.junit.Rule
 import org.junit.Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
index cb5488c..94f9cf5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
@@ -27,7 +27,7 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.R
-import com.android.wm.shell.common.bubbles.BubbleInfo
+import com.android.wm.shell.shared.bubbles.BubbleInfo
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 2bca74a..84e872d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -37,9 +37,9 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleView
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
-import com.android.wm.shell.common.bubbles.BubbleInfo
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.android.wm.shell.shared.bubbles.BubbleInfo
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index c0a5dfa..4106a2c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -251,7 +251,7 @@
 
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
         whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY)
-        whenever(bubbleBarViewController.bubbleBarScale).thenReturn(scale)
+        whenever(bubbleBarViewController.bubbleBarScaleY).thenReturn(scale)
         whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha)
         whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index b5809c2..262d8da 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -31,7 +31,6 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
-import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.STASHED_BAR_SCALE
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
@@ -55,11 +54,13 @@
 
     companion object {
         const val TASKBAR_BOTTOM_SPACE = 5
+        const val BUBBLE_BAR_WIDTH = 200f
         const val BUBBLE_BAR_HEIGHT = 100f
         const val HOTSEAT_TRANSLATION_Y = -45f
         const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE
+        const val HANDLE_VIEW_WIDTH = 150
         const val HANDLE_VIEW_HEIGHT = 4
-        const val BUBBLE_BAR_STASHED_TRANSLATION_Y = 48
+        const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -2.5f
     }
 
     @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
@@ -76,7 +77,8 @@
     private lateinit var bubbleBarView: BubbleBarView
     private lateinit var stashedHandleView: StashedHandleView
     private lateinit var barTranslationY: AnimatedFloat
-    private lateinit var barScale: AnimatedFloat
+    private lateinit var barScaleX: AnimatedFloat
+    private lateinit var barScaleY: AnimatedFloat
     private lateinit var barAlpha: MultiValueAlpha
     private lateinit var stashedHandleAlpha: MultiValueAlpha
     private lateinit var stashedHandleScale: AnimatedFloat
@@ -90,7 +92,7 @@
         val taskbarHotseatDimensionsProvider =
             DefaultDimensionsProvider(taskBarBottomSpace = TASKBAR_BOTTOM_SPACE)
         mTransientBubbleStashController =
-            TransientBubbleStashController(taskbarHotseatDimensionsProvider, context.resources)
+            TransientBubbleStashController(taskbarHotseatDimensionsProvider, context)
         setUpBubbleBarView()
         setUpBubbleBarController()
         setUpStashedHandleView()
@@ -174,8 +176,8 @@
         assertThat(mTransientBubbleStashController.isStashed).isTrue()
         assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
         assertThat(bubbleBarView.alpha).isEqualTo(0f)
-        assertThat(bubbleBarView.scaleX).isEqualTo(STASHED_BAR_SCALE)
-        assertThat(bubbleBarView.scaleY).isEqualTo(STASHED_BAR_SCALE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX())
+        assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY())
         // Handle view is visible
         assertThat(stashedHandleView.translationY).isEqualTo(0)
         assertThat(stashedHandleView.alpha).isEqualTo(1)
@@ -196,7 +198,7 @@
 
         // Then
         assertThat(barTranslationY.isAnimating).isTrue()
-        assertThat(barScale.isAnimating).isTrue()
+        assertThat(barScaleX.isAnimating).isTrue()
         // Wait until animation ends
         advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
 
@@ -243,8 +245,8 @@
         // Then all property values are updated
         assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
-        assertThat(bubbleBarView.scaleX).isEqualTo(STASHED_BAR_SCALE)
-        assertThat(bubbleBarView.scaleY).isEqualTo(STASHED_BAR_SCALE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX())
+        assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY())
         // Handle is visible at correct Y position
         assertThat(stashedHandleView.alpha).isEqualTo(1)
         assertThat(stashedHandleView.translationY).isEqualTo(0)
@@ -294,20 +296,16 @@
     private fun setUpBubbleBarController() {
         barTranslationY =
             AnimatedFloat(Runnable { bubbleBarView.translationY = barTranslationY.value })
-        barScale =
-            AnimatedFloat(
-                Runnable {
-                    val scale: Float = barScale.value
-                    bubbleBarView.scaleX = scale
-                    bubbleBarView.scaleY = scale
-                }
-            )
+        barScaleX = AnimatedFloat { value -> bubbleBarView.scaleX = value }
+        barScaleY = AnimatedFloat { value -> bubbleBarView.scaleY = value }
         barAlpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */)
 
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
         whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(barTranslationY)
-        whenever(bubbleBarViewController.bubbleBarScale).thenReturn(barScale)
+        whenever(bubbleBarViewController.bubbleBarScaleX).thenReturn(barScaleX)
+        whenever(bubbleBarViewController.bubbleBarScaleY).thenReturn(barScaleY)
         whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(barAlpha)
+        whenever(bubbleBarViewController.bubbleBarCollapsedWidth).thenReturn(BUBBLE_BAR_WIDTH)
         whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT)
     }
 
@@ -317,7 +315,7 @@
         stashedHandleScale =
             AnimatedFloat(
                 Runnable {
-                    val scale: Float = barScale.value
+                    val scale: Float = barScaleX.value
                     bubbleBarView.scaleX = scale
                     bubbleBarView.scaleY = scale
                 }
@@ -327,6 +325,7 @@
         whenever(bubbleStashedHandleViewController.stashedHandleAlpha)
             .thenReturn(stashedHandleAlpha)
         whenever(bubbleStashedHandleViewController.physicsAnimator).thenReturn(stashPhysicsAnimator)
+        whenever(bubbleStashedHandleViewController.stashedWidth).thenReturn(HANDLE_VIEW_WIDTH)
         whenever(bubbleStashedHandleViewController.stashedHeight).thenReturn(HANDLE_VIEW_HEIGHT)
         whenever(bubbleStashedHandleViewController.setTranslationYForSwipe(any())).thenAnswer {
             invocation ->
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
new file mode 100644
index 0000000..2a0aa4c
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherRootView;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.SystemUiController;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+public abstract class AbsSwipeUpHandlerTestCase<
+        RECENTS_CONTAINER extends Context & RecentsViewContainer,
+        STATE extends BaseState<STATE>,
+        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
+        ACTIVITY_TYPE extends  StatefulActivity<STATE> & RecentsViewContainer,
+        ACTIVITY_INTERFACE extends BaseActivityInterface<STATE, ACTIVITY_TYPE>,
+        SWIPE_HANDLER extends AbsSwipeUpHandler<RECENTS_CONTAINER, RECENTS_VIEW, STATE>> {
+
+    protected final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+    protected final TaskAnimationManager mTaskAnimationManager = new TaskAnimationManager(mContext);
+    protected final RecentsAnimationDeviceState mRecentsAnimationDeviceState =
+            new RecentsAnimationDeviceState(mContext, true);
+    protected final InputConsumerController mInputConsumerController =
+            InputConsumerController.getRecentsAnimationInputConsumer();
+    protected final ActivityManager.RunningTaskInfo mRunningTaskInfo =
+            new ActivityManager.RunningTaskInfo();
+    protected final TopTaskTracker.CachedTaskInfo mCachedTaskInfo =
+            new TopTaskTracker.CachedTaskInfo(Collections.singletonList(mRunningTaskInfo));
+    protected final RemoteAnimationTarget mRemoteAnimationTarget = new RemoteAnimationTarget(
+            /* taskId= */ 0,
+            /* mode= */ RemoteAnimationTarget.MODE_CLOSING,
+            /* leash= */ new SurfaceControl(),
+            /* isTranslucent= */ false,
+            /* clipRect= */ null,
+            /* contentInsets= */ null,
+            /* prefixOrderIndex= */ 0,
+            /* position= */ null,
+            /* localBounds= */ null,
+            /* screenSpaceBounds= */ null,
+            new Configuration().windowConfiguration,
+            /* isNotInRecents= */ false,
+            /* startLeash= */ null,
+            /* startBounds= */ null,
+            /* taskInfo= */ mRunningTaskInfo,
+            /* allowEnterPip= */ false);
+    protected final RecentsAnimationTargets mRecentsAnimationTargets = new RecentsAnimationTargets(
+            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+            /* homeContentInsets= */ new Rect(),
+            /* minimizedHomeBounds= */ null,
+            new Bundle());
+
+    @Mock protected ACTIVITY_INTERFACE mActivityInterface;
+    @Mock protected ActivityInitListener<?> mActivityInitListener;
+    @Mock protected RecentsAnimationController mRecentsAnimationController;
+    @Mock protected STATE mState;
+    @Mock protected ViewTreeObserver mViewTreeObserver;
+    @Mock protected DragLayer mDragLayer;
+    @Mock protected LauncherRootView mRootView;
+    @Mock protected SystemUiController mSystemUiController;
+    @Mock protected GestureState mGestureState;
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Before
+    public void setUpRunningTaskInfo() {
+        mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+
+    @Before
+    public void setUpGestureState() {
+        when(mGestureState.getRunningTask()).thenReturn(mCachedTaskInfo);
+        when(mGestureState.getLastAppearedTaskIds()).thenReturn(new int[0]);
+        when(mGestureState.getLastStartedTaskIds()).thenReturn(new int[1]);
+        when(mGestureState.getHomeIntent()).thenReturn(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+        doReturn(mActivityInterface).when(mGestureState).getContainerInterface();
+    }
+
+    @Before
+    public void setUpRecentsView() {
+        RECENTS_VIEW recentsView = getRecentsView();
+        when(recentsView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
+        doAnswer(answer -> {
+            runOnMainSync(() -> answer.<Runnable>getArgument(0).run());
+            return this;
+        }).when(recentsView).runOnPageScrollsInitialized(any());
+    }
+
+    @Before
+    public void setUpRecentsContainer() {
+        RecentsViewContainer recentsContainer = getRecentsContainer();
+        RECENTS_VIEW recentsView = getRecentsView();
+
+        when(recentsContainer.getDeviceProfile()).thenReturn(new DeviceProfile());
+        when(recentsContainer.getOverviewPanel()).thenReturn(recentsView);
+        when(recentsContainer.getDragLayer()).thenReturn(mDragLayer);
+        when(recentsContainer.getRootView()).thenReturn(mRootView);
+        when(recentsContainer.getSystemUiController()).thenReturn(mSystemUiController);
+        when(mActivityInterface.createActivityInitListener(any()))
+                .thenReturn(mActivityInitListener);
+        doReturn(recentsContainer).when(mActivityInterface).getCreatedContainer();
+        doAnswer(answer -> {
+            answer.<Runnable>getArgument(0).run();
+            return this;
+        }).when(recentsContainer).runOnBindToTouchInteractionService(any());
+    }
+
+    @Test
+    public void testInitWhenReady_registersActivityInitListener() {
+        String reasonString = "because i said so";
+
+        createSwipeHandler().initWhenReady(reasonString);
+        verify(mActivityInitListener).register(eq(reasonString));
+    }
+
+    @Test
+    public void testOnRecentsAnimationCanceled_unregistersActivityInitListener() {
+        createSwipeHandler()
+                .onRecentsAnimationCanceled(new HashMap<>());
+
+        runOnMainSync(() -> verify(mActivityInitListener)
+                .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled")));
+    }
+
+    @Test
+    public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() {
+        createSwipeHandler().onConsumerAboutToBeSwitched();
+
+        runOnMainSync(() -> verify(mActivityInitListener)
+                .unregister("AbsSwipeUpHandler.invalidateHandler"));
+    }
+
+    @Test
+    public void testOnConsumerAboutToBeSwitched_midQuickSwitch_unregistersActivityInitListener() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK)
+                .onConsumerAboutToBeSwitched();
+
+        runOnMainSync(() -> verify(mActivityInitListener)
+                .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation")));
+    }
+
+    @Test
+    public void testStartNewTask_finishesRecentsAnimationController() {
+        SWIPE_HANDLER absSwipeUpHandler = createSwipeHandler();
+
+        onRecentsAnimationStart(absSwipeUpHandler);
+
+        runOnMainSync(() -> {
+            absSwipeUpHandler.startNewTask(unused -> {});
+            verify(mRecentsAnimationController).finish(anyBoolean(), any());
+        });
+    }
+
+    @Test
+    public void testHomeGesture_finishesRecentsAnimationController() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController).detachNavigationBarFromApp(true);
+            verify(mRecentsAnimationController).finish(anyBoolean(), any(), anyBoolean());
+        });
+    }
+
+    private SWIPE_HANDLER createSwipeUpHandlerForGesture(GestureState.GestureEndTarget endTarget) {
+        boolean isQuickSwitch = endTarget == GestureState.GestureEndTarget.NEW_TASK;
+
+        doReturn(mState).when(mActivityInterface).stateFromGestureEndTarget(any());
+
+        SWIPE_HANDLER swipeHandler = createSwipeHandler(SystemClock.uptimeMillis(), isQuickSwitch);
+
+        swipeHandler.onActivityInit(/* alreadyOnHome= */ false);
+        swipeHandler.onGestureStarted(isQuickSwitch);
+        onRecentsAnimationStart(swipeHandler);
+
+        when(mGestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]);
+        runOnMainSync(swipeHandler::switchToScreenshot);
+
+        when(mGestureState.getEndTarget()).thenReturn(endTarget);
+        when(mGestureState.isRecentsAnimationRunning()).thenReturn(isQuickSwitch);
+        float xVelocityPxPerMs = isQuickSwitch ? 100 : 0;
+        float yVelocityPxPerMs = isQuickSwitch ? 0 : -100;
+        swipeHandler.onGestureEnded(
+                yVelocityPxPerMs, new PointF(xVelocityPxPerMs, yVelocityPxPerMs));
+        swipeHandler.onCalculateEndTarget();
+        runOnMainSync(swipeHandler::onSettledOnEndTarget);
+
+        return swipeHandler;
+    }
+
+    private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) {
+        when(mActivityInterface.getOverviewWindowBounds(any(), any())).thenReturn(new Rect());
+        doNothing().when(mActivityInterface).setOnDeferredActivityLaunchCallback(any());
+
+        runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart(
+                mRecentsAnimationController, mRecentsAnimationTargets));
+    }
+
+    private static void runOnMainSync(Runnable runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    @NonNull
+    private SWIPE_HANDLER createSwipeHandler() {
+        return createSwipeHandler(SystemClock.uptimeMillis(), false);
+    }
+
+    @NonNull
+    protected abstract SWIPE_HANDLER createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture);
+
+    @NonNull
+    protected abstract RecentsViewContainer getRecentsContainer();
+
+    @NonNull
+    protected abstract RECENTS_VIEW getRecentsView();
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
new file mode 100644
index 0000000..dd0b4b3
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase<
+        RecentsActivity,
+        RecentsState,
+        FallbackRecentsView,
+        RecentsActivity,
+        FallbackActivityInterface,
+        FallbackSwipeHandler> {
+
+    @Mock private RecentsActivity mRecentsActivity;
+    @Mock private FallbackRecentsView mRecentsView;
+
+
+    @Override
+    protected FallbackSwipeHandler createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture) {
+        return new FallbackSwipeHandler(
+                mContext,
+                mRecentsAnimationDeviceState,
+                mTaskAnimationManager,
+                mGestureState,
+                touchTimeMs,
+                continuingLastGesture,
+                mInputConsumerController);
+    }
+
+    @Override
+    protected RecentsActivity getRecentsContainer() {
+        return mRecentsActivity;
+    }
+
+    @Override
+    protected FallbackRecentsView getRecentsView() {
+        return mRecentsView;
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
new file mode 100644
index 0000000..653dc01
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceStateTransitionAnimation;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+import com.android.quickstep.views.RecentsView;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase<
+        QuickstepLauncher,
+        LauncherState,
+        RecentsView<QuickstepLauncher, LauncherState>,
+        QuickstepLauncher,
+        LauncherActivityInterface,
+        LauncherSwipeHandlerV2> {
+
+    @Mock private QuickstepLauncher mQuickstepLauncher;
+    @Mock private RecentsView<QuickstepLauncher, LauncherState> mRecentsView;
+    @Mock private Workspace<?> mWorkspace;
+    @Mock private Hotseat mHotseat;
+    @Mock private WorkspaceStateTransitionAnimation mTransitionAnimation;
+
+    @Before
+    public void setUpQuickStepLauncher() {
+        when(mQuickstepLauncher.createAtomicAnimationFactory())
+                .thenReturn(new AtomicAnimationFactory<>(0));
+        when(mQuickstepLauncher.getHotseat()).thenReturn(mHotseat);
+        doReturn(mWorkspace).when(mQuickstepLauncher).getWorkspace();
+        doReturn(new StateManager(mQuickstepLauncher, LauncherState.NORMAL))
+                .when(mQuickstepLauncher).getStateManager();
+
+    }
+
+    @Before
+    public void setUpWorkspace() {
+        when(mWorkspace.getStateTransitionAnimation()).thenReturn(mTransitionAnimation);
+    }
+
+    @Override
+    protected LauncherSwipeHandlerV2 createSwipeHandler(
+            long touchTimeMs, boolean continuingLastGesture) {
+        return new LauncherSwipeHandlerV2(
+                mContext,
+                mRecentsAnimationDeviceState,
+                mTaskAnimationManager,
+                mGestureState,
+                touchTimeMs,
+                continuingLastGesture,
+                mInputConsumerController);
+    }
+
+    @Override
+    protected QuickstepLauncher getRecentsContainer() {
+        return mQuickstepLauncher;
+    }
+
+    @Override
+    protected RecentsView<QuickstepLauncher, LauncherState> getRecentsView() {
+        return mRecentsView;
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 2087016..9bc1c59 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -15,20 +15,18 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
 
+import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.quickstep.util.SplitScreenTestUtils;
 
 import org.junit.Test;
 
@@ -70,39 +68,17 @@
 
     @Test
     public void testSplitTaskTapBothIconMenus() {
-        createAndLaunchASplitPair();
+        Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
 
-        OverviewTaskMenu taskMenu =
-                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+        OverviewTaskMenu taskMenu = overview.getCurrentTask().tapMenu();
         assertTrue("App info item not appearing in expanded task menu.",
                 taskMenu.hasMenuItem("App info"));
         taskMenu.touchOutsideTaskMenuToDismiss();
 
-        OverviewTaskMenu splitMenu =
-                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu(
+        OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
                         OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
         assertTrue("App info item not appearing in expanded split task's menu.",
                 splitMenu.hasMenuItem("App info"));
         splitMenu.touchOutsideTaskMenuToDismiss();
     }
-
-    private void createAndLaunchASplitPair() {
-        clearAllRecentTasks();
-
-        startTestActivity(2);
-        startTestActivity(3);
-
-        if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
-            mLauncher.goHome().switchToOverview().getOverviewActions()
-                    .clickSplit()
-                    .getTestActivityTask(2)
-                    .open();
-        } else {
-            mLauncher.goHome().switchToOverview().getCurrentTask()
-                    .tapMenu()
-                    .tapSplitMenuItem()
-                    .getCurrentTask()
-                    .open();
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index f971afb..943c1bd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -290,9 +290,6 @@
         }
     }
 
-    // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
-
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
@@ -481,7 +478,8 @@
 //        assertTrue("Launcher internal state didn't remain in Overview",
 //                isInState(() -> LauncherState.OVERVIEW));
 //        overview.getCurrentTask().dismiss();
-//        executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple dismissals",
+//        executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple
+//        dismissals",
 //                (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet(
 //                        launcher)) <= 1)));
 
@@ -592,7 +590,8 @@
         if (overview.hasTasks()) {
             currentTask = overview.getCurrentTask();
             assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home",
-                    currentTask.containsContentDescription("ExcludeFromRecents")
+                    currentTask.containsContentDescription(
+                            "ExcludeFromRecents")
                             || currentTask.containsContentDescription(null));
         } else {
             // Presumably the test started with 0 tasks and remains that way after going home.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 733ea4e..daa4ec3 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -17,8 +17,6 @@
 
 
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -33,7 +31,7 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.Taskbar;
 import com.android.launcher3.tapl.TaskbarAppIcon;
-import com.android.launcher3.util.rule.TestStabilityRule;
+import com.android.quickstep.util.SplitScreenTestUtils;
 import com.android.wm.shell.Flags;
 
 import org.junit.After;
@@ -110,9 +108,8 @@
         assumeTrue("App pairs feature is currently not enabled, no test needed",
                 Flags.enableAppPairs());
 
-        createAndLaunchASplitPair();
+        Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
 
-        Overview overview = mLauncher.goHome().switchToOverview();
         if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) {
             assertTrue("Save app pair menu item is missing",
                     overview.getCurrentTask()
@@ -156,24 +153,4 @@
         TaskbarAppIcon firstApp = taskbar.getAppIcon(firstAppName);
         firstApp.launchIntoSplitScreen();
     }
-
-    private void createAndLaunchASplitPair() {
-        clearAllRecentTasks();
-
-        startTestActivity(2);
-        startTestActivity(3);
-
-        if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
-            mLauncher.goHome().switchToOverview().getOverviewActions()
-                    .clickSplit()
-                    .getTestActivityTask(2)
-                    .open();
-        } else {
-            mLauncher.goHome().switchToOverview().getCurrentTask()
-                    .tapMenu()
-                    .tapSplitMenuItem()
-                    .getCurrentTask()
-                    .open();
-        }
-    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
new file mode 100644
index 0000000..82361aa
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import androidx.test.uiautomator.By
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.launcher3.tapl.Overview
+import com.android.launcher3.tapl.OverviewTask
+import com.android.launcher3.ui.AbstractLauncherUiTest
+
+object SplitScreenTestUtils {
+
+    /** Creates 2 tasks and makes a split mode pair. Also asserts the accessibility labels. */
+    @JvmStatic
+    fun createAndLaunchASplitPairInOverview(launcher: LauncherInstrumentation): Overview {
+        clearAllRecentTasks(launcher)
+
+        AbstractLauncherUiTest.startTestActivity(2)
+        AbstractLauncherUiTest.startTestActivity(3)
+
+        val overView = launcher.goHome().switchToOverview()
+        if (launcher.isTablet && !launcher.isGridOnlyOverviewEnabled) {
+            overView.overviewActions.clickSplit().getTestActivityTask(2).open()
+        } else {
+            overView.currentTask.tapMenu().tapSplitMenuItem().currentTask.open()
+        }
+
+        val overviewWithSplitPair = launcher.goHome().switchToOverview()
+        val currentTask = overviewWithSplitPair.currentTask
+        currentTask.containsContentDescription(
+            By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
+            OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT
+        )
+        currentTask.containsContentDescription(
+            By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
+            OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT
+        )
+        return overviewWithSplitPair
+    }
+
+    private fun clearAllRecentTasks(launcher: LauncherInstrumentation) {
+        if (launcher.recentTasks.isNotEmpty()) {
+            launcher.goHome().switchToOverview().dismissAllTasks()
+        }
+    }
+}
diff --git a/res/color-night-v31/popup_color_background.xml b/res/color-night-v31/popup_color_background.xml
deleted file mode 100644
index 13ceaa0..0000000
--- a/res/color-night-v31/popup_color_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item
-        android:color="@android:color/system_neutral1_900"
-        android:lStar="12" />
-</selector>
diff --git a/res/color-v31/popup_color_background.xml b/res/color-v31/popup_color_background.xml
deleted file mode 100644
index 99155d8..0000000
--- a/res/color-v31/popup_color_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item
-        android:color="@android:color/system_neutral1_50"
-        android:lStar="94" />
-</selector>
diff --git a/res/color/popup_color_background.xml b/res/color/popup_color_background.xml
deleted file mode 100644
index e87e772..0000000
--- a/res/color/popup_color_background.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?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.
--->
-<selector xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:color="?attr/popupColorBackground" />
-</selector>
diff --git a/res/drawable/popup_background.xml b/res/drawable/popup_background.xml
index 6eedecb..4ddd228 100644
--- a/res/drawable/popup_background.xml
+++ b/res/drawable/popup_background.xml
@@ -15,6 +15,6 @@
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <solid android:color="?attr/popupColorBackground"/>
+    <solid android:color="?attr/materialColorSurfaceContainer"/>
     <corners android:radius="@dimen/dialogCornerRadius"/>
 </shape>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 6151b5f..57c9bc7 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -25,7 +25,6 @@
     <attr name="popupColorPrimary" format="color" />
     <attr name="popupColorSecondary" format="color" />
     <attr name="popupColorTertiary" format="color" />
-    <attr name="popupColorBackground" format="color" />
     <attr name="popupTextColor" format="color" />
     <attr name="popupShadeFirst" format="color" />
     <attr name="popupShadeSecond" format="color" />
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 2d7808b..728c523 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -90,7 +90,6 @@
         <item name="popupColorPrimary">@color/popup_color_primary_light</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
-        <item name="popupColorBackground">#EFEDED</item>
         <item name="popupTextColor">@color/system_on_surface_light</item>
         <item name="popupShadeFirst">@color/popup_shade_first_light</item>
         <item name="popupShadeSecond">@color/popup_shade_second_light</item>
@@ -165,7 +164,6 @@
         <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
         <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
         <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
-        <item name="popupColorBackground">#1F2020</item>
         <item name="popupTextColor">@color/system_on_surface_dark</item>
         <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
         <item name="popupShadeFirst">@color/popup_shade_first_dark</item>
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index ef56246..b51e850 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,8 +37,6 @@
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
-import com.android.launcher3.debug.TestEventEmitter;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.InstanceId;
@@ -223,9 +221,6 @@
         dl.addView(frame);
         frame.mIsOpen = true;
         frame.post(() -> frame.snapToWidget(false));
-        TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
-                TestEvent.RESIZE_FRAME_SHOWING
-        );
     }
 
     private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 5cbf6fb..8ae6d73 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -316,6 +316,74 @@
     // DragController
     public int flingToDeleteThresholdVelocity;
 
+    /** Used only as an alternative to mocking when null values cannot be used. */
+    @VisibleForTesting
+    public DeviceProfile() {
+        inv = null;
+        mInfo = null;
+        mMetrics = null;
+        mIconSizeSteps = null;
+        isTablet = false;
+        isPhone = false;
+        transposeLayoutWithOrientation = false;
+        isMultiDisplay = false;
+        isTwoPanels = false;
+        isPredictiveBackSwipe = false;
+        isQsbInline = false;
+        isLandscape = false;
+        isMultiWindowMode = false;
+        isGestureMode = false;
+        isLeftRightSplit = false;
+        windowX = 0;
+        windowY = 0;
+        widthPx = 0;
+        heightPx = 0;
+        availableWidthPx = 0;
+        availableHeightPx = 0;
+        rotationHint = 0;
+        aspectRatio = 1;
+        mIsScalableGrid = false;
+        mTypeIndex = 0;
+        mIsResponsiveGrid = false;
+        desiredWorkspaceHorizontalMarginOriginalPx = 0;
+        edgeMarginPx = 0;
+        workspaceContentScale = 0;
+        workspaceSpringLoadedMinNextPageVisiblePx = 0;
+        extraSpace = 0;
+        workspacePageIndicatorHeight = 0;
+        mWorkspacePageIndicatorOverlapWorkspace = 0;
+        numFolderRows = 0;
+        numFolderColumns = 0;
+        folderLabelTextScale = 0;
+        areNavButtonsInline = false;
+        mHotseatBarEdgePaddingPx = 0;
+        mHotseatBarWorkspaceSpacePx = 0;
+        hotseatQsbWidth = 0;
+        hotseatQsbHeight = 0;
+        hotseatQsbVisualHeight = 0;
+        hotseatQsbShadowHeight = 0;
+        hotseatBorderSpace = 0;
+        mMinHotseatIconSpacePx = 0;
+        mMinHotseatQsbWidthPx = 0;
+        mMaxHotseatIconSpacePx = 0;
+        inlineNavButtonsEndSpacingPx = 0;
+        mBubbleBarSpaceThresholdPx = 0;
+        numShownAllAppsColumns = 0;
+        overviewActionsHeight = 0;
+        overviewActionsTopMarginPx = 0;
+        overviewActionsButtonSpacing = 0;
+        mViewScaleProvider = null;
+        mDotRendererWorkSpace = null;
+        mDotRendererAllApps = null;
+        taskbarHeight = 0;
+        stashedTaskbarHeight = 0;
+        taskbarBottomMargin = 0;
+        taskbarIconSize = 0;
+        mTransientTaskbarClaimedSpace = 0;
+        startAlignTaskbar = false;
+        isTransientTaskbar = false;
+    }
+
     /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
     DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
             SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 4d4a8f7..c2debfa 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.popup;
 
-import static androidx.core.content.ContextCompat.getColorStateList;
-
 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.app.animation.Interpolators.LINEAR;
@@ -56,8 +54,6 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 
-import java.util.Arrays;
-
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
  *
@@ -130,7 +126,7 @@
     // Tag for Views that have children that will need to be iterated to add styling.
     private final String mIterateChildrenTag;
 
-    protected final int[] mColorIds;
+    protected final int[] mColors;
 
     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -142,8 +138,7 @@
 
         // Initialize arrow view
         final Resources resources = getResources();
-        mArrowColor = getColorStateList(getContext(), R.color.popup_color_background)
-                .getDefaultColor();
+        mArrowColor = Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceContainer);
         mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin);
         mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
         mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
@@ -158,21 +153,25 @@
         mRoundedTop = new GradientDrawable();
         int popupPrimaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
         mRoundedTop.setColor(popupPrimaryColor);
-        mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius,
+        mRoundedTop.setCornerRadii(new float[]{mOutlineRadius, mOutlineRadius, mOutlineRadius,
                 mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius});
 
         mRoundedBottom = new GradientDrawable();
         mRoundedBottom.setColor(popupPrimaryColor);
-        mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius,
+        mRoundedBottom.setCornerRadii(new float[]{smallerRadius, smallerRadius, smallerRadius,
                 smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
 
         mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children);
 
         if (mActivityContext.canUseMultipleShadesForPopup()) {
-            mColorIds = new int[]{R.color.popup_shade_first, R.color.popup_shade_second,
-                    R.color.popup_shade_third};
+            mColors = new int[]{
+                    getContext().getColor(R.color.popup_shade_first),
+                    getContext().getColor(R.color.popup_shade_second),
+                    getContext().getColor(R.color.popup_shade_third)
+            };
         } else {
-            mColorIds = new int[]{R.color.popup_color_background};
+            mColors = new int[]{Themes.getAttrColor(getContext(),
+                    R.attr.materialColorSurfaceContainer)};
         }
     }
 
@@ -219,15 +218,14 @@
     }
 
     /**
-     * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColorIds}.
+     * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColors}.
      *                        Otherwise, we will use this color for all child views.
      */
     protected void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) {
         int[] colors = null;
         if (backgroundColor == Color.TRANSPARENT) {
             // Lazily get the colors so they match the current wallpaper colors.
-            colors = Arrays.stream(mColorIds).map(
-                    r -> getColorStateList(getContext(), r).getDefaultColor()).toArray();
+            colors = mColors;
         }
 
         int count = viewGroup.getChildCount();
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 62eed5c..82cc40d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.views;
 
-import static androidx.core.content.ContextCompat.getColorStateList;
-
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
@@ -147,8 +145,7 @@
 
     @Override
     public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
-        assignMarginsAndBackgrounds(viewGroup,
-                getColorStateList(getContext(), mColorIds[0]).getDefaultColor());
+        assignMarginsAndBackgrounds(viewGroup, mColors[0]);
         // last shortcut doesn't need bottom margin
         final int count = viewGroup.getChildCount() - 1;
         for (int i = 0; i < count; i++) {
diff --git a/tests/Android.bp b/tests/Android.bp
index 1fa6e05..9f62d02 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -127,9 +127,9 @@
         "com_android_launcher3_flags_lib",
     ],
     libs: [
-        "android.test.base",
-        "android.test.runner",
-        "android.test.mock",
+        "android.test.base.stubs.system",
+        "android.test.runner.stubs.system",
+        "android.test.mock.stubs.system",
     ],
     // Libraries used by mockito inline extended
     jni_libs: [
@@ -229,9 +229,9 @@
         "android.appwidget.flags-aconfig-java",
     ],
     libs: [
-        "android.test.runner",
-        "android.test.base",
-        "android.test.mock",
+        "android.test.runner.stubs.system",
+        "android.test.base.stubs.system",
+        "android.test.mock.stubs.system",
         "truth",
     ],
     instrumentation_for: "Launcher3",
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index ab48a21..5433fa7 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -284,8 +284,9 @@
      *
      * TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
      */
-    public boolean containsContentDescription(@Nullable String expected) {
-        String actual = mTask.getContentDescription();
+    public boolean containsContentDescription(@Nullable String expected,
+            OverviewSplitTask overviewSplitTask) {
+        String actual = findObjectInTask(overviewSplitTask.snapshotRes).getContentDescription();
         if (actual == null && expected == null) {
             return true;
         }
@@ -295,6 +296,14 @@
         return actual.contains(expected);
     }
 
+    /**
+     * Returns whether the given String is contained in this Task's contentDescription. Also returns
+     * true if both Strings are null
+     */
+    public boolean containsContentDescription(@Nullable String expected) {
+        return containsContentDescription(expected, DEFAULT);
+    }
+
     private TaskViewType getType() {
         String resourceName = mTask.getResourceName();
         if (resourceName.endsWith("task_view_grouped")) {