Merge "Remove padding from All Apps and Divider views." into main
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 0472007..5270284 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -24,10 +24,6 @@
     android:focusable="true"
     launcher:focusBorderColor="?attr/materialColorOutline"
     launcher:hoverBorderColor="?attr/materialColorPrimary">
-    <View
-        android:id="@+id/background"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
 
     <ViewStub
         android:id="@+id/icon"
@@ -38,6 +34,13 @@
     <com.android.quickstep.views.DesktopTaskContentView
         android:id="@+id/desktop_content"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent">
+
+        <View
+            android:id="@+id/background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </com.android.quickstep.views.DesktopTaskContentView>
 
 </com.android.quickstep.views.DesktopTaskView>
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 3b7ad3e..de42669 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -299,10 +299,6 @@
             return;
         }
         mQuickSwitchViewController.closeQuickSwitchView(animate);
-        if (mOnClosed != null) {
-            mOnClosed.run();
-            mOnClosed = null;
-        }
     }
 
     /**
@@ -394,6 +390,13 @@
             });
         }
 
+        void onCloseStarted() {
+            if (mOnClosed != null) {
+                mOnClosed.run();
+                mOnClosed = null;
+            }
+        }
+
         void onCloseComplete() {
             if (Flags.taskbarOverflow() && mOverlayContext != null) {
                 mOverlayContext.getDragLayer()
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 985cc26..e623b21 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -198,6 +198,7 @@
             // Let currently-running animation finish.
             return;
         }
+        mControllerCallbacks.onCloseStarted();
         if (!animate) {
             InteractionJankMonitorWrapper.begin(
                     mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
diff --git a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
index c0c2a02..860e822 100644
--- a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -46,7 +46,7 @@
 class ManageWindowsTaskbarShortcut<T>(
     private val target: T,
     private val itemInfo: ItemInfo?,
-    private val originalView: View?,
+    private val originalView: View,
     private val controllers: TaskbarControllers,
 ) :
     SystemShortcut<T>(
@@ -123,7 +123,7 @@
 
         taskbarShortcutAllWindowsView =
             TaskbarShortcutManageWindowsView(
-                originalView!!,
+                originalView,
                 controllers.taskbarOverlayController.requestWindow(),
                 taskList,
                 onIconClickListener,
@@ -160,7 +160,7 @@
 
         /** Adds the carousel menu to the taskbar overlay drag layer */
         override fun addToContainer(menuView: ManageWindowsView) {
-            taskbarOverlayContext.dragLayer.post { positionCarouselMenu() }
+            positionCarouselMenu()
 
             controllers.taskbarAutohideSuspendController.updateFlag(
                 FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
@@ -184,28 +184,28 @@
          * align with the calling app while ensuring it doesn't go beyond the screen edge.
          */
         private fun positionCarouselMenu() {
+            val deviceProfile = taskbarActivityContext.deviceProfile
             val margin =
                 context.resources.getDimension(
                     R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge
                 )
 
             // Calculate the Y position to place the carousel above the taskbar
-            val availableHeight = taskbarOverlayContext.dragLayer.height
             menuView.rootView.y =
-                availableHeight -
+                deviceProfile.availableHeightPx -
                     menuView.menuHeight -
                     controllers.taskbarStashController.touchableHeight -
                     margin
 
             // Calculate the X position to align with the calling app,
             // but avoid clashing with the screen edge
-            val availableWidth = taskbarOverlayContext.dragLayer.width
-            if (Utilities.isRtl(context.resources)) {
-                menuView.rootView.translationX = -(availableWidth - menuView.menuWidth) / 2f
-            } else {
-                val maxX = availableWidth - menuView.menuWidth - margin
-                menuView.rootView.translationX = minOf(originalView.x, maxX)
-            }
+            menuView.rootView.translationX =
+                if (Utilities.isRtl(context.resources)) {
+                    -(deviceProfile.availableWidthPx - menuView.menuWidth) / 2f
+                } else {
+                    val maxX = deviceProfile.availableWidthPx - menuView.menuWidth - margin
+                    minOf(originalView.x, maxX)
+                }
         }
 
         /** Closes the carousel menu and removes it from the taskbar overlay drag layer */
@@ -227,8 +227,8 @@
         override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
             ev?.let {
                 if (
-                    ev.action == MotionEvent.ACTION_DOWN &&
-                        !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, ev)
+                    it.action == MotionEvent.ACTION_DOWN &&
+                        !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it)
                 ) {
                     removeFromContainer()
                 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index e05178d..f9e7cf0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.view.KeyEvent.ACTION_UP;
 import static android.view.View.AccessibilityDelegate;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -72,7 +73,6 @@
 import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
-import android.os.SystemClock;
 import android.util.Property;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -864,17 +864,12 @@
             TaskbarNavButtonController navButtonController) {
         buttonView.setOnTouchListener((v, event) -> {
             if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
-            long time = SystemClock.uptimeMillis();
-            int action = event.getAction();
-            KeyEvent keyEvent = new KeyEvent(time, time,
-                    action == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
-                    KeyEvent.KEYCODE_BACK, 0);
-            if (event.getAction() == MotionEvent.ACTION_CANCEL) {
-                keyEvent.cancel();
-            }
-            navButtonController.executeBack(keyEvent);
-
-            if (action == MotionEvent.ACTION_UP) {
+            int motionEventAction = event.getAction();
+            int keyEventAction = motionEventAction == MotionEvent.ACTION_DOWN
+                    ? KeyEvent.ACTION_DOWN : ACTION_UP;
+            boolean isCancelled = event.getAction() == MotionEvent.ACTION_CANCEL;
+            navButtonController.sendBackKeyEvent(keyEventAction, isCancelled);
+            if (motionEventAction == MotionEvent.ACTION_UP) {
                 buttonView.performClick();
             }
             return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index dce377d..f33666a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -475,7 +475,7 @@
         }
         mControllers.bubbleControllers.ifPresent(controllers -> {
             // Show the bubble bar when on launcher home (hotseat icons visible) or in overview
-            boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
+            boolean onOverview = isInLauncher && mLauncherState == LauncherState.OVERVIEW;
             boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible(
                     mLauncher, HOTSEAT_ICONS);
             BubbleLauncherState state = onOverview
@@ -1009,12 +1009,7 @@
 
         @Override
         public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-            endGestureStateOverride(!controller.getFinishTargetIsLauncher(),
-                    controller.getLauncherIsVisibleAtFinish(), false /*canceled*/);
-        }
-
-        private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
-            endGestureStateOverride(finishedToApp, finishedToApp, canceled);
+            endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
         }
 
         /**
@@ -1024,13 +1019,11 @@
          *
          * @param finishedToApp {@code true} if the recents animation finished to showing an app and
          *                      not workspace or overview
-         * @param launcherIsVisible {code true} if launcher is visible at finish
          * @param canceled      {@code true} if the recents animation was canceled instead of
          *                      finishing
          *                      to completion
          */
-        private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible,
-                boolean canceled) {
+        private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
             mCallbacks.removeListener(this);
             mTaskBarRecentsAnimationListener = null;
             ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
@@ -1039,27 +1032,18 @@
                 mSkipNextRecentsAnimEnd = false;
                 return;
             }
-            updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible);
+            updateStateForUserFinishedToApp(finishedToApp);
         }
     }
 
     /**
-     * @see #updateStateForUserFinishedToApp(boolean, boolean)
-     */
-    private void updateStateForUserFinishedToApp(boolean finishedToApp) {
-        updateStateForUserFinishedToApp(finishedToApp, !finishedToApp);
-    }
-
-    /**
      * Updates the visible state immediately to ensure a seamless handoff.
      *
      * @param finishedToApp True iff user is in an app.
-     * @param launcherIsVisible True iff launcher is still visible (ie. transparent app)
      */
-    private void updateStateForUserFinishedToApp(boolean finishedToApp,
-            boolean launcherIsVisible) {
+    private void updateStateForUserFinishedToApp(boolean finishedToApp) {
         // Update the visible state immediately to ensure a seamless handoff
-        boolean launcherVisible = !finishedToApp || launcherIsVisible;
+        boolean launcherVisible = !finishedToApp;
         updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
         updateStateForFlag(FLAG_VISIBLE, launcherVisible);
         applyState();
@@ -1068,7 +1052,7 @@
         if (DEBUG) {
             Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
         }
-        controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible);
+        controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
         controller.applyState();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 7ab9ef3..d4814d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -46,6 +46,7 @@
 import android.os.Trace;
 import android.provider.Settings;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.WindowManager;
@@ -115,13 +116,13 @@
     private final Context mContext;
     private final @Nullable Context mNavigationBarPanelContext;
     private WindowManager mWindowManager;
-    private FrameLayout mTaskbarRootLayout;
+    private FrameLayout mDefaultRootLayout;
     private boolean mAddedWindow;
-    private final TaskbarNavButtonController mNavButtonController;
-    private final ComponentCallbacks mComponentCallbacks;
+    private final TaskbarNavButtonController mDefaultNavButtonController;
+    private final ComponentCallbacks mDefaultComponentCallbacks;
 
     private final SimpleBroadcastReceiver mShutdownReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyExistingTaskbar());
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
 
     // The source for this provider is set when Launcher is available
     // We use 'non-destroyable' version here so the original provider won't be destroyed
@@ -129,8 +130,8 @@
     // It's destruction/creation will be managed by the activity.
     private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider =
             new NonDestroyableScopedUnfoldTransitionProgressProvider();
-
-    private TaskbarActivityContext mTaskbarActivityContext;
+    /** DisplayId - {@link TaskbarActivityContext} map for Connected Display. */
+    private final SparseArray<TaskbarActivityContext> mTaskbars = new SparseArray<>();
     private StatefulActivity mActivity;
     private RecentsViewContainer mRecentsViewContainer;
 
@@ -167,7 +168,9 @@
     private final Runnable mActivityOnDestroyCallback = new Runnable() {
         @Override
         public void run() {
+            int displayId = getDefaultDisplayId();
             if (mActivity != null) {
+                displayId = mActivity.getDisplayId();
                 mActivity.removeOnDeviceProfileChangeListener(
                         mDebugActivityDeviceProfileChanged);
                 Log.d(TASKBAR_NOT_DESTROYED_TAG,
@@ -180,8 +183,9 @@
             }
             mActivity = null;
             debugWhyTaskbarNotDestroyed("clearActivity");
-            if (mTaskbarActivityContext != null) {
-                mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
+            TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+            if (taskbar != null) {
+                taskbar.setUIController(TaskbarUIController.DEFAULT);
             }
             mUnfoldProgressProvider.setSourceProvider(null);
         }
@@ -236,27 +240,27 @@
         mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
-            mTaskbarRootLayout = new FrameLayout(mContext) {
+            mDefaultRootLayout = new FrameLayout(mContext) {
                 @Override
                 public boolean dispatchTouchEvent(MotionEvent ev) {
                     // The motion events can be outside the view bounds of task bar, and hence
                     // manually dispatching them to the drag layer here.
-                    if (mTaskbarActivityContext != null
-                            && mTaskbarActivityContext.getDragLayer().isAttachedToWindow()) {
-                        return mTaskbarActivityContext.getDragLayer().dispatchTouchEvent(ev);
+                    TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+                    if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
+                        return taskbar.getDragLayer().dispatchTouchEvent(ev);
                     }
                     return super.dispatchTouchEvent(ev);
                 }
             };
         }
-        mNavButtonController = new TaskbarNavButtonController(
+        mDefaultNavButtonController = new TaskbarNavButtonController(
                 context,
                 navCallbacks,
                 SystemUiProxy.INSTANCE.get(mContext),
                 ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
                 ContextualSearchInvoker.newInstance(mContext));
-        mComponentCallbacks = new ComponentCallbacks() {
+        mDefaultComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
             @Override
@@ -265,6 +269,7 @@
                         "onConfigurationChanged: " + newConfig);
                 debugWhyTaskbarNotDestroyed(
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
+                // TODO: adapt this logic to be specific to different displays.
                 DeviceProfile dp = mUserUnlocked
                         ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
                         : null;
@@ -281,12 +286,12 @@
 
                 debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
                         + "configDiff=" + Configuration.configurationDiffToString(configDiff));
-                if (configDiff != 0 || mTaskbarActivityContext == null) {
+                if (configDiff != 0 || getCurrentActivityContext() == null) {
                     recreateTaskbar();
                 } else {
                     // Config change might be handled without re-creating the taskbar
                     if (dp != null && !isTaskbarEnabled(dp)) {
-                        destroyExistingTaskbar();
+                        destroyDefaultTaskbar();
                     } else {
                         if (dp != null && isTaskbarEnabled(dp)) {
                             if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
@@ -295,10 +300,10 @@
                                 // block above?
                                 recreateTaskbar();
                             } else {
-                                mTaskbarActivityContext.updateDeviceProfile(dp);
+                                getCurrentActivityContext().updateDeviceProfile(dp);
                             }
                         }
-                        mTaskbarActivityContext.onConfigurationChanged(configDiff);
+                        getCurrentActivityContext().onConfigurationChanged(configDiff);
                     }
                 }
                 mOldConfig = new Configuration(newConfig);
@@ -315,7 +320,7 @@
         SettingsCache.INSTANCE.get(mContext)
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        mContext.registerComponentCallbacks(mComponentCallbacks);
+        mContext.registerComponentCallbacks(mDefaultComponentCallbacks);
         mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
@@ -331,13 +336,25 @@
         recreateTaskbar();
     }
 
-    private void destroyExistingTaskbar() {
-        debugWhyTaskbarNotDestroyed("destroyExistingTaskbar: " + mTaskbarActivityContext);
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onDestroy();
-            if (!ENABLE_TASKBAR_NAVBAR_UNIFICATION || enableTaskbarNoRecreate()) {
-                mTaskbarActivityContext = null;
-            }
+    private void destroyAllTaskbars() {
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            destroyTaskbarForDisplay(displayId);
+        }
+    }
+
+    private void destroyDefaultTaskbar() {
+        destroyTaskbarForDisplay(getDefaultDisplayId());
+    }
+
+    private void destroyTaskbarForDisplay(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        debugWhyTaskbarNotDestroyed(
+                "destroyTaskbarForDisplay: " + taskbar + " displayId=" + displayId);
+        if (taskbar != null) {
+            taskbar.onDestroy();
+            // remove all defaults that we store
+            removeTaskbarFromMap(displayId);
         }
         DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
@@ -350,8 +367,10 @@
      * Show Taskbar upon receiving broadcast
      */
     private void showTaskbarFromBroadcast(Intent intent) {
-        if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.showTaskbarFromBroadcast();
+        // TODO: make this code displayId specific
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && taskbar != null) {
+            taskbar.showTaskbarFromBroadcast();
         }
     }
 
@@ -359,12 +378,13 @@
      * Toggles All Apps for Taskbar or Launcher depending on the current state.
      */
     public void toggleAllApps() {
-        if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar == null || taskbar.canToggleHomeAllApps()) {
             // Home All Apps should be toggled from this class, because the controllers are not
             // initialized when Taskbar is disabled (i.e. TaskbarActivityContext is null).
             if (mActivity instanceof Launcher l) l.toggleAllAppsSearch();
         } else {
-            mTaskbarActivityContext.toggleAllAppsSearch();
+            taskbar.toggleAllAppsSearch();
         }
     }
 
@@ -375,8 +395,8 @@
      * progress.
      */
     public AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) {
-        return mTaskbarActivityContext == null
-                ? null : mTaskbarActivityContext.createLauncherStartFromSuwAnim(duration);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        return taskbar == null ? null : taskbar.createLauncherStartFromSuwAnim(duration);
     }
 
     /**
@@ -386,7 +406,7 @@
         mUserUnlocked = true;
         DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
         recreateTaskbar();
-        addTaskbarRootViewToWindow();
+        addTaskbarRootViewToWindow(getDefaultDisplayId());
     }
 
     /**
@@ -429,8 +449,9 @@
             mActivityOnDestroyCallback.run();
         }
         mRecentsViewContainer = recentsViewContainer;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setUIController(
+        TaskbarActivityContext taskbar = getCurrentActivityContext();
+        if (taskbar != null) {
+            taskbar.setUIController(
                     createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
@@ -472,11 +493,21 @@
 
     /**
      * This method is called multiple times (ex. initial init, then when user unlocks) in which case
-     * we fully want to destroy an existing taskbar and create a new one.
+     * we fully want to destroy the existing default display's taskbar and create a new one.
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
+        // TODO: make this recreate all taskbars in map.
+        recreateTaskbarForDisplay(getDefaultDisplayId());
+    }
+
+    /**
+     * This method is called multiple times (ex. initial init, then when user unlocks) in which case
+     * we fully want to destroy an existing taskbar for a specified display and create a new one.
+     * In other case (folding/unfolding) we don't need to remove and add window.
+     */
+    private void recreateTaskbarForDisplay(int displayId) {
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -486,7 +517,7 @@
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
             mAllAppsActionManager.setTaskbarPresent(isLargeScreenTaskbar);
 
-            destroyExistingTaskbar();
+            destroyTaskbarForDisplay(displayId);
 
             boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
             debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
@@ -501,29 +532,31 @@
                 }
             }
 
-            if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
-                mTaskbarActivityContext = new TaskbarActivityContext(mContext,
-                        mNavigationBarPanelContext, dp, mNavButtonController,
+            TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+            if (enableTaskbarNoRecreate() || taskbar == null) {
+                taskbar = new TaskbarActivityContext(mContext,
+                        mNavigationBarPanelContext, dp, mDefaultNavButtonController,
                         mUnfoldProgressProvider, mDesktopVisibilityController);
             } else {
-                mTaskbarActivityContext.updateDeviceProfile(dp);
+                taskbar.updateDeviceProfile(dp);
             }
             mSharedState.startTaskbarVariantIsTransient =
-                    DisplayController.isTransientTaskbar(mTaskbarActivityContext);
+                    DisplayController.isTransientTaskbar(taskbar);
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
-            mTaskbarActivityContext.init(mSharedState);
+            taskbar.init(mSharedState);
 
             if (mRecentsViewContainer != null) {
-                mTaskbarActivityContext.setUIController(
+                taskbar.setUIController(
                         createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
 
             if (enableTaskbarNoRecreate()) {
-                addTaskbarRootViewToWindow();
-                mTaskbarRootLayout.removeAllViews();
-                mTaskbarRootLayout.addView(mTaskbarActivityContext.getDragLayer());
-                mTaskbarActivityContext.notifyUpdateLayoutParams();
+                addTaskbarRootViewToWindow(displayId);
+                mDefaultRootLayout.removeAllViews();
+                mDefaultRootLayout.addView(taskbar.getDragLayer());
+                taskbar.notifyUpdateLayoutParams();
             }
+            addTaskbarToMap(displayId, taskbar);
         } finally {
             Trace.endSection();
         }
@@ -535,14 +568,15 @@
                     mSharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString));
         }
         mSharedState.sysuiStateFlags = systemUiStateFlags;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */);
         }
     }
 
     public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
-        if (mNavButtonController != null) {
-            mNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+        if (mDefaultNavButtonController != null) {
+            mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
         }
     }
 
@@ -551,46 +585,53 @@
      */
     public void setSetupUIVisible(boolean isVisible) {
         mSharedState.setupUIVisible = isVisible;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setSetupUIVisible(isVisible);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.setSetupUIVisible(isVisible);
         }
     }
 
     public void setWallpaperVisible(boolean isVisible) {
         mSharedState.wallpaperVisible = isVisible;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setWallpaperVisible(isVisible);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.setWallpaperVisible(isVisible);
         }
     }
 
     public void checkNavBarModes(int displayId) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.checkNavBarModes();
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.checkNavBarModes();
         }
     }
 
     public void finishBarAnimations(int displayId) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.finishBarAnimations();
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.finishBarAnimations();
         }
     }
 
     public void touchAutoDim(int displayId, boolean reset) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.touchAutoDim(reset);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.touchAutoDim(reset);
         }
     }
 
     public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
             boolean animate) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.transitionTo(barMode, animate);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.transitionTo(barMode, animate);
         }
     }
 
     public void appTransitionPending(boolean pending) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.appTransitionPending(pending);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.appTransitionPending(pending);
         }
     }
 
@@ -599,8 +640,9 @@
     }
 
     public void onRotationProposal(int rotation, boolean isValid) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onRotationProposal(rotation, isValid);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onRotationProposal(rotation, isValid);
         }
     }
 
@@ -608,38 +650,43 @@
         mSharedState.disableNavBarDisplayId = displayId;
         mSharedState.disableNavBarState1 = state1;
         mSharedState.disableNavBarState2 = state2;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.disableNavBarElements(displayId, state1, state2, animate);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.disableNavBarElements(displayId, state1, state2, animate);
         }
     }
 
     public void onSystemBarAttributesChanged(int displayId, int behavior) {
         mSharedState.systemBarAttrsDisplayId = displayId;
         mSharedState.systemBarAttrsBehavior = behavior;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onSystemBarAttributesChanged(displayId, behavior);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.onSystemBarAttributesChanged(displayId, behavior);
         }
     }
 
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
         mSharedState.barMode = barMode;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onTransitionModeUpdated(barMode, checkBarModes);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onTransitionModeUpdated(barMode, checkBarModes);
         }
     }
 
     public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
         mSharedState.navButtonsDarkIntensity = darkIntensity;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onNavButtonsDarkIntensityChanged(darkIntensity);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onNavButtonsDarkIntensityChanged(darkIntensity);
         }
     }
 
     public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
         mSharedState.mLumaSamplingDisplayId = displayId;
         mSharedState.mIsLumaSamplingEnabled = enable;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onNavigationBarLumaSamplingEnabled(displayId, enable);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.onNavigationBarLumaSamplingEnabled(displayId, enable);
         }
     }
 
@@ -666,7 +713,7 @@
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
-        destroyExistingTaskbar();
+        destroyAllTaskbars();
         removeTaskbarRootViewFromWindow();
         if (mUserUnlocked) {
             DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
@@ -676,38 +723,64 @@
         SettingsCache.INSTANCE.get(mContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mContext.unregisterComponentCallbacks(mComponentCallbacks);
+        mContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
         mShutdownReceiver.unregisterReceiverSafely(mContext);
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
-        return mTaskbarActivityContext;
+        return getTaskbarForDisplay(mContext.getDisplayId());
     }
 
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarManager:");
-        if (mTaskbarActivityContext == null) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar == null) {
             pw.println(prefix + "\tTaskbarActivityContext: null");
         } else {
-            mTaskbarActivityContext.dumpLogs(prefix + "\t", pw);
+            taskbar.dumpLogs(prefix + "\t", pw);
         }
     }
 
-    private void addTaskbarRootViewToWindow() {
-        if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
-            mWindowManager.addView(mTaskbarRootLayout,
-                    mTaskbarActivityContext.getWindowLayoutParams());
+    private void addTaskbarRootViewToWindow(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (enableTaskbarNoRecreate() && !mAddedWindow && taskbar != null) {
+            mWindowManager.addView(mDefaultRootLayout, taskbar.getWindowLayoutParams());
             mAddedWindow = true;
         }
     }
 
     private void removeTaskbarRootViewFromWindow() {
         if (enableTaskbarNoRecreate() && mAddedWindow) {
-            mWindowManager.removeViewImmediate(mTaskbarRootLayout);
+            mWindowManager.removeViewImmediate(mDefaultRootLayout);
             mAddedWindow = false;
         }
     }
 
+    /**
+     * Returns the {@link TaskbarActivityContext} associated with the given display ID.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @return The {@link TaskbarActivityContext} for the specified display, or
+     *         {@code null} if no taskbar is associated with that display.
+     */
+    private TaskbarActivityContext getTaskbarForDisplay(int displayId) {
+        return mTaskbars.get(displayId);
+    }
+
+    private void addTaskbarToMap(int displayId, TaskbarActivityContext newTaskbar) {
+        if (!mTaskbars.contains(displayId)) {
+            mTaskbars.put(displayId, newTaskbar);
+        }
+    }
+
+    private void removeTaskbarFromMap(int displayId) {
+        mTaskbars.delete(displayId);
+    }
+
+    private int getDefaultDisplayId() {
+        return mContext.getDisplayId();
+    }
+
     /** Temp logs for b/254119092. */
     public void debugWhyTaskbarNotDestroyed(String debugReason) {
         StringJoiner log = new StringJoiner("\n");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index c20617d..f905c5f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -208,7 +208,7 @@
 
     private void commitHotseatItemUpdates(
             ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
-        mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
+        mContainer.updateItems(hotseatItemInfos, recentTasks);
         mControllers.taskbarViewController.updateIconViewsRunningStates();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d4764c7..4881836 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,7 +16,8 @@
 
 package com.android.launcher3.taskbar;
 
-import static android.view.MotionEvent.ACTION_UP;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.ACTION_UP;
 
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
@@ -38,6 +39,7 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -78,6 +80,7 @@
     private long mLastScreenPinLongPress;
     private boolean mScreenPinned;
     private boolean mAssistantLongPressEnabled;
+    private int mLastSentBackAction = ACTION_UP;
 
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
@@ -85,6 +88,8 @@
 
         pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
         pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
+        pw.println(prefix + "\tmLastSentBackAction="
+                + KeyEvent.actionToString(mLastSentBackAction));
     }
 
     @Retention(RetentionPolicy.SOURCE)
@@ -141,6 +146,11 @@
         if (buttonType == BUTTON_SPACE) {
             return;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN) {
+            Log.i(TAG, "Button click ignored while back button is pressed");
+            // prevent interactions with other buttons while back button is pressed
+            return;
+        }
         // Provide the same haptic feedback that the system offers for virtual keys.
         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
         switch (buttonType) {
@@ -180,6 +190,13 @@
         if (buttonType == BUTTON_SPACE) {
             return false;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN
+                && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) {
+            // prevent interactions with other buttons while back button is pressed (except back
+            // and recents button for screen-unpin action).
+            Log.i(TAG, "Button long click ignored while back button is pressed");
+            return false;
+        }
 
         // Provide the same haptic feedback that the system offers for long press.
         // The haptic feedback from long pressing on the home button is handled by circle to search.
@@ -327,13 +344,27 @@
         mCallbacks.onToggleOverview();
     }
 
-    void executeBack(@Nullable KeyEvent keyEvent) {
+    void sendBackKeyEvent(int action, boolean cancelled) {
+        if (action == mLastSentBackAction) {
+            // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events
+            return;
+        }
+        long time = SystemClock.uptimeMillis();
+        KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0);
+        if (cancelled) {
+            keyEvent.cancel();
+        }
+        executeBack(keyEvent);
+    }
+
+    private void executeBack(@Nullable KeyEvent keyEvent) {
         if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
             logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
             mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
                     GestureType.BACK);
         }
         mSystemUiProxy.onBackEvent(keyEvent);
+        mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP;
     }
 
     private void onImeSwitcherPress() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 8816a6d..b609511 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -20,11 +20,14 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.Flags.enableRecentsInTaskbar;
+import static com.android.launcher3.Flags.taskbarRecentsLayoutTransition;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
+import static java.util.function.Predicate.not;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -70,7 +73,9 @@
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -108,6 +113,8 @@
     // Only non-null when device supports having a Taskbar Overflow button.
     @Nullable private TaskbarOverflowView mTaskbarOverflowView;
 
+    private int mNextViewIndex;
+
     /**
      * Whether the divider is between Hotseat icons and Recents,
      * instead of between All Apps button and Hotseat.
@@ -125,6 +132,8 @@
 
     private final int mAllAppsButtonTranslationOffset;
 
+    private final int mNumStaticViews;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -189,6 +198,8 @@
 
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
+
+        mNumStaticViews = taskbarRecentsLayoutTransition() ? addStaticViews() : 0;
     }
 
     /**
@@ -249,6 +260,24 @@
                 && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons);
     }
 
+    /**
+     * Pre-adds views that are always children of this view for LayoutTransition support.
+     * <p>
+     * Normally these views are removed and re-added when updating hotseat and recents. This
+     * approach does not behave well with LayoutTransition, so we instead need to add them
+     * initially and avoid removing them during updates.
+     */
+    private int addStaticViews() {
+        int numStaticViews = 1;
+        addView(mAllAppsButtonContainer);
+        if (mActivityContext.getDeviceProfile().isQsbInline) {
+            addView(mQsb, mIsRtl ? 1 : 0);
+            mQsb.setVisibility(View.INVISIBLE);
+            numStaticViews++;
+        }
+        return numStaticViews;
+    }
+
     @Override
     public void setVisibility(int visibility) {
         boolean changed = getVisibility() != visibility;
@@ -362,12 +391,26 @@
         view.setTag(null);
     }
 
-    /**
-     * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
-     */
-    protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
-        int nextViewIndex = 0;
-        int numViewsAnimated = 0;
+    /** Inflates/binds the hotseat items and recent tasks to the view. */
+    protected void updateItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+        // Filter out unsupported items.
+        hotseatItemInfos = Arrays.stream(hotseatItemInfos)
+                .filter(Objects::nonNull)
+                .toArray(ItemInfo[]::new);
+        // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+        recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList();
+
+        if (taskbarRecentsLayoutTransition()) {
+            updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks);
+        } else {
+            updateItemsWithoutLayoutTransition(hotseatItemInfos, recentTasks);
+        }
+    }
+
+    private void updateItemsWithoutLayoutTransition(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+
+        mNextViewIndex = 0;
         mAddedDividerForRecents = false;
 
         removeView(mAllAppsButtonContainer);
@@ -380,12 +423,101 @@
         }
         removeView(mQsb);
 
-        // Add Hotseat icons.
-        for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
-            if (hotseatItemInfo == null) {
-                continue;
-            }
+        updateHotseatItems(hotseatItemInfos);
 
+        if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
+            addView(mTaskbarDividerContainer, mNextViewIndex++);
+            mAddedDividerForRecents = true;
+        }
+
+        updateRecents(recentTasks);
+
+        addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0);
+
+        // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+        if (!mAddedDividerForRecents
+                && mTaskbarDividerContainer != null
+                && getChildCount() > 1) {
+            addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
+        }
+
+        if (mActivityContext.getDeviceProfile().isQsbInline) {
+            addView(mQsb, mIsRtl ? getChildCount() : 0);
+            // Always set QSB to invisible after re-adding.
+            mQsb.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    private void updateItemsWithLayoutTransition(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+
+        // Skip static views and potential All Apps divider, if they are on the left.
+        mNextViewIndex = mIsRtl ? 0 : mNumStaticViews;
+        if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) {
+            mNextViewIndex++;
+        }
+
+        // Update left section.
+        if (mIsRtl) {
+            updateRecents(recentTasks.reversed());
+        } else {
+            updateHotseatItems(hotseatItemInfos);
+        }
+
+        // Now at theoretical position for recent apps divider.
+        updateRecentsDivider(!recentTasks.isEmpty());
+        if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) {
+            mNextViewIndex++;
+        }
+
+        // Update right section.
+        if (mIsRtl) {
+            updateHotseatItems(hotseatItemInfos);
+        } else {
+            updateRecents(recentTasks);
+        }
+
+        // Recents divider takes priority.
+        if (!mAddedDividerForRecents) {
+            updateAllAppsDivider();
+        }
+    }
+
+    private void updateRecentsDivider(boolean hasRecents) {
+        if (hasRecents && !mAddedDividerForRecents) {
+            mAddedDividerForRecents = true;
+
+            // Remove possible All Apps divider.
+            if (getChildAt(mNumStaticViews) == mTaskbarDividerContainer) {
+                mNextViewIndex--; // All Apps divider on the left. Need to account for removing it.
+            }
+            removeView(mTaskbarDividerContainer);
+
+            addView(mTaskbarDividerContainer, mNextViewIndex);
+        } else if (!hasRecents && mAddedDividerForRecents) {
+            mAddedDividerForRecents = false;
+            removeViewAt(mNextViewIndex);
+        }
+    }
+
+    private void updateAllAppsDivider() {
+        final int allAppsDividerIndex =
+                mIsRtl ? getChildCount() - mNumStaticViews : mNumStaticViews;
+        if (getChildAt(allAppsDividerIndex) == mTaskbarDividerContainer
+                && getChildCount() == mNumStaticViews + 1) {
+            // Only static views with divider so remove divider.
+            removeView(mTaskbarDividerContainer);
+        } else if (getChildAt(allAppsDividerIndex) != mTaskbarDividerContainer
+                && getChildCount() >= mNumStaticViews + 1) {
+            // Static views with at least one app icon so add divider.
+            addView(mTaskbarDividerContainer, allAppsDividerIndex);
+        }
+    }
+
+    private void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+        int numViewsAnimated = 0;
+
+        for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
             // Replace any Hotseat views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
             boolean isCollection = false;
@@ -401,8 +533,8 @@
             }
 
             View hotseatView = null;
-            while (nextViewIndex < getChildCount()) {
-                hotseatView = getChildAt(nextViewIndex);
+            while (isNextViewInSection(ItemInfo.class)) {
+                hotseatView = getChildAt(mNextViewIndex);
 
                 // see if the view can be reused
                 if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
@@ -443,7 +575,7 @@
                 }
                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
                 hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-                addView(hotseatView, nextViewIndex, lp);
+                addView(hotseatView, mNextViewIndex, lp);
             }
 
             // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
@@ -459,14 +591,15 @@
             if (enableCursorHoverStates()) {
                 setHoverListenerForIcon(hotseatView);
             }
-            nextViewIndex++;
+            mNextViewIndex++;
         }
 
-        if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
-            addView(mTaskbarDividerContainer, nextViewIndex++);
-            mAddedDividerForRecents = true;
+        while (isNextViewInSection(ItemInfo.class)) {
+            removeAndRecycle(getChildAt(mNextViewIndex));
         }
+    }
 
+    private void updateRecents(List<GroupTask> recentTasks) {
         // At this point, the all apps button has not been added as a child view, but needs to be
         // accounted for when comparing current icon count to max number of icons.
         int nonTaskIconsToBeAdded = 1;
@@ -474,19 +607,11 @@
         boolean supportsOverflow = Flags.taskbarOverflow();
         int overflowSize = 0;
         if (supportsOverflow) {
-            int numberOfSupportedRecents = 0;
-            for (GroupTask task : recentTasks) {
-                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-                if (!task.supportsMultipleTasks()) {
-                    ++numberOfSupportedRecents;
-                }
-            }
-
-            mIdealNumIcons = nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded;
+            mIdealNumIcons = mNextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded;
             overflowSize = mIdealNumIcons - mMaxNumIcons;
 
             if (overflowSize > 0 && mTaskbarOverflowView != null) {
-                addView(mTaskbarOverflowView, nextViewIndex++);
+                addView(mTaskbarOverflowView, mNextViewIndex++);
             } else if (mTaskbarOverflowView != null) {
                 mTaskbarOverflowView.clearItems();
             }
@@ -495,9 +620,10 @@
         List<Task> overflownTasks = null;
         // An extra item needs to be added to overflow button to account for the space taken up by
         // the overflow button.
-        final int itemsToAddToOverflow = overflowSize > 0 ? overflowSize + 1 : 0;
+        final int itemsToAddToOverflow =
+                (overflowSize > 0) ? Math.min(overflowSize + 1, recentTasks.size()) : 0;
         if (overflowSize > 0) {
-            overflownTasks = new ArrayList<Task>(itemsToAddToOverflow);
+            overflownTasks = new ArrayList<>(itemsToAddToOverflow);
         }
 
         // Add Recent/Running icons.
@@ -505,10 +631,6 @@
             if (mTaskbarOverflowView != null && overflownTasks != null
                     && overflownTasks.size() < itemsToAddToOverflow) {
                 // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-                if (task.supportsMultipleTasks()) {
-                    continue;
-                }
-
                 overflownTasks.add(task.task1);
                 if (overflownTasks.size() == itemsToAddToOverflow) {
                     mTaskbarOverflowView.setItems(overflownTasks);
@@ -533,8 +655,8 @@
             }
 
             View recentIcon = null;
-            while (nextViewIndex < getChildCount()) {
-                recentIcon = getChildAt(nextViewIndex);
+            while (isNextViewInSection(GroupTask.class)) {
+                recentIcon = getChildAt(mNextViewIndex);
 
                 // see if the view can be reused
                 if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId)
@@ -548,15 +670,11 @@
             }
 
             if (recentIcon == null) {
-                if (isCollection) {
-                    // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-                    continue;
-                }
-
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
                 recentIcon = inflate(expectedLayoutResId);
                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
                 recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-                addView(recentIcon, nextViewIndex, lp);
+                addView(recentIcon, mNextViewIndex, lp);
             }
 
             if (recentIcon instanceof BubbleTextView btv) {
@@ -566,29 +684,17 @@
             if (enableCursorHoverStates()) {
                 setHoverListenerForIcon(recentIcon);
             }
-            nextViewIndex++;
+            mNextViewIndex++;
         }
 
-        // Remove remaining views
-        while (nextViewIndex < getChildCount()) {
-            removeAndRecycle(getChildAt(nextViewIndex));
+        while (isNextViewInSection(GroupTask.class)) {
+            removeAndRecycle(getChildAt(mNextViewIndex));
         }
+    }
 
-        addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0);
-
-        // If there are no recent tasks, add divider after All Apps (unless it's the only view).
-        if (!mAddedDividerForRecents
-                && mTaskbarDividerContainer != null
-                && getChildCount() > 1) {
-            addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
-        }
-
-
-        if (mActivityContext.getDeviceProfile().isQsbInline) {
-            addView(mQsb, mIsRtl ? getChildCount() : 0);
-            // Always set QSB to invisible after re-adding.
-            mQsb.setVisibility(View.INVISIBLE);
-        }
+    private boolean isNextViewInSection(Class<?> tagClass) {
+        return mNextViewIndex < getChildCount()
+                && tagClass.isInstance(getChildAt(mNextViewIndex).getTag());
     }
 
     /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
@@ -848,6 +954,13 @@
     }
 
     /**
+     * The max number of icon views the taskbar can have when taskbar overflow is enabled.
+     */
+    int getMaxNumIconViews() {
+        return mMaxNumIcons;
+    }
+
+    /**
      * Returns the all apps button in the taskbar.
      */
     public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index bb4f07a..bc5f9a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -351,6 +351,11 @@
         OneShotPreDrawListener.add(mTaskbarView, listener);
     }
 
+    @VisibleForTesting
+    int getMaxNumIconViews() {
+        return mTaskbarView.getMaxNumIconViews();
+    }
+
     public Rect getIconLayoutVisualBounds() {
         return mTaskbarView.getIconLayoutVisualBounds();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index e0814d3..987937e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -115,7 +115,6 @@
     private BubbleBarItem mSelectedBubble;
 
     private TaskbarSharedState mSharedState;
-    private ImeVisibilityChecker mImeVisibilityChecker;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
     private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
@@ -126,6 +125,8 @@
     // Cache last sent top coordinate to avoid sending duplicate updates to shell
     private int mLastSentBubbleBarTop;
 
+    private boolean mIsImeVisible = false;
+
     /**
      * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses
      * {@link BubbleBarBubble}s so that it can be used to update the views.
@@ -192,10 +193,8 @@
     /** Initializes controllers. */
     public void init(BubbleControllers bubbleControllers,
             BubbleBarLocationListener bubbleBarLocationListener,
-            ImeVisibilityChecker imeVisibilityChecker,
             TaskbarSharedState sharedState) {
         mSharedState = sharedState;
-        mImeVisibilityChecker = imeVisibilityChecker;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
@@ -234,6 +233,10 @@
 
         boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
         mBubbleStashController.setSysuiLocked(sysuiLocked);
+        mIsImeVisible = (flags & SYSUI_STATE_IME_SHOWING) != 0;
+        if (mIsImeVisible) {
+            mBubbleBarViewController.onImeVisible();
+        }
     }
 
     //
@@ -309,8 +312,7 @@
         // enabling gesture nav. also suppress animation if the bubble bar is hidden for sysui e.g.
         // the shade is open, or we're locked.
         final boolean suppressAnimation =
-                update.initialState || mBubbleBarViewController.isHiddenForSysui()
-                        || mImeVisibilityChecker.isImeVisible();
+                update.initialState || mBubbleBarViewController.isHiddenForSysui() || mIsImeVisible;
 
         if (update.initialState && mSharedState.hasSavedBubbles()) {
             // clear restored state
@@ -572,12 +574,6 @@
         mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
     }
 
-    /** Interface for checking whether the IME is visible. */
-    public interface ImeVisibilityChecker {
-        /** Whether the IME is visible. */
-        boolean isImeVisible();
-    }
-
     /** Listener of {@link BubbleBarLocation} updates. */
     public interface BubbleBarLocationListener {
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c5c2d69..833be61 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -456,7 +456,7 @@
             return;
         }
         mDragging = dragging;
-        setElevation(dragging ? mDragElevation : mBubbleElevation);
+        mController.setIsDragging(dragging);
         if (!mDragging) {
             // Relayout after dragging to ensure that the dragged bubble is positioned correctly
             requestLayout();
@@ -1575,5 +1575,8 @@
         /** Requests the controller to update bubble bar location to the given value */
         void updateBubbleBarLocation(BubbleBarLocation location,
                 @BubbleBarLocation.UpdateSource int source);
+
+        /** Notifies the controller that bubble bar is being dragged */
+        void setIsDragging(boolean dragging);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index d13e0c4..fd08078 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -85,6 +85,7 @@
     private final BubbleBarView mBarView;
     private int mIconSize;
     private int mBubbleBarPadding;
+    private final int mDragElevation;
 
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
@@ -152,6 +153,8 @@
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
         mIconSize = activity.getResources().getDimensionPixelSize(
                 R.dimen.bubblebar_icon_size);
+        mDragElevation = activity.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_drag_elevation);
         mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
     }
 
@@ -221,6 +224,11 @@
                     @BubbleBarLocation.UpdateSource int source) {
                 mBubbleBarController.updateBubbleBarLocation(location, source);
             }
+
+            @Override
+            public void setIsDragging(boolean dragging) {
+                mBubbleBarContainer.setElevation(dragging ? mDragElevation : 0);
+            }
         });
 
         mBubbleViewController = new BubbleView.Controller() {
@@ -383,6 +391,13 @@
         }
     }
 
+    /** Notifies that the IME became visible. */
+    public void onImeVisible() {
+        if (isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.interruptForIme();
+        }
+    }
+
     //
     // The below animators are exposed to BubbleStashController so it can manage the stashing
     // animation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index d993685..cb592e6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -89,7 +89,6 @@
                 );
         bubbleBarController.init(this,
                 bubbleBarLocationListeners,
-                taskbarControllers.navbarButtonsViewController::isImeVisible,
                 taskbarSharedState);
         bubbleStashedHandleViewController.ifPresent(
                 controller -> controller.init(/* bubbleControllers = */ this));
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 5184a9f..447dad1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -473,8 +473,8 @@
         ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
             .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
             .withEndAction {
-                if (animatingBubble?.expand == true) expandBubbleBar()
                 springBackAnimation.start()
+                if (animatingBubble?.expand == true) expandBubbleBar()
             }
             .start()
     }
@@ -532,6 +532,21 @@
         )
     }
 
+    /** Interrupts the animation due to the IME becoming visible. */
+    fun interruptForIme() {
+        cancelFlyout()
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
+        bubbleBarView.relativePivotY = 1f
+        // stash the bubble bar since the IME is now visible
+        bubbleStashController.onNewBubbleAnimationInterrupted(
+            /* isStashed= */ true,
+            bubbleBarView.translationY,
+        )
+    }
+
     fun expandedWhileAnimating() {
         val animatingBubble = animatingBubble ?: return
         this.animatingBubble = animatingBubble.copy(expand = true)
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 e62c0d4..22d504f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -90,7 +90,10 @@
             val hasBubbles = bubbleBarViewController.hasBubbles()
             bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
             if (!hasBubbles) {
-                // if there are no bubbles, there's nothing to show, so just return.
+                // if there are no bubbles, there's no need to update the bubble bar, just keep the
+                // isStashed state up to date so that we can process state changes when bubbles are
+                // created.
+                isStashed = launcherState == BubbleLauncherState.IN_APP
                 return
             }
             if (field == BubbleLauncherState.HOME) {
@@ -486,10 +489,9 @@
         val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview
         if (this.isStashed != isStashed) {
             this.isStashed = isStashed
+
             // notify the view controller that the stash state is about to change so that it can
             // cancel an ongoing animation if there is one.
-            // note that this has to be called before updating mIsStashed with the new value,
-            // otherwise interrupting an ongoing animation may update it again with the wrong state
             bubbleBarViewController.onStashStateChanging()
             animator?.cancel()
             animator =
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 4590efe..535ae1c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -418,7 +418,9 @@
         mIconRingPaint.setColor(RING_SHADOW_COLOR);
         mIconRingPaint.setMaskFilter(mShadowFilter);
         int count = canvas.save();
-        canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
+        if (Float.compare(1, mRingScale) != 0) {
+            canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
+        }
         canvas.drawPath(mRingPath, mIconRingPaint);
         mIconRingPaint.setColor(mPlateColor);
         mIconRingPaint.setMaskFilter(null);
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 95e7737..5648dad 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -56,9 +56,11 @@
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.TaskViewUtils.extractTargetsAndStates;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -77,6 +79,7 @@
 import android.os.IBinder;
 import android.os.SystemClock;
 import android.util.Log;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -90,6 +93,7 @@
 import android.widget.Toast;
 import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -143,6 +147,7 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.contextualeducation.GestureType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -152,10 +157,13 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
 
+import kotlin.Unit;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -165,8 +173,6 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
-import kotlin.Unit;
-
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
@@ -347,6 +353,9 @@
     // Indicates whether the divider is shown, only used when split screen is activated.
     private boolean mIsDividerShown = true;
     private boolean mStartMovingTasks;
+    // Whether the animation to home should be handed off to another handler once the gesture is
+    // committed.
+    protected boolean mHandOffAnimationToHome = false;
 
     @Nullable
     private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null;
@@ -945,6 +954,10 @@
         mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck();
         mSwipePipToHomeReleaseCheck.setCanRelease(true);
         mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck);
+        if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            mHandOffAnimationToHome =
+                    targets.extras.getBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, false);
+        }
 
         // Only initialize the device profile, if it has not been initialized before, as in some
         // configurations targets.homeContentInsets may not be correct.
@@ -1629,6 +1642,10 @@
                 }
                 windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
 
+                if (mHandOffAnimationToHome) {
+                    handOffAnimation(velocityPxPerMs);
+                }
+
                 windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
                     @Override
                     public void onAnimationSuccess(Animator animator) {
@@ -1711,6 +1728,19 @@
         }
     }
 
+    private void handOffAnimation(PointF velocityPxPerMs) {
+        if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
+                || mRecentsAnimationController == null) {
+            return;
+        }
+
+        Pair<RemoteAnimationTarget[], WindowAnimationState[]> targetsAndStates =
+                extractTargetsAndStates(mRemoteTargetHandles, velocityPxPerMs);
+        mRecentsAnimationController.handOffAnimation(
+                targetsAndStates.first, targetsAndStates.second);
+        ActiveGestureProtoLogProxy.logHandOffAnimation();
+    }
+
     private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget,
             RecentsOrientedState orientationState) {
         if (runningTaskTarget.rotationChange != 0) {
@@ -2176,8 +2206,9 @@
                     mSwipePipToHomeAnimator.getFinishTransaction(),
                     mSwipePipToHomeAnimator.getContentOverlay());
             mIsSwipingPipToHome = false;
-        } else if (mIsSwipeForSplit) {
+        } else if (mIsSwipeForSplit && !Flags.enablePip2()) {
             // Transaction to hide the task to avoid flicker for entering PiP from split-screen.
+            // Note: PiP2 handles entering differently, so skip if enable_pip2=true
             PictureInPictureSurfaceTransaction tx =
                     new PictureInPictureSurfaceTransaction.Builder()
                             .setAlpha(0f)
diff --git a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
new file mode 100644
index 0000000..bafb0b2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 android.content.Context
+import com.android.systemui.shared.system.QuickStepContract
+
+// DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
+open class DesktopFullscreenDrawParams
+@JvmOverloads
+constructor(context: Context, cornerRadiusProvider: (Context) -> Float = ::computeCornerRadius) :
+    FullscreenDrawParams(context, cornerRadiusProvider, cornerRadiusProvider) {
+    companion object {
+        // computeCornerRadius is used as cornerRadiusProvider, so
+        // QuickStepContract::getWindowCornerRadius can be mocked properly.
+        private fun computeCornerRadius(context: Context): Float =
+            QuickStepContract.getWindowCornerRadius(context)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/FullscreenDrawParams.kt
new file mode 100644
index 0000000..a5ba52a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FullscreenDrawParams.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 android.content.Context
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.TaskCornerRadius
+import com.android.systemui.shared.system.QuickStepContract
+
+/**
+ * Class for computing corner radius by interpolating between overview and fullscreen corner radius
+ * with fullscreenProgress set in [setProgress].
+ */
+open class FullscreenDrawParams
+@JvmOverloads
+constructor(
+    context: Context,
+    private val taskCornerRadiusProvider: (Context) -> Float = ::computeTaskCornerRadius,
+    private val windowCornerRadiusProvider: (Context) -> Float = ::computeWindowCornerRadius,
+) : SafeCloseable {
+    private var taskCornerRadius = 0f
+    private var windowCornerRadius = 0f
+    var currentCornerRadius = 0f
+
+    init {
+        updateCornerRadius(context)
+    }
+
+    /** Recomputes the start and end corner radius for the given Context. */
+    fun updateCornerRadius(context: Context) {
+        taskCornerRadius = taskCornerRadiusProvider(context)
+        windowCornerRadius = windowCornerRadiusProvider(context)
+    }
+
+    /** Sets the progress in range [0, 1] */
+    fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) {
+        currentCornerRadius =
+            Utilities.mapRange(fullscreenProgress, taskCornerRadius, windowCornerRadius) /
+                parentScale /
+                taskViewScale
+    }
+
+    override fun close() {}
+
+    companion object {
+        private fun computeTaskCornerRadius(context: Context): Float = TaskCornerRadius.get(context)
+
+        private fun computeWindowCornerRadius(context: Context): Float {
+            val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
+            return if (
+                activityContext?.deviceProfile?.isTaskbarPresent == true &&
+                    DisplayController.isTransientTaskbar(context)
+            ) {
+                context.resources
+                    .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
+                    .toFloat()
+            } else {
+                // The corner radius is fixed to match when Taskbar is persistent mode
+                QuickStepContract.getWindowCornerRadius(context)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 6719ab7..4bd9ffb 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -49,8 +49,8 @@
 import android.window.BackEvent;
 import android.window.BackMotionEvent;
 import android.window.BackProgressAnimator;
+import android.window.IBackAnimationHandoffHandler;
 import android.window.IOnBackInvokedCallback;
-
 import com.android.app.animation.Interpolators;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.view.AppearanceRegion;
@@ -225,6 +225,12 @@
         public void setTriggerBack(boolean triggerBack) {
             // TODO(b/261654570): track touch from the Launcher process.
         }
+
+        @Override
+        public void setHandoffHandler(IBackAnimationHandoffHandler unused) {
+            // For now, Launcher handles this internally so it doesn't need to hand off the
+            // animation.
+        }
     }
 
     private static class RemoteAnimationRunnerStub extends IRemoteAnimationRunner.Stub {
@@ -384,10 +390,11 @@
         // Move the window along the Y axis.
         float top = (screenHeight - height) * 0.5f + deltaY;
         // Move the window along the X axis.
-        float left = event.getSwipeEdge() == BackEvent.EDGE_RIGHT
-                ? progress * mWindowScaleMarginX
-                : screenWidth - progress * mWindowScaleMarginX - width;
-
+        float left = switch (event.getSwipeEdge()) {
+            case BackEvent.EDGE_RIGHT -> progress * mWindowScaleMarginX;
+            case BackEvent.EDGE_LEFT -> screenWidth - progress * mWindowScaleMarginX - width;
+            default -> (screenWidth - width) / 2;
+        };
         mCurrentRect.set(left, top, left + width, top + height);
         float cornerRadius = Utilities.mapRange(
                 progress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index dacafd4..6087dc2 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -46,7 +46,6 @@
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.FloatingView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -54,6 +53,7 @@
 import com.android.quickstep.views.FloatingWidgetView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.shared.system.InputConsumerController;
 
 import java.util.Collections;
@@ -108,7 +108,9 @@
 
         mContainer.getRootView().setForceHideBackArrow(true);
 
-        if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
+        boolean handOffAnimation = TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
+                && mHandOffAnimationToHome;
+        if (handOffAnimation || !canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
             return new LauncherHomeAnimationFactory() {
 
                 @Nullable
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 60fcff8..055aadb 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -21,9 +21,11 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManagerGlobal;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.UiThread;
 
@@ -32,6 +34,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
+import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -53,8 +56,6 @@
     private boolean mFinishRequested = false;
     // Only valid when mFinishRequested == true.
     private boolean mFinishTargetIsLauncher;
-    // Only valid when mFinishRequested == true
-    private boolean mLauncherIsVisibleAtFinish;
     private RunnableList mPendingFinishCallbacks = new RunnableList();
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -90,6 +91,16 @@
     }
 
     @UiThread
+    public void handOffAnimation(RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
+        if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            UI_HELPER_EXECUTOR.execute(() -> mController.handOffAnimation(targets, states));
+        } else {
+            Log.e(TAG, "Tried to hand off the animation, but the feature is disabled",
+                    new Exception());
+        }
+    }
+
+    @UiThread
     public void finishAnimationToHome() {
         finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
     }
@@ -119,27 +130,13 @@
     }
 
     @UiThread
-    public void finish(boolean toRecents, boolean launcherIsVisibleAtFinish,
-            Runnable onFinishComplete, boolean sendUserLeaveHint) {
-        Preconditions.assertUIThread();
-        finishController(toRecents, launcherIsVisibleAtFinish, onFinishComplete, sendUserLeaveHint,
-                false);
-    }
-
-    @UiThread
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
-        finishController(toRecents, false, callback, sendUserLeaveHint, false /* forceFinish */);
+        finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */);
     }
 
     @UiThread
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint,
             boolean forceFinish) {
-        finishController(toRecents, toRecents, callback, sendUserLeaveHint, forceFinish);
-    }
-
-    @UiThread
-    public void finishController(boolean toRecents, boolean launcherIsVisibleAtFinish,
-            Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) {
         mPendingFinishCallbacks.add(callback);
         if (!forceFinish && mFinishRequested) {
             // If finish has already been requested, then add the callback to the pending list.
@@ -151,7 +148,6 @@
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
-        mLauncherIsVisibleAtFinish = launcherIsVisibleAtFinish;
         mOnFinishedListener.accept(this);
         Runnable finishCb = () -> {
             mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
@@ -228,14 +224,6 @@
         return mFinishTargetIsLauncher;
     }
 
-    /**
-     * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
-     * the animation was finished to launcher vs an app.
-     */
-    public boolean getLauncherIsVisibleAtFinish() {
-        return mLauncherIsVisibleAtFinish;
-    }
-
     public void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "RecentsAnimationController:");
 
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 06b2972..91d0776 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -93,8 +93,7 @@
             BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop) {
         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
         for (int i = 0; i < numHandles; i++) {
-            TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
-            tvs.setIsDesktopTask(forDesktop , i);
+            TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy, forDesktop , i);
             TransformParams transformParams = new TransformParams();
             handles[i] = new RemoteTargetHandle(tvs, transformParams);
         }
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 0dbdcb7..fef4c30 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -348,7 +348,7 @@
         }
 
         /** Called when the snapshot has updated its full screen drawing parameters. */
-        public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {}
+        public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {}
 
         /** Sets visibility for the overlay associated elements. */
         public void setVisibility(int visibility) {}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 07ee479..783c87c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -47,12 +47,15 @@
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Pair;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -784,4 +787,43 @@
         animatorHandler.accept(dockFadeAnimator);
         return dockFadeAnimator;
     }
+
+    /**
+     * Creates an array of {@link RemoteAnimationTarget}s and a matching array of
+     * {@link WindowAnimationState}s from the provided handles.
+     * Important: the ordering of the two arrays is the same, so the state at each index of the
+     * second applies to the target in the same index of the first.
+     *
+     * @param handles The handles wrapping each target.
+     * @param velocityPxPerMs The current velocity of the target animations.
+     */
+    @NonNull
+    public static Pair<RemoteAnimationTarget[], WindowAnimationState[]> extractTargetsAndStates(
+            @NonNull RemoteTargetHandle[] handles, @NonNull PointF velocityPxPerMs) {
+        RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length];
+        WindowAnimationState[] animationStates = new WindowAnimationState[handles.length];
+        long timestamp = System.currentTimeMillis();
+
+        for (int i = 0; i < handles.length; i++) {
+            targets[i] = handles[i].getTransformParams().getTargetSet().apps[i];
+
+            TaskViewSimulator taskViewSimulator = handles[i].getTaskViewSimulator();
+            RectF startRect = taskViewSimulator.getCurrentRect();
+            float cornerRadius = taskViewSimulator.getCurrentCornerRadius();
+
+            WindowAnimationState state = new WindowAnimationState();
+            state.timestamp = timestamp;
+            state.bounds = new RectF(
+                    startRect.left, startRect.top, startRect.right, startRect.bottom);
+            state.topLeftRadius = cornerRadius;
+            state.topRightRadius = cornerRadius;
+            state.bottomRightRadius = cornerRadius;
+            state.bottomLeftRadius = cornerRadius;
+            state.velocityPxPerMs = velocityPxPerMs;
+
+            animationStates[i] = state;
+        }
+
+        return new Pair<>(targets, animationStates);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 7065f37..210065a 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -134,6 +134,22 @@
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId);
         mOrderedTaskList.addFirst(taskInfo);
 
+        // Workaround for b/372067617, if the home task is being brought to front, then it will
+        // occlude all other tasks, so mark them as not-visible
+        if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+            // We've moved the task to the front of the list above, so only iterate the tasks after
+            for (int i = 1; i < mOrderedTaskList.size(); i++) {
+                final TaskInfo info = mOrderedTaskList.get(i);
+                if (info.displayId != taskInfo.displayId) {
+                    // Only fall through to reset visibility for tasks on the same display as the
+                    // home task being brought forward
+                    continue;
+                }
+                info.isVisible = false;
+                info.isVisibleRequested = false;
+            }
+        }
+
         // Keep the home display's top running task in the first while adding a non-home
         // display's task to the list, to avoid showing non-home display's task upon going to
         // Recents animation.
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index dd11d48..b78e214 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -36,9 +36,6 @@
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.quickstep.task.viewmodel.TaskViewModel
-import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
@@ -186,17 +183,11 @@
                     }
                 }
                 RecentsViewData::class.java -> RecentsViewData()
-                TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
-                TaskViewData::class.java -> {
-                    val taskViewType = extras["TaskViewType"] as TaskViewType
-                    TaskViewData(taskViewType)
-                }
                 TaskContainerData::class.java -> TaskContainerData()
                 TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
                 TaskThumbnailViewModel::class.java ->
                     TaskThumbnailViewModelImpl(
                         recentsViewData = inject(),
-                        taskViewData = inject(scopeId, extras),
                         taskContainerData = inject(scopeId),
                         dispatcherProvider = inject(),
                         getThumbnailPositionUseCase = inject(),
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index 87446b0..6ccf372 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -22,9 +22,6 @@
 class RecentsViewData {
     val fullscreenProgress = MutableStateFlow(1f)
 
-    // This is typically a View concern but it is used to invalidate rendering in other Views
-    val scale = MutableStateFlow(1f)
-
     // Whether the current RecentsView state supports task overlays.
     // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
     val overlayEnabled = MutableStateFlow(false)
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index c511005..cfebb81 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -34,10 +34,6 @@
         recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
     }
 
-    fun updateScale(scale: Float) {
-        recentsViewData.scale.value = scale
-    }
-
     fun updateFullscreenProgress(fullscreenProgress: Float) {
         recentsViewData.fullscreenProgress.value = fullscreenProgress
     }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index a8c8659..0c783d3 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -17,7 +17,6 @@
 package com.android.quickstep.task.thumbnail
 
 import android.content.Context
-import android.content.res.Configuration
 import android.graphics.Color
 import android.graphics.Outline
 import android.graphics.Rect
@@ -29,7 +28,6 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.isInvisible
 import com.android.launcher3.R
-import com.android.launcher3.Utilities
 import com.android.launcher3.util.ViewPool
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
@@ -39,9 +37,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.views.FixedSizeImageView
-import com.android.systemui.shared.system.QuickStepContract
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -63,18 +59,15 @@
     private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
 
     private var uiState: TaskThumbnailUiState = Uninitialized
-    private var inheritedScale: Float = 1f
 
-    private val _measuredBounds = Rect()
-    private val measuredBounds: Rect
-        get() {
-            _measuredBounds.set(0, 0, measuredWidth, measuredHeight)
-            return _measuredBounds
+    private val bounds = Rect()
+
+    var cornerRadius: Float = 0f
+        set(value) {
+            field = value
+            invalidateOutline()
         }
 
-    private var overviewCornerRadius: Float = TaskCornerRadius.get(context)
-    private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
-
     constructor(context: Context) : super(context)
 
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@@ -114,19 +107,12 @@
                 splashIcon.alpha = splashAlpha
             }
             .launchIn(viewAttachedScope)
-        viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
-        viewModel.inheritedScale
-            .onEach { viewModelInheritedScale ->
-                inheritedScale = viewModelInheritedScale
-                invalidateOutline()
-            }
-            .launchIn(viewAttachedScope)
 
         clipToOutline = true
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(measuredBounds, getCurrentCornerRadius())
+                    outline.setRoundRect(bounds, cornerRadius)
                 }
             }
     }
@@ -157,6 +143,8 @@
         if (uiState is SnapshotSplash) {
             setImageMatrix()
         }
+        bounds.set(0, 0, w, h)
+        invalidateOutline()
     }
 
     override fun setScaleX(scaleX: Float) {
@@ -171,14 +159,6 @@
         splashIcon.scaleY = 1 / scaleY
     }
 
-    override fun onConfigurationChanged(newConfig: Configuration?) {
-        super.onConfigurationChanged(newConfig)
-
-        overviewCornerRadius = TaskCornerRadius.get(context)
-        fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context)
-        invalidateOutline()
-    }
-
     private fun resetViews() {
         liveTileView.isInvisible = true
         thumbnailView.isInvisible = true
@@ -214,13 +194,6 @@
         thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
     }
 
-    private fun getCurrentCornerRadius() =
-        Utilities.mapRange(
-            viewModel.cornerRadiusProgress.value,
-            overviewCornerRadius,
-            fullscreenCornerRadius,
-        ) / inheritedScale
-
     private companion object {
         const val TAG = "TaskThumbnailView"
     }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index f55462a..a048a1d 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -19,19 +19,9 @@
 import android.graphics.Matrix
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 
 /** ViewModel for representing TaskThumbnails */
 interface TaskThumbnailViewModel {
-    /**
-     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
-     * corner radius.
-     */
-    val cornerRadiusProgress: StateFlow<Float>
-
-    /** The accumulated View.scale value for parent Views up to and including RecentsView */
-    val inheritedScale: Flow<Float>
-
     /** Provides the level of dimming that the View should have */
     val dimProgress: Flow<Float>
 
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index 8b15a82..b6cb984 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
@@ -50,7 +49,6 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class TaskThumbnailViewModelImpl(
     recentsViewData: RecentsViewData,
-    taskViewData: TaskViewData,
     taskContainerData: TaskContainerData,
     dispatcherProvider: DispatcherProvider,
     private val tasksRepository: RecentTasksRepository,
@@ -61,15 +59,6 @@
     private val splashProgress = MutableStateFlow(flowOf(0f))
     private var taskId: Int = INVALID_TASK_ID
 
-    override val cornerRadiusProgress =
-        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
-        else MutableStateFlow(1f).asStateFlow()
-
-    override val inheritedScale =
-        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
-            recentsScale * taskScale
-        }
-
     override val dimProgress: Flow<Float> =
         combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
             taskMenuOpenProgress,
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
deleted file mode 100644
index 7a9ecf2..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.task.viewmodel
-
-import com.android.quickstep.views.TaskViewType
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskViewData(taskViewType: TaskViewType) {
-    // This is typically a View concern but it is used to invalidate rendering in other Views
-    val scale = MutableStateFlow(1f)
-
-    // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
-    /** Whether outline of TaskView is formed by outline thumbnail view(s). */
-    val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
deleted file mode 100644
index ec75d59..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.task.viewmodel
-
-import androidx.lifecycle.ViewModel
-
-class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
-    fun updateScale(scale: Float) {
-        taskViewData.scale.value = scale
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
index 083f192..334ff06 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN;
 
-import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
@@ -78,7 +77,6 @@
             this::onContextualSearchSettingChanged;
     protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
 
-    @Nullable private SettingsCache mSettingsCache;
     // Cached value whether the ContextualSearch intent filter matched any enabled components.
     private boolean mIsContextualSearchIntentAvailable;
     private boolean mIsContextualSearchSettingEnabled;
@@ -108,11 +106,10 @@
                 context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
                 Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
 
-        mSettingsCache = SettingsCache.INSTANCE.get(context);
-        mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+        SettingsCache.INSTANCE.get(context).register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
                 mContextualSearchSettingChangedListener);
         onContextualSearchSettingChanged(
-                mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
+                SettingsCache.INSTANCE.get(context).getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
         SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener);
     }
 
@@ -266,11 +263,8 @@
     public void close() {
         mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
         unregisterSearchScreenSystemAction();
-
-        if (mSettingsCache != null) {
-            mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
-                    mContextualSearchSettingChangedListener);
-        }
+        SettingsCache.INSTANCE.get(mContext).unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                mContextualSearchSettingChangedListener);
         SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener);
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index a4b8fec..706cfe4 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -50,9 +50,10 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.BaseContainerInterface;
+import com.android.quickstep.DesktopFullscreenDrawParams;
+import com.android.quickstep.FullscreenDrawParams;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
 
@@ -116,20 +117,25 @@
     private SplitBounds mSplitBounds;
     private Boolean mDrawsBelowRecents = null;
     private boolean mIsGridTask;
-    private boolean mIsDesktopTask;
+    private final boolean mIsDesktopTask;
     private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
     private int mDesktopTaskIndex = 0;
 
-    public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
+    public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy,
+            boolean isDesktop, int desktopTaskIndex) {
         mContext = context;
         mSizeStrategy = sizeStrategy;
+        mIsDesktopTask = isDesktop;
+        mDesktopTaskIndex = desktopTaskIndex;
 
         mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
                 () -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
         mOrientationState.setGestureActive(true);
-        mCurrentFullscreenParams = new FullscreenDrawParams(context);
+        mCurrentFullscreenParams = mIsDesktopTask
+                ? new DesktopFullscreenDrawParams(context)
+                : new FullscreenDrawParams(context);
         mOrientationStateId = mOrientationState.getStateId();
         Resources resources = context.getResources();
         mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
@@ -289,14 +295,6 @@
     }
 
     /**
-     * Sets whether this task is part of desktop tasks in overview.
-     */
-    public void setIsDesktopTask(boolean desktop, int index) {
-        mIsDesktopTask = desktop;
-        mDesktopTaskIndex = index;
-    }
-
-    /**
      * Apply translations on TaskRect's starting location.
      */
     public void setTaskRectTranslation(int taskRectTranslationX, int taskRectTranslationY) {
@@ -558,7 +556,7 @@
      * TaskView
      */
     public float getCurrentCornerRadius() {
-        float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius();
+        float visibleRadius = mCurrentFullscreenParams.getCurrentCornerRadius();
         mTempPoint[0] = visibleRadius;
         mTempPoint[1] = 0;
         mInversePositionMatrix.mapVectors(mTempPoint);
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
index 481acac..ef044f4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
@@ -23,14 +23,15 @@
 import android.view.View
 import android.view.ViewOutlineProvider
 import android.widget.FrameLayout
-import com.android.quickstep.views.TaskView.FullscreenDrawParams
 
 class DesktopTaskContentView
 @JvmOverloads
 constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
-    private val currentFullscreenParams = FullscreenDrawParams(context)
-    private val taskCornerRadius: Float
-        get() = currentFullscreenParams.cornerRadius
+    var cornerRadius: Float = 0f
+        set(value) {
+            field = value
+            invalidateOutline()
+        }
 
     private val bounds = Rect()
 
@@ -39,7 +40,7 @@
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(bounds, taskCornerRadius)
+                    outline.setRoundRect(bounds, cornerRadius)
                 }
             }
     }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 5e842aa..576a56e 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -20,13 +20,10 @@
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
-import android.graphics.drawable.ShapeDrawable
-import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
 import android.util.Log
 import android.view.Gravity
 import android.view.View
-import android.widget.FrameLayout
 import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -39,6 +36,8 @@
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.rects.set
 import com.android.quickstep.BaseContainerInterface
+import com.android.quickstep.DesktopFullscreenDrawParams
+import com.android.quickstep.FullscreenDrawParams
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.ViewUtils
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
@@ -47,14 +46,13 @@
 
 /** TaskView that contains all tasks that are part of the desktop. */
 class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    TaskView(context, attrs, type = TaskViewType.DESKTOP) {
-
-    private val snapshotDrawParams =
-        object : FullscreenDrawParams(context) {
-            // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
-            override fun computeTaskCornerRadius(context: Context) =
-                computeWindowCornerRadius(context)
-        }
+    TaskView(
+        context,
+        attrs,
+        type = TaskViewType.DESKTOP,
+        thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
+    ) {
+    private val contentViewFullscreenParams = FullscreenDrawParams(context)
 
     private val taskThumbnailViewDeprecatedPool =
         if (!enableRefactorTaskThumbnail()) {
@@ -80,28 +78,12 @@
 
     private val tempPointF = PointF()
     private val tempRect = Rect()
-    private lateinit var backgroundView: View
     private lateinit var iconView: TaskViewIcon
-    private lateinit var contentView: FrameLayout
+    private lateinit var contentView: DesktopTaskContentView
+    private lateinit var backgroundView: View
 
     override fun onFinishInflate() {
         super.onFinishInflate()
-        backgroundView =
-            findViewById<View>(R.id.background).apply {
-                updateLayoutParams<LayoutParams> {
-                    topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-                }
-                background =
-                    ShapeDrawable(RoundRectShape(FloatArray(8) { taskCornerRadius }, null, null))
-                        .apply {
-                            setTint(
-                                resources.getColor(
-                                    android.R.color.system_neutral2_300,
-                                    context.theme,
-                                )
-                            )
-                        }
-            }
         iconView =
             getOrInflateIconView(R.id.icon).apply {
                 setIcon(
@@ -115,10 +97,15 @@
                 setText(resources.getText(R.string.recent_task_desktop))
             }
         contentView =
-            findViewById<FrameLayout>(R.id.desktop_content).apply {
+            findViewById<DesktopTaskContentView>(R.id.desktop_content).apply {
                 updateLayoutParams<LayoutParams> {
                     topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
                 }
+                cornerRadius = contentViewFullscreenParams.currentCornerRadius
+                backgroundView = findViewById(R.id.background)
+                backgroundView.setBackgroundColor(
+                    resources.getColor(android.R.color.system_neutral2_300, context.theme)
+                )
             }
     }
 
@@ -135,6 +122,7 @@
             Log.d(TAG, sb.toString())
         }
         cancelPendingLoadTasks()
+        val backgroundViewIndex = contentView.indexOfChild(backgroundView)
         taskContainers =
             tasks.map { task ->
                 val snapshotView =
@@ -143,7 +131,7 @@
                     } else {
                         taskThumbnailViewDeprecatedPool!!.view
                     }
-                contentView.addView(snapshotView, 0)
+                contentView.addView(snapshotView, backgroundViewIndex + 1)
 
                 TaskContainer(
                     this,
@@ -157,8 +145,7 @@
                     taskOverlayFactory,
                 )
             }
-        taskContainers.forEach { it.bind() }
-        setOrientationState(orientedState)
+        onBind(orientedState)
     }
 
     override fun onRecycle() {
@@ -242,8 +229,12 @@
         }
     }
 
-    override fun needsUpdate(dataChange: Int, flag: Int) =
-        if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
+    override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) {
+        super.onTaskListVisibilityChanged(visible, changes)
+        if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
+            contentViewFullscreenParams.updateCornerRadius(context)
+        }
+    }
 
     override fun onIconLoaded(taskContainer: TaskContainer) {
         // Update contentDescription of snapshotView only, individual task icon is unused.
@@ -258,9 +249,9 @@
 
     override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) {
         if (relativeToDragLayer) {
-            container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds)
+            container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds)
         } else {
-            bounds.set(backgroundView)
+            bounds.set(contentView)
         }
     }
 
@@ -306,13 +297,12 @@
         backgroundView.alpha = 1 - fullscreenProgress
     }
 
-    override fun updateCurrentFullscreenParams() {
-        super.updateCurrentFullscreenParams()
-        updateFullscreenParams(snapshotDrawParams)
+    override fun updateFullscreenParams() {
+        super.updateFullscreenParams()
+        updateFullscreenParams(contentViewFullscreenParams)
+        contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius
     }
 
-    override fun getThumbnailFullscreenParams() = snapshotDrawParams
-
     override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
         super.addChildrenForAccessibility(outChildren)
         ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 92c1e93..0d9583d 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -104,7 +104,7 @@
                     R.id.show_windows,
                     R.id.digital_wellbeing_toast,
                     STAGE_POSITION_TOP_OR_LEFT,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 ),
                 createTaskContainer(
                     secondaryTask,
@@ -113,14 +113,12 @@
                     R.id.show_windows_right,
                     R.id.bottomRight_digital_wellbeing_toast,
                     STAGE_POSITION_BOTTOM_OR_RIGHT,
-                    taskOverlayFactory
-                )
+                    taskOverlayFactory,
+                ),
             )
-        taskContainers.forEach { it.bind() }
-
         this.splitBoundsConfig = splitBoundsConfig
         taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
-        setOrientationState(orientedState)
+        onBind(orientedState)
     }
 
     override fun setOrientationState(orientationState: RecentsOrientedState) {
@@ -131,7 +129,7 @@
                         container.deviceProfile,
                         it,
                         layoutParams.width,
-                        layoutParams.height
+                        layoutParams.height,
                     )
                 val iconViewMarginStart =
                     resources.getDimensionPixelSize(
@@ -168,7 +166,7 @@
                     container.deviceProfile,
                     splitBoundsConfig,
                     layoutParams.width,
-                    layoutParams.height
+                    layoutParams.height,
                 )
             pagedOrientationHandler.setSplitIconParams(
                 taskContainers[0].iconView.asView(),
@@ -181,7 +179,7 @@
                 isRtl,
                 container.deviceProfile,
                 splitBoundsConfig,
-                inSplitSelection
+                inSplitSelection,
             )
         } else {
             pagedOrientationHandler.setSplitIconParams(
@@ -195,7 +193,7 @@
                 isRtl,
                 container.deviceProfile,
                 splitBoundsConfig,
-                inSplitSelection
+                inSplitSelection,
             )
         }
     }
@@ -216,7 +214,7 @@
         InteractionJankMonitorWrapper.begin(
             this,
             Cuj.CUJ_SPLIT_SCREEN_ENTER,
-            "Enter form GroupedTaskView"
+            "Enter form GroupedTaskView",
         )
         launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) {
             endCallback.executeAllAndDestroy()
@@ -230,7 +228,7 @@
 
     override fun launchWithoutAnimation(
         isQuickSwitch: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) {
         launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
     }
@@ -244,7 +242,7 @@
     private fun launchTaskInternal(
         isQuickSwitch: Boolean,
         launchingExistingTaskView: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) {
         recentsView?.let {
             it.splitSelectController.launchExistingSplitPair(
@@ -254,11 +252,11 @@
                 STAGE_POSITION_TOP_OR_LEFT,
                 callback,
                 isQuickSwitch,
-                snapPosition
+                snapPosition,
             )
             Log.d(
                 TAG,
-                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView"
+                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView",
             )
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 6ab3e28..9cc42b4 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -417,9 +417,6 @@
                 public void setValue(RecentsView view, float scale) {
                     view.setScaleX(scale);
                     view.setScaleY(scale);
-                    if (enableRefactorTaskThumbnail()) {
-                        view.mRecentsViewModel.updateScale(scale);
-                    }
                     view.mLastComputedTaskStartPushOutDistance = null;
                     view.mLastComputedTaskEndPushOutDistance = null;
                     view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
@@ -692,8 +689,6 @@
     protected int mRunningTaskViewId = -1;
     private int mTaskViewIdCount;
     protected boolean mRunningTaskTileHidden;
-    @Nullable
-    private Task[] mTmpRunningTasks;
     protected int mFocusedTaskViewId = INVALID_TASK_ID;
 
     private boolean mTaskIconScaledDown = false;
@@ -1566,9 +1561,6 @@
         updateTaskStackListenerState();
         mOrientationState.setRotationWatcherEnabled(enabled);
         if (!enabled) {
-            // Reset the running task when leaving overview since it can still have a reference to
-            // its thumbnail
-            mTmpRunningTasks = null;
             mSplitBoundsConfig = null;
             mTaskOverlayFactory.clearAllActiveState();
         }
@@ -2496,13 +2488,6 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
-                if (mTmpRunningTasks != null) {
-                    for (Task t : mTmpRunningTasks) {
-                        // Skip loading if this is the task that we are animating into
-                        // TODO(b/280812109) change this equality check to use A.equals(B)
-                        tasksToUpdate.removeIf(task -> task == t);
-                    }
-                }
                 if (enableRefactorTaskThumbnail()) {
                     visibleTaskIds.addAll(
                             tasksToUpdate.stream().map((task) -> task.key.id).toList());
@@ -2510,6 +2495,7 @@
                 if (tasksToUpdate.isEmpty()) {
                     continue;
                 }
+                int visibilityChanges = 0;
                 for (Task task : tasksToUpdate) {
                     if (!mHasVisibleTaskData.get(task.key.id)) {
                         // Ignore thumbnail update if it's current running task during the gesture
@@ -2518,21 +2504,28 @@
                         if (taskView == getRunningTaskView() && isGestureActive()) {
                             changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
                         }
-                        taskView.onTaskListVisibilityChanged(true /* visible */, changes);
+                        visibilityChanges |= changes;
                     }
                     mHasVisibleTaskData.put(task.key.id, true);
                 }
+                if (visibilityChanges != 0) {
+                    taskView.onTaskListVisibilityChanged(true /* visible */, visibilityChanges);
+                }
             } else {
+                int visibilityChanges = 0;
                 for (TaskContainer container : containers) {
                     if (container == null) {
                         continue;
                     }
 
                     if (mHasVisibleTaskData.get(container.getTask().key.id)) {
-                        taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
+                        visibilityChanges = dataChanges;
                     }
                     mHasVisibleTaskData.delete(container.getTask().key.id);
                 }
+                if (visibilityChanges != 0) {
+                    taskView.onTaskListVisibilityChanged(false /* visible */, visibilityChanges);
+                }
             }
         }
         if (enableRefactorTaskThumbnail()) {
@@ -2965,23 +2958,18 @@
             final TaskView taskView;
             if (needDesktopTask) {
                 taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
-                mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
-                ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
+                ((DesktopTaskView) taskView).bind(Arrays.asList(runningTasks),
                         mOrientationState, mTaskOverlayFactory);
             } else if (needGroupTaskView) {
                 taskView = getTaskViewFromPool(TaskViewType.GROUPED);
-                mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
                 // the actual app running we won't need to show the thumbnail until all the tasks
                 // load later anyways
-                ((GroupedTaskView) taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
+                ((GroupedTaskView) taskView).bind(runningTasks[0], runningTasks[1],
                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
             } else {
                 taskView = getTaskViewFromPool(TaskViewType.SINGLE);
-                // The temporary running task is only used for the duration between the start of the
-                // gesture and the task list is loaded and applied
-                mTmpRunningTasks = new Task[]{runningTasks[0]};
-                taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory);
+                taskView.bind(runningTasks[0], mOrientationState, mTaskOverlayFactory);
             }
             addView(taskView, 0);
             runningTaskViewId = taskView.getTaskViewId();
@@ -5034,7 +5022,7 @@
 
     private void updateTaskViewsSnapshotRadius() {
         for (TaskView taskView : getTaskViews()) {
-            taskView.updateSnapshotRadius();
+            taskView.updateFullscreenParams();
         }
     }
 
@@ -5167,7 +5155,7 @@
                 if (!enableRefactorTaskThumbnail()) {
                     taskContainer.getThumbnailViewDeprecated().refreshSplashView();
                 }
-                mSplitHiddenTaskView.updateSnapshotRadius();
+                mSplitHiddenTaskView.updateFullscreenParams();
             });
         } else if (isInitiatingSplitFromTaskView) {
             if (Flags.enableHoverOfChildElementsInTaskview()) {
@@ -5879,22 +5867,15 @@
      * Finish recents animation.
      */
     public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) {
-        finishRecentsAnimation(toRecents, false, true /* shouldPip */, onFinishComplete);
+        finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
     }
 
     /**
-     * Finish recents animation.
-     */
-    public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
-            @Nullable Runnable onFinishComplete) {
-        finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete);
-    }
-    /**
      * NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
      * {@link #mRecentsAnimationController#setWillFinishToHome}.
      */
     public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
-            boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) {
+            @Nullable Runnable onFinishComplete) {
         Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
                 + mRecentsAnimationController);
         // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
@@ -5926,7 +5907,7 @@
                         tx, null /* overlay */);
             }
         }
-        mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
+        mRecentsAnimationController.finish(toRecents, () -> {
             if (onFinishComplete != null) {
                 onFinishComplete.run();
             }
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 5dbc2ef..9f2bb9a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -50,9 +50,9 @@
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags;
 import com.android.launcher3.util.ViewPool;
+import com.android.quickstep.FullscreenDrawParams;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
@@ -107,7 +107,7 @@
     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
     private final Rect mPreviewRect = new Rect();
     private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
-    private TaskView.FullscreenDrawParams mFullscreenParams;
+    private FullscreenDrawParams mFullscreenParams;
     private ImageView mSplashView;
     private Drawable mSplashViewDrawable;
     private TaskView mTaskView;
@@ -279,7 +279,7 @@
         canvas.save();
         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
         drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(),
-                mFullscreenParams.getCurrentDrawnCornerRadius());
+                mFullscreenParams.getCurrentCornerRadius());
         canvas.restore();
     }
 
@@ -287,7 +287,7 @@
         return mPreviewPositionHelper;
     }
 
-    public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
+    public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {
         mFullscreenParams = fullscreenParams;
         invalidate();
     }
@@ -473,7 +473,7 @@
             mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix());
             mPaint.setShader(mBitmapShader);
         }
-        mTaskView.updateCurrentFullscreenParams();
+        mTaskView.updateFullscreenParams();
         invalidate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b1cb407..7e489ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -59,13 +59,11 @@
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.CancellableTask
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.launcher3.util.RunnableList
-import com.android.launcher3.util.SafeCloseable
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
@@ -74,16 +72,13 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.rects.set
-import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.FullscreenDrawParams
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RemoteAnimationTargets
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.BorderAnimator
@@ -95,7 +90,6 @@
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.ActivityManagerWrapper
-import com.android.systemui.shared.system.QuickStepContract
 
 /** A task in the Recents view. */
 open class TaskView
@@ -108,6 +102,7 @@
     focusBorderAnimator: BorderAnimator? = null,
     hoverBorderAnimator: BorderAnimator? = null,
     private val type: TaskViewType = TaskViewType.SINGLE,
+    protected val thumbnailFullscreenParams: FullscreenDrawParams = FullscreenDrawParams(context),
 ) : FrameLayout(context, attrs), ViewPool.Reusable {
     /**
      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -117,8 +112,6 @@
     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
     annotation class TaskDataChanges
 
-    private lateinit var taskViewModel: TaskViewModel
-
     val taskIds: IntArray
         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
         get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -142,9 +135,6 @@
             this == recentsView?.focusedTaskView ||
                 (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP)
 
-    val taskCornerRadius: Float
-        get() = currentFullscreenParams.cornerRadius
-
     val recentsView: RecentsView<*, *>?
         get() = parent as? RecentsView<*, *>
 
@@ -157,15 +147,9 @@
         get() = taskContainers[0].task
 
     @get:Deprecated("Use [taskContainers] instead.")
-    val firstSnapshotView: View
-        /** Returns the first snapshotView of the TaskView. */
-        get() = taskContainers[0].snapshotView
-
-    @get:Deprecated("Use [taskContainers] instead.")
     val firstItemInfo: ItemInfo
         get() = taskContainers[0].itemInfo
 
-    private val currentFullscreenParams = FullscreenDrawParams(context)
     protected val container: RecentsViewContainer =
         RecentsViewContainer.containerFromContext(context)
     protected val lastTouchDownPosition = PointF()
@@ -489,17 +473,13 @@
     init {
         setOnClickListener { _ -> onClick() }
 
-        if (enableRefactorTaskThumbnail()) {
-            taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
-        }
-
         val cursorHoverStatesEnabled = enableCursorHoverStates()
         setWillNotDraw(!cursorHoverStatesEnabled)
         context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use {
             this.focusBorderAnimator =
                 focusBorderAnimator
                     ?: createSimpleBorderAnimator(
-                        currentFullscreenParams.cornerRadius.toInt(),
+                        TaskCornerRadius.get(context).toInt(),
                         context.resources.getDimensionPixelSize(
                             R.dimen.keyboard_quick_switch_border_width
                         ),
@@ -514,7 +494,7 @@
                 hoverBorderAnimator
                     ?: if (cursorHoverStatesEnabled)
                         createSimpleBorderAnimator(
-                            currentFullscreenParams.cornerRadius.toInt(),
+                            TaskCornerRadius.get(context).toInt(),
                             context.resources.getDimensionPixelSize(
                                 R.dimen.task_hover_border_width
                             ),
@@ -716,7 +696,16 @@
                     taskOverlayFactory,
                 )
             )
-        taskContainers.forEach { it.bind() }
+        onBind(orientedState)
+    }
+
+    open fun onBind(orientedState: RecentsOrientedState) {
+        taskContainers.forEach {
+            it.bind()
+            if (enableRefactorTaskThumbnail()) {
+                it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+            }
+        }
         setOrientationState(orientedState)
     }
 
@@ -935,7 +924,7 @@
             }
         }
         if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
-            currentFullscreenParams.updateCornerRadius(context)
+            thumbnailFullscreenParams.updateCornerRadius(context)
         }
     }
 
@@ -1514,10 +1503,7 @@
         val scale = persistentScale * dismissScale
         scaleX = scale
         scaleY = scale
-        if (enableRefactorTaskThumbnail()) {
-            taskViewModel.updateScale(scale)
-        }
-        updateSnapshotRadius()
+        updateFullscreenParams()
     }
 
     protected open fun applyThumbnailSplashAlpha() {
@@ -1560,30 +1546,25 @@
         }
         focusTransitionFullscreen.value =
             FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
-        updateSnapshotRadius()
+        updateFullscreenParams()
     }
 
-    protected open fun updateSnapshotRadius() {
-        updateCurrentFullscreenParams()
+    protected open fun updateFullscreenParams() {
+        updateFullscreenParams(thumbnailFullscreenParams)
         taskContainers.forEach {
-            if (!enableRefactorTaskThumbnail()) {
-                it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+            if (enableRefactorTaskThumbnail()) {
+                it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+            } else {
+                it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams)
             }
-            it.overlay.setFullscreenParams(getThumbnailFullscreenParams())
+            it.overlay.setFullscreenParams(thumbnailFullscreenParams)
         }
     }
 
-    protected open fun updateCurrentFullscreenParams() {
-        updateFullscreenParams(currentFullscreenParams)
-    }
-
     protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) {
         recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) }
     }
 
-    protected open fun getThumbnailFullscreenParams(): FullscreenDrawParams =
-        currentFullscreenParams
-
     private fun onModalnessUpdated(modalness: Float) {
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
@@ -1630,56 +1611,6 @@
     private fun getNonGridTrans(endTranslation: Float) =
         endTranslation - getGridTrans(endTranslation)
 
-    /** We update and subsequently draw these in [fullscreenProgress]. */
-    open class FullscreenDrawParams(context: Context) : SafeCloseable {
-        var cornerRadius = 0f
-        private var windowCornerRadius = 0f
-        var currentDrawnCornerRadius = 0f
-
-        init {
-            updateCornerRadius(context)
-        }
-
-        /** Recomputes the start and end corner radius for the given Context. */
-        fun updateCornerRadius(context: Context) {
-            cornerRadius = computeTaskCornerRadius(context)
-            windowCornerRadius = computeWindowCornerRadius(context)
-        }
-
-        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-        open fun computeTaskCornerRadius(context: Context): Float {
-            return TaskCornerRadius.get(context)
-        }
-
-        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-        open fun computeWindowCornerRadius(context: Context): Float {
-            val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
-
-            // The corner radius is fixed to match when Taskbar is persistent mode
-            return if (
-                activityContext != null &&
-                    activityContext.deviceProfile?.isTaskbarPresent == true &&
-                    DisplayController.isTransientTaskbar(context)
-            ) {
-                context.resources
-                    .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
-                    .toFloat()
-            } else {
-                QuickStepContract.getWindowCornerRadius(context)
-            }
-        }
-
-        /** Sets the progress in range [0, 1] */
-        fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) {
-            currentDrawnCornerRadius =
-                Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) /
-                    parentScale /
-                    taskViewScale
-        }
-
-        override fun close() {}
-    }
-
     private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
         return thumbnailBounds.contains(x.toInt(), y.toInt())
     }
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
index f43a125..0091036 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -96,6 +96,12 @@
                 + "force finish recents animation complete; clearing state callback.");
     }
 
+    public static void logHandOffAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation");
+    }
+
     public static void logFinishRecentsAnimationOnTasksAppeared() {
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
         if (!enableActiveGestureProtoLog()) return;
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
index ff5d8bd..47d2bfc 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -22,8 +22,6 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
-    override val cornerRadiusProgress = MutableStateFlow(0f)
-    override val inheritedScale = MutableStateFlow(1f)
     override val dimProgress = MutableStateFlow(0f)
     override val splashAlpha = MutableStateFlow(0f)
     override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 75769e9..49fe614 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -66,11 +66,13 @@
         val di = RecentsDependencies.initialize(context)
         val taskThumbnailView =
             LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
+                as TaskThumbnailView
+        taskThumbnailView.cornerRadius = CORNER_RADIUS
         val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
         di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
         di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
 
-        return taskThumbnailView as TaskThumbnailView
+        return taskThumbnailView
     }
 
     companion object {
@@ -82,5 +84,7 @@
                 isDarkTheme = false,
                 isLandscape = false,
             )
+
+        const val CORNER_RADIUS = 56f
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 4b04dba..c682990 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -15,9 +15,11 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,10 @@
 import static org.mockito.Mockito.when;
 
 import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -43,8 +49,10 @@
 import com.android.systemui.contextualeducation.GestureType;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -76,6 +84,9 @@
     @Mock
     View mockView;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private int mHomePressCount;
     private int mOverviewToggleCount;
     private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() {
@@ -333,4 +344,46 @@
         verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
         verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
     }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackInvoked() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, false);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackCancelled() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, true);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testButtonsDisabledWhileBackPressed() {
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
+        mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
+        mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView);
+        mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        assertThat(mHomePressCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked();
+        assertThat(mOverviewToggleCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+    }
+
+    private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) {
+        assertEquals(isCancelled, keyEvent.isCanceled());
+        assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction());
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
new file mode 100644
index 0000000..cc8582c
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -0,0 +1,313 @@
+/*
+ * 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.launcher3.taskbar
+
+import android.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.TestUtil.getOnUiThread
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.DesktopTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.android.wm.shell.desktopmode.IDesktopTaskListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+@EnableFlags(
+    FLAG_TASKBAR_OVERFLOW,
+    FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS,
+    FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+    FLAG_ENABLE_BUBBLE_BAR,
+)
+class TaskbarOverflowTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+
+    @get:Rule(order = 1)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun setDesktopTaskListener(listener: IDesktopTaskListener?) {
+                        desktopTaskListener = listener
+                    }
+                }
+            )
+        }
+
+    @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(context)
+
+    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
+
+    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var taskbarViewController: TaskbarViewController
+    @InjectController lateinit var recentAppsController: TaskbarRecentAppsController
+    @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
+    @InjectController lateinit var bubbleStashController: BubbleStashController
+
+    private var desktopTaskListener: IDesktopTaskListener? = null
+
+    @Before
+    fun ensureRunningAppsShowing() {
+        runOnMainSync {
+            if (!recentAppsController.canShowRunningApps) {
+                recentAppsController.onDestroy()
+                recentAppsController.canShowRunningApps = true
+                recentAppsController.init(taskbarUnitTestRule.activityContext.controllers)
+            }
+            recentsModel.resolvePendingTaskRequests()
+        }
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testTaskbarWithMaxNumIcons_pinned() {
+        addRunningAppsAndVerifyOverflowState(0)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testTaskbarWithMaxNumIcons_transient() {
+        addRunningAppsAndVerifyOverflowState(0)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOverflownTaskbar_pinned() {
+        addRunningAppsAndVerifyOverflowState(5)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOverflownTaskbar_transient() {
+        addRunningAppsAndVerifyOverflowState(5)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_transient() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin)
+            .isAtLeast(
+                navButtonEndSpacing +
+                    bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+            )
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+        runOnMainSync {
+            bubbleStashController.stashBubbleBarImmediate()
+            bubbleBarViewController.setHiddenForBubbles(false)
+        }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin)
+            .isAtLeast(
+                navButtonEndSpacing +
+                    bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+            )
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testStashingBubbleBarMaintainsMaxNumIcons_transient() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+
+        runOnMainSync { bubbleStashController.stashBubbleBarImmediate() }
+        assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testHidingBubbleBarIncreasesMaxNumIcons_transient() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    private fun createDesktopTask(tasksToAdd: Int) {
+        val tasks =
+            (0..<tasksToAdd).map {
+                Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 2000))
+            }
+        recentsModel.updateRecentTasks(listOf(DesktopTask(tasks)))
+        desktopTaskListener?.onTasksVisibilityChanged(
+            context.virtualDisplay.display.displayId,
+            tasksToAdd,
+        )
+        runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+    }
+
+    private val navButtonEndSpacing: Int
+        get() {
+            return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize(
+                taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing
+            )
+        }
+
+    private val taskbarOverflowIconIndex: Int
+        get() {
+            return getOnUiThread {
+                taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView }
+            }
+        }
+
+    private val maxNumberOfTaskbarIcons: Int
+        get() = getOnUiThread { taskbarViewController.maxNumIconViews }
+
+    private val currentNumberOfTaskbarIcons: Int
+        get() = getOnUiThread { taskbarViewController.iconViews.size }
+
+    private val taskbarIconsCentered: Boolean
+        get() {
+            return getOnUiThread {
+                val iconLayoutBounds = taskbarViewController.iconLayoutBounds
+                val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx
+                iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2
+            }
+        }
+
+    private val taskbarEndMargin: Int
+        get() {
+            return getOnUiThread {
+                taskbarUnitTestRule.activityContext.deviceProfile.widthPx -
+                    taskbarViewController.iconLayoutBounds.right
+            }
+        }
+
+    /**
+     * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies
+     * * max number of icons in the taskbar remains unchanged
+     * * number of icons in the taskbar is at most max number of icons
+     * * whether the taskbar overflow icon is shown, and its position in taskbar.
+     *
+     * Returns max number of icons.
+     */
+    private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int {
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(0)
+        // Assume there are at least all apps and divider icon, as they would appear once running
+        // apps are added, even if not present initially.
+        val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+        assertThat(initialIconCount).isLessThan(maxNumIconViews)
+
+        createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize)
+
+        assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex)
+            .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1)
+        return maxNumIconViews
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt
new file mode 100644
index 0000000..0bb404b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.launcher3.taskbar
+
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents
+import com.android.launcher3.taskbar.rules.TaskbarDeviceEmulationRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) {
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0},{1}")
+        fun getParams(): List<Array<Any>> {
+            val devices =
+                if (isRunningInRobolectric) {
+                    listOf("pixelFoldable2023", "pixelTablet2023")
+                } else {
+                    listOf("onDevice") // Unused.
+                }
+            val flags = allCombinationsOf(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION)
+            return devices.flatMap { d -> flags.map { f -> arrayOf(d, f) } } // Cartesian product.
+        }
+    }
+
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule(flags)
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val deviceEmulationRule = TaskbarDeviceEmulationRule(context, deviceName)
+    @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    private lateinit var taskbarView: TaskbarView
+
+    @Before
+    fun obtainView() {
+        taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+    }
+
+    @Test
+    fun testUpdateItems_noItems_hasOnlyAllApps() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_hotseatItems_hasDividerBetweenAllAppsAndHotseat() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtlWithHotseatItems_hasDividerBetweenHotseatAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_withNullHotseatItem_filtersNullItem() {
+        runOnMainSync {
+            taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtlWithNullHotseatItem_filtersNullItem() {
+        runOnMainSync {
+            taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_recentsItems_hasDividerBetweenAllAppsAndRecents() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * 4)
+    }
+
+    @Test
+    fun testUpdateItems_hotseatItemsAndRecents_hasDividerBetweenHotseatAndRecents() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 3, DIVIDER, *RECENT * 2)
+    }
+
+    @Test
+    fun testUpdateItems_addHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 2, DIVIDER, RECENT)
+    }
+
+    @Test
+    fun testUpdateItems_removeHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT)
+    }
+
+    @Test
+    fun testUpdateItems_addRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, *RECENT * 2)
+    }
+
+    @Test
+    fun testUpdateItems_removeRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
new file mode 100644
index 0000000..a6bdbb0
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.launcher3.taskbar
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Process
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+
+/** Common utilities for testing [TaskbarView]. */
+object TaskbarViewTestUtil {
+
+    /** Begins an assertion about a [TaskbarView]. */
+    fun assertThat(view: TaskbarView): TaskbarViewSubject {
+        return assertAbout(::TaskbarViewSubject).that(view)
+    }
+
+    /** Creates an array of fake hotseat items. */
+    fun createHotseatItems(size: Int): Array<ItemInfo> {
+        return Array(size) {
+            WorkspaceItemInfo(
+                    AppInfo(TEST_COMPONENT, "Test App $it", Process.myUserHandle(), Intent())
+                )
+                .apply { id = it }
+        }
+    }
+
+    /** Creates a list of fake recent tasks. */
+    fun createRecents(size: Int): List<GroupTask> {
+        return List(size) {
+            GroupTask(
+                Task().apply {
+                    key =
+                        TaskKey(
+                            it,
+                            5,
+                            TEST_INTENT,
+                            TEST_COMPONENT,
+                            Process.myUserHandle().identifier,
+                            System.currentTimeMillis(),
+                        )
+                }
+            )
+        }
+    }
+}
+
+/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */
+class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView) :
+    Subject(failureMetadata, view) {
+
+    /** Verifies that the types of icons match [expectedTypes] in order. */
+    fun hasIconTypes(vararg expectedTypes: TaskbarIconType) {
+        val actualTypes =
+            view.iconViews.map {
+                when (it) {
+                    view.allAppsButtonContainer -> ALL_APPS
+                    view.taskbarDividerViewContainer -> DIVIDER
+                    view.taskbarOverflowView -> OVERFLOW
+                    else ->
+                        when (it.tag) {
+                            is ItemInfo -> HOTSEAT
+                            is GroupTask -> RECENT
+                            else -> throw IllegalStateException("Unknown type for $it")
+                        }
+                }
+            }
+        assertThat(actualTypes).containsExactly(*expectedTypes).inOrder()
+    }
+
+    /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */
+    fun hasRecentsOrder(startIndex: Int, expectedIds: List<Int>) {
+        val actualIds =
+            view.iconViews.slice(startIndex..<expectedIds.size).map {
+                assertThat(it.tag).isInstanceOf(GroupTask::class.java)
+                (it.tag as? GroupTask)?.task1?.key?.id
+            }
+        assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder()
+    }
+}
+
+/** Types of icons in the [TaskbarView]. */
+enum class TaskbarIconType {
+    ALL_APPS,
+    DIVIDER,
+    HOTSEAT,
+    RECENT,
+    OVERFLOW;
+
+    operator fun times(size: Int) = Array(size) { this }
+}
+
+private const val TEST_PACKAGE = "com.android.launcher3.taskbar"
+private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "Activity")
+private val TEST_INTENT = Intent().apply { `package` = TEST_PACKAGE }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
new file mode 100644
index 0000000..15ded8d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.launcher3.taskbar
+
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@EnableFlags(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION)
+class TaskbarViewWithLayoutTransitionTest {
+
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    private lateinit var taskbarView: TaskbarView
+
+    @Before
+    fun obtainView() {
+        taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_hotseatItems_hasDividerBetweenHotseatAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_recentsItems_hasDividerBetweenRecentsAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 4, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_recentsItems_recentsAreReversed() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(3, 2, 1, 0))
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_hotseatItemsAndRecents_hasDividerBetweenRecentsAndHotseat() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, *HOTSEAT * 3, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, *HOTSEAT * 2, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_removeHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+        }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_removeRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+}
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 582ea54..e12876f 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
@@ -48,12 +48,15 @@
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
 import com.android.wm.shell.shared.bubbles.BubbleInfo
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
 import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -764,10 +767,12 @@
         whenever(bubbleStashController.bubbleBarTranslationY)
             .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
-        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
-
+        val semaphore = Semaphore(0)
         var notifiedExpanded = false
-        val onExpanded = Runnable { notifiedExpanded = true }
+        val onExpanded = Runnable {
+            notifiedExpanded = true
+            semaphore.release()
+        }
         val animator =
             BubbleBarViewAnimator(
                 bubbleBarView,
@@ -792,7 +797,12 @@
 
         // the lift animation is complete; the spring back animation should start now
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
-        barAnimator.assertIsRunning()
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        // we should be expanded now
+        assertThat(bubbleBarView.isExpanded).isTrue()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         // verify there is no hide animation
@@ -800,7 +810,6 @@
 
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
-        assertThat(bubbleBarView.isExpanded).isTrue()
         verify(bubbleStashController).showBubbleBarImmediate()
         assertThat(notifiedExpanded).isTrue()
     }
@@ -1266,6 +1275,50 @@
         verify(bubbleStashController).stashBubbleBarImmediate()
     }
 
+    @Test
+    fun interruptForIme() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        handleAnimator.assertIsRunning()
+        assertThat(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForIme() }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(eq(true), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        handleAnimator.assertIsNotRunning()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
new file mode 100644
index 0000000..ed1443d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.launcher3.taskbar.rules
+
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.GroupTask
+import java.util.function.Consumer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class MockedRecentsModelTestRule(private val context: TaskbarWindowSandboxContext) : TestRule {
+
+    private val mockIconCache: TaskIconCache = mock()
+
+    private val mockRecentsModel: RecentsModel = mock {
+        on { iconCache } doReturn mockIconCache
+
+        on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
+
+        on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
+            {
+                recentTasksChangedListener = it.getArgument<RecentTasksChangedListener>(0)
+            }
+
+        on { getTasks(anyOrNull(), anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { getTasks(anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) }
+    }
+
+    private var recentTasks: List<GroupTask> = emptyList()
+    private var taskListId = 0
+    private var recentTasksChangedListener: RecentTasksChangedListener? = null
+    private var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
+
+    override fun apply(base: Statement?, description: Description?): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                context.putObject(RecentsModel.INSTANCE, mockRecentsModel)
+                base?.evaluate()
+            }
+        }
+    }
+
+    // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so
+    // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list.
+    fun updateRecentTasks(tasks: List<GroupTask>) {
+        ++taskListId
+        recentTasks = tasks
+        recentTasksChangedListener?.onRecentTasksChanged()
+    }
+
+    fun resolvePendingTaskRequests() {
+        val requests = mutableListOf<(List<GroupTask>) -> Unit>()
+        requests.addAll(taskRequests)
+        taskRequests.clear()
+
+        requests.forEach { it(recentTasks) }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 6b95f8d..970bdec 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -17,6 +17,7 @@
 package com.android.quickstep;
 
 import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
 
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
@@ -28,6 +29,7 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -40,6 +42,9 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.ViewTreeObserver;
@@ -58,6 +63,7 @@
 import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.InputConsumerController;
 
 import org.junit.Before;
@@ -103,14 +109,8 @@
             /* 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());
 
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected TaskAnimationManager mTaskAnimationManager;
     protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
 
@@ -127,6 +127,22 @@
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUpAnimationTargets() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true);
+        mRecentsAnimationTargets = new RecentsAnimationTargets(
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                /* homeContentInsets= */ new Rect(),
+                /* minimizedHomeBounds= */ null,
+                extras);
+    }
+
     @Before
     public void setUpRunningTaskInfo() {
         mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
@@ -237,6 +253,30 @@
         });
     }
 
+    @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+            Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+    @Test
+    public void testHomeGesture_handsOffAnimation() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController).handOffAnimation(any(), any());
+            verifyRecentsAnimationFinishedAndCallCallback();
+        });
+    }
+
+    @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+            Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+    @Test
+    public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController, never()).handOffAnimation(any(), any());
+            verifyRecentsAnimationFinishedAndCallCallback();
+        });
+    }
+
     @Test
     public void testHomeGesture_invalidatesHandlerAfterParallelAnim() {
         ValueAnimator parallelAnim = new ValueAnimator();
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt
new file mode 100644
index 0000000..e62455f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 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 android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/** Test for [DesktopFullscreenDrawParams] class. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopFullscreenDrawParamsTest() {
+    private val params =
+        DesktopFullscreenDrawParams(mock<Context>(), cornerRadiusProvider = { CORNER_RADIUS })
+
+    @Test
+    fun setMiddleProgress_invariantCornerRadiusForDesktop() {
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+
+        params.setProgress(fullscreenProgress = 0.67f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+    }
+
+    companion object {
+        const val CORNER_RADIUS = 32f
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
index 5d62a4c..99b81e0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
@@ -20,21 +20,17 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.FakeInvariantDeviceProfileTest
 import com.android.quickstep.util.TaskCornerRadius
-import com.android.quickstep.views.TaskView.FullscreenDrawParams
 import com.android.systemui.shared.system.QuickStepContract
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
+import org.mockito.kotlin.mock
 
-/** Test for FullscreenDrawParams class. */
+/** Test for [FullscreenDrawParams] class. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() {
-
     private lateinit var params: FullscreenDrawParams
 
     @Before
@@ -46,115 +42,108 @@
     fun setStartProgress_correctCornerRadiusForTablet() {
         initializeVarsForTablet()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1.0f, taskViewScale = 1.0f)
 
         val expectedRadius = TaskCornerRadius.get(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForTablet() {
         initializeVarsForTablet()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = QuickStepContract.getWindowCornerRadius(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setStartProgress_correctCornerRadiusForPhone() {
         initializeVarsForPhone()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = TaskCornerRadius.get(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForPhone() {
         initializeVarsForPhone()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = QuickStepContract.getWindowCornerRadius(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setStartProgress_correctCornerRadiusForMultiDisplay() {
-        val display1Context = context
-        val display2Context = mock(Context::class.java)
-        val spyParams = spy(params)
+        val display1Context = mock<Context>()
+        val display2Context = mock<Context>()
+        val display1TaskRadius = TASK_CORNER_RADIUS + 1
+        val display2TaskRadius = TASK_CORNER_RADIUS + 2
 
-        val display1TaskRadius = TaskCornerRadius.get(display1Context)
-        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
-        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
-        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
-        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
-        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+        val params =
+            FullscreenDrawParams(
+                context,
+                taskCornerRadiusProvider = { context ->
+                    when (context) {
+                        display1Context -> display1TaskRadius
+                        display2Context -> display2TaskRadius
+                        else -> TASK_CORNER_RADIUS
+                    }
+                },
+                windowCornerRadiusProvider = { 0f },
+            )
 
-        spyParams.updateCornerRadius(display1Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1TaskRadius)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(TASK_CORNER_RADIUS)
 
-        spyParams.updateCornerRadius(display2Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2TaskRadius)
+        params.updateCornerRadius(display1Context)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display1TaskRadius)
+
+        params.updateCornerRadius(display2Context)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display2TaskRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForMultiDisplay() {
-        val display1Context = context
-        val display2Context = mock(Context::class.java)
-        val spyParams = spy(params)
+        val display1Context = mock<Context>()
+        val display2Context = mock<Context>()
+        val display1WindowRadius = WINDOW_CORNER_RADIUS + 1
+        val display2WindowRadius = WINDOW_CORNER_RADIUS + 2
 
-        val display1TaskRadius = TaskCornerRadius.get(display1Context)
-        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
-        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
-        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
-        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
-        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+        val params =
+            FullscreenDrawParams(
+                context,
+                taskCornerRadiusProvider = { 0f },
+                windowCornerRadiusProvider = { context ->
+                    when (context) {
+                        display1Context -> display1WindowRadius
+                        display2Context -> display2WindowRadius
+                        else -> WINDOW_CORNER_RADIUS
+                    }
+                },
+            )
 
-        spyParams.updateCornerRadius(display1Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1WindowRadius)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(WINDOW_CORNER_RADIUS)
 
-        spyParams.updateCornerRadius(display2Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f,
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2WindowRadius)
+        params.updateCornerRadius(display1Context)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display1WindowRadius)
+
+        params.updateCornerRadius(display2Context)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display2WindowRadius)
+    }
+
+    companion object {
+        const val TASK_CORNER_RADIUS = 56f
+        const val WINDOW_CORNER_RADIUS = 32f
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index e3a6adf..a777bd4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -37,8 +37,6 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
-import com.android.quickstep.task.viewmodel.TaskViewData
-import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
@@ -57,9 +55,7 @@
     private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
-    private var taskViewType = TaskViewType.SINGLE
     private val recentsViewData = RecentsViewData()
-    private val taskViewData by lazy { TaskViewData(taskViewType) }
     private val taskContainerData = TaskContainerData()
     private val dispatcherProvider = TestDispatcherProvider(dispatcher)
     private val tasksRepository = FakeTasksRepository()
@@ -69,7 +65,6 @@
     private val systemUnderTest by lazy {
         TaskThumbnailViewModelImpl(
             recentsViewData,
-            taskViewData,
             taskContainerData,
             dispatcherProvider,
             tasksRepository,
@@ -124,40 +119,6 @@
         }
 
     @Test
-    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() =
-        testScope.runTest {
-            recentsViewData.fullscreenProgress.value = 0.5f
-
-            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
-
-            recentsViewData.fullscreenProgress.value = 0.6f
-
-            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
-        }
-
-    @Test
-    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() =
-        testScope.runTest {
-            taskViewType = TaskViewType.DESKTOP
-            recentsViewData.fullscreenProgress.value = 0.5f
-
-            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
-
-            recentsViewData.fullscreenProgress.value = 0.6f
-
-            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
-        }
-
-    @Test
-    fun setAncestorScales_thenScaleIsCalculated() =
-        testScope.runTest {
-            recentsViewData.scale.value = 0.5f
-            taskViewData.scale.value = 0.6f
-
-            assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
-        }
-
-    @Test
     fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
         testScope.runTest {
             val runningTaskId = 1
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index 72cfd92..fa81680 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -202,7 +202,7 @@
                 mDeviceProfile.updateInsets(mLauncherInsets);
 
                 TaskViewSimulator tvs = new TaskViewSimulator(helper.sandboxContext,
-                        FallbackActivityInterface.INSTANCE);
+                        FallbackActivityInterface.INSTANCE, false, 0);
                 tvs.setDp(mDeviceProfile);
 
                 int launcherRotation = info.rotation;
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c280307..f7069a6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -458,7 +458,7 @@
     <string name="widget_resized">Widget resized to width <xliff:g id="number" example="2">%1$s</xliff:g> height <xliff:g id="number" example="1">%2$s</xliff:g></string>
 
     <!-- Accessibility action to show quick actions menu for an icon. [CHAR_LIMIT=30] -->
-    <string name="action_deep_shortcut">Shortcuts</string>
+    <string name="action_deep_shortcut">Shortcut Menu</string>
 
     <!-- Accessibility action to dismiss a notification in the shortcuts menu for an icon. [CHAR_LIMIT=30] -->
     <string name="action_dismiss_notification">Dismiss</string>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 74dd971..8981024 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -233,6 +233,7 @@
 import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.KeyboardShortcutsDelegate;
 import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.PluginManagerWrapper;
@@ -784,7 +785,7 @@
         }
         // When the flag oneGridSpecs is on we want to disable ALLOW_ROTATION which is replaced
         // by FIXED_LANDSCAPE_MODE, ALLOW_ROTATION will only be used on Tablets afterwards.
-        if (!getDeviceProfile().isTablet) {
+        if (getDeviceProfile().isPhone || getDeviceProfile().isTwoPanels) {
             LauncherPrefs.get(this).put(LauncherPrefs.ALLOW_ROTATION, false);
         }
         getRotationHelper().setFixedLandscape(
@@ -2745,6 +2746,7 @@
         mModel.dumpState(prefix, fd, writer, args);
         mOverlayManager.dump(prefix, writer);
         ACTIVITY_TRACKER.dump(prefix, writer);
+        MSDLPlayerWrapper.INSTANCE.get(getApplicationContext()).dump(prefix, writer);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 7e3e392..fe11ee2 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -150,6 +150,7 @@
         mCallback.clearSearchResult();
         mInput.reset();
         mInput.clearFocus();
+        mInput.hideKeyboard();
         mQuery = null;
     }
 
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index 8368256..ed4f492 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -56,6 +56,7 @@
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.ToLongFunction;
 import java.util.stream.Collectors;
 
@@ -79,8 +80,8 @@
     private final Context mContext;
     private final ToLongFunction<UserHandle> mUserSerialProvider;
     private final Runnable mOnEmptyDbCreateCallback;
+    private final AtomicInteger mMaxItemId = new AtomicInteger(-1);
 
-    private int mMaxItemId = -1;
     public boolean mHotseatRestoreTableExists;
 
     /**
@@ -97,21 +98,19 @@
     protected void initIds() {
         // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
         // the DB here
-        if (mMaxItemId == -1) {
-            mMaxItemId = initializeMaxItemId(getWritableDatabase());
-        }
+        mMaxItemId.compareAndSet(-1, initializeMaxItemId(getWritableDatabase()));
     }
 
     @Override
     public void onCreate(SQLiteDatabase db) {
         if (LOGD) Log.d(TAG, "creating new launcher database");
 
-        mMaxItemId = 1;
+        mMaxItemId.set(1);
 
         addTableToDb(db, getDefaultUserSerial(), false /* optional */);
 
         // Fresh and clean launcher DB.
-        mMaxItemId = initializeMaxItemId(db);
+        mMaxItemId.set(initializeMaxItemId(db));
         mOnEmptyDbCreateCallback.run();
     }
 
@@ -451,11 +450,10 @@
     // after that point
     @Override
     public int generateNewItemId() {
-        if (mMaxItemId < 0) {
+        if (mMaxItemId.get() < 0) {
             throw new RuntimeException("Error: max item id was not initialized");
         }
-        mMaxItemId += 1;
-        return mMaxItemId;
+        return mMaxItemId.incrementAndGet();
     }
 
     /**
@@ -484,7 +482,7 @@
 
     public void checkId(ContentValues values) {
         int id = values.getAsInteger(Favorites._ID);
-        mMaxItemId = Math.max(id, mMaxItemId);
+        mMaxItemId.accumulateAndGet(id, Math::max);
     }
 
     private int initializeMaxItemId(SQLiteDatabase db) {
@@ -508,7 +506,7 @@
         int count = loader.loadLayout(db);
 
         // Ensure that the max ids are initialized
-        mMaxItemId = initializeMaxItemId(db);
+        mMaxItemId.set(initializeMaxItemId(db));
         return count;
     }
 
diff --git a/src/com/android/launcher3/util/ContextTracker.java b/src/com/android/launcher3/util/ContextTracker.java
index c729b4b..3201bd1 100644
--- a/src/com/android/launcher3/util/ContextTracker.java
+++ b/src/com/android/launcher3/util/ContextTracker.java
@@ -35,7 +35,7 @@
     private static final String TAG = "ContextTracker";
 
     private WeakReference<CONTEXT> mCurrentContext = new WeakReference<>(null);
-    private CopyOnWriteArrayList<SchedulerCallback<CONTEXT>> mCallbacks =
+    private final CopyOnWriteArrayList<SchedulerCallback<CONTEXT>> mCallbacks =
             new CopyOnWriteArrayList<>();
 
     @Nullable
@@ -81,7 +81,7 @@
 
     public boolean handleCreate(CONTEXT context) {
         mCurrentContext = new WeakReference<>(context);
-        return handleCreate(context, /* alreadyOnHome= */ false);
+        return handleCreate(context, isHomeStarted(context));
     }
 
     public boolean handleNewIntent(CONTEXT context) {
diff --git a/src/com/android/launcher3/util/MSDLPlayerWrapper.java b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
index aab7009..eccccc7 100644
--- a/src/com/android/launcher3/util/MSDLPlayerWrapper.java
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
@@ -30,6 +30,7 @@
 import com.google.android.msdl.domain.MSDLPlayer;
 import com.google.android.msdl.logging.MSDLEvent;
 
+import java.io.PrintWriter;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -65,4 +66,13 @@
     public List<MSDLEvent> getHistory() {
         return mMSDLPlayer.getHistory();
     }
+
+    /** Print the latest history of MSDL tokens played */
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "MSDLPlayerWrapper history of latest events:");
+        List<MSDLEvent> events = getHistory();
+        for (MSDLEvent event: events) {
+            writer.println(prefix + "\t" + event);
+        }
+    }
 }
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 1ddd453..68e493d 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -183,6 +183,7 @@
         </activity>
         <activity-alias android:name="Activity2"
                         android:label="TestActivity2"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -192,6 +193,7 @@
         </activity-alias>
         <activity-alias android:name="Activity3"
                         android:label="TestActivity3"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -201,6 +203,7 @@
         </activity-alias>
         <activity-alias android:name="Activity4"
                         android:label="TestActivity4"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -210,6 +213,7 @@
         </activity-alias>
         <activity-alias android:name="Activity5"
                         android:label="TestActivity5"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -219,6 +223,7 @@
         </activity-alias>
         <activity-alias android:name="Activity6"
                         android:label="TestActivity6"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -228,6 +233,7 @@
         </activity-alias>
         <activity-alias android:name="Activity7"
                         android:label="TestActivity7"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -237,6 +243,7 @@
         </activity-alias>
         <activity-alias android:name="Activity8"
                         android:label="TestActivity8"
+                        android:icon="@drawable/test_icon"
                         android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
@@ -246,6 +253,7 @@
         </activity-alias>
         <activity-alias android:name="Activity9" android:exported="true"
             android:label="TestActivity9"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -254,6 +262,7 @@
         </activity-alias>
         <activity-alias android:name="Activity10" android:exported="true"
             android:label="TestActivity10"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -262,6 +271,7 @@
         </activity-alias>
         <activity-alias android:name="Activity11" android:exported="true"
             android:label="TestActivity11"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -270,6 +280,7 @@
         </activity-alias>
         <activity-alias android:name="Activity12" android:exported="true"
             android:label="TestActivity12"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -278,6 +289,7 @@
         </activity-alias>
         <activity-alias android:name="Activity13" android:exported="true"
             android:label="TestActivity13"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -286,6 +298,7 @@
         </activity-alias>
         <activity-alias android:name="Activity14" android:exported="true"
             android:label="TestActivity14"
+            android:icon="@drawable/test_icon"
             android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -363,7 +376,7 @@
         </activity>
         <activity android:name="com.android.launcher3.testcomponent.ImeTestActivity"
             android:label="ImeTestActivity"
-            android:icon="@drawable/test_theme_icon"
+            android:icon="@drawable/test_icon"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index 7cd5da4..e8f778f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -18,6 +18,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.icons.waitForUpdateHandlerToFinish
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
@@ -160,6 +161,9 @@
             assertWithMessage("Index $index was not highRes")
                 .that(items[index].bitmap.isNullOrLowRes)
                 .isFalse()
+            assertWithMessage("Index $index was the default icon")
+                .that(isDefaultIcon(items[index].bitmap))
+                .isFalse()
         }
     }
 
@@ -168,9 +172,17 @@
             assertWithMessage("Index $index was not lowRes")
                 .that(items[index].bitmap.isNullOrLowRes)
                 .isTrue()
+            assertWithMessage("Index $index was the default icon")
+                .that(isDefaultIcon(items[index].bitmap))
+                .isFalse()
         }
     }
 
+    private fun isDefaultIcon(bitmap: BitmapInfo) =
+        LauncherAppState.getInstance(modelHelper.sandboxContext)
+            .iconCache
+            .isDefaultIcon(bitmap, modelHelper.sandboxContext.user)
+
     /** Recreate DeviceProfiles after changing InvariantDeviceProfile */
     private fun recreateSupportedDeviceProfiles() {
         LauncherAppState.getIDP(modelHelper.sandboxContext).supportedProfiles =
diff --git a/tests/res/drawable/test_icon.xml b/tests/res/drawable/test_icon.xml
new file mode 100644
index 0000000..72ebfeb
--- /dev/null
+++ b/tests/res/drawable/test_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFF0000" />
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>