Merge "Use the Coreographer's frame time for a more reliable timestamp." into main
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index e624be7..7d193aa 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.role.RoleManager.ROLE_HOME;
 import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -74,6 +75,7 @@
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.app.WindowConfiguration;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
@@ -1194,7 +1196,9 @@
                 .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         if (mBackAnimationController != null) {
             mBackAnimationController.registerComponentCallbacks();
-            mBackAnimationController.registerBackCallbacks(mHandler);
+            if (isHomeRoleHeld()) {
+                mBackAnimationController.registerBackCallbacks(mHandler);
+            }
         }
     }
 
@@ -1207,6 +1211,22 @@
                 .unregisterContentObserver(mAnimationRemovalObserver));
     }
 
+    /**
+     * Called when the overview-target changes. Updates the back callback registration state.
+     */
+    public void onOverviewTargetChange() {
+        if (isHomeRoleHeld()) {
+            mBackAnimationController.registerBackCallbacks(mHandler);
+        } else {
+            mBackAnimationController.unregisterBackCallbacks();
+        }
+    }
+
+    private boolean isHomeRoleHeld() {
+        RoleManager roleManager = mLauncher.getSystemService(RoleManager.class);
+        return roleManager == null || roleManager.isRoleHeld(ROLE_HOME);
+    }
+
     private void unregisterRemoteAnimations() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
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..0a53bd3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -34,7 +34,7 @@
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.ThumbnailData
-import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
+import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer
 import java.util.Collections
 import java.util.function.Predicate
 
@@ -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 f9e7cf0..f346e19 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -66,6 +66,7 @@
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Region;
 import android.graphics.Region.Op;
 import android.graphics.drawable.Drawable;
@@ -75,6 +76,7 @@
 import android.os.Handler;
 import android.util.Property;
 import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -862,15 +864,21 @@
 
     private void setBackButtonTouchListener(View buttonView,
             TaskbarNavButtonController navButtonController) {
+        final RectF rect = new RectF();
         buttonView.setOnTouchListener((v, event) -> {
-            if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                rect.set(0, 0, v.getWidth(), v.getHeight());
+            }
+            boolean isCancelled = event.getAction() == MotionEvent.ACTION_CANCEL
+                    || !rect.contains(event.getX(), event.getY());
+            if (event.getAction() == MotionEvent.ACTION_MOVE && !isCancelled) return false;
             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) {
+            if (motionEventAction == MotionEvent.ACTION_UP && !isCancelled) {
                 buttonView.performClick();
+                buttonView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
             }
             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..4498fea 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,12 @@
     private final Context mContext;
     private final @Nullable Context mNavigationBarPanelContext;
     private WindowManager mWindowManager;
-    private FrameLayout mTaskbarRootLayout;
     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 +129,10 @@
     // 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<>();
+    /** DisplayId - {@link FrameLayout} map for Connected Display. */
+    private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
     private StatefulActivity mActivity;
     private RecentsViewContainer mRecentsViewContainer;
 
@@ -167,7 +169,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 +184,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 +241,28 @@
         mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
-            mTaskbarRootLayout = new FrameLayout(mContext) {
+            FrameLayout taskbarRootLayout = 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);
                 }
             };
+            addTaskbarRootLayoutToMap(getDefaultDisplayId(), taskbarRootLayout);
         }
-        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 +271,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 +288,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 +302,10 @@
                                 // block above?
                                 recreateTaskbar();
                             } else {
-                                mTaskbarActivityContext.updateDeviceProfile(dp);
+                                getCurrentActivityContext().updateDeviceProfile(dp);
                             }
                         }
-                        mTaskbarActivityContext.onConfigurationChanged(configDiff);
+                        getCurrentActivityContext().onConfigurationChanged(configDiff);
                     }
                 }
                 mOldConfig = new Configuration(newConfig);
@@ -315,7 +322,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,18 +338,31 @@
         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);
+            removeTaskbarRootViewFromWindow(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;
         if (dp == null || !isTaskbarEnabled(dp)) {
-            removeTaskbarRootViewFromWindow();
+            removeTaskbarRootViewFromWindow(displayId);
         }
     }
 
@@ -350,8 +370,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 +381,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 +398,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 +409,7 @@
         mUserUnlocked = true;
         DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
         recreateTaskbar();
-        addTaskbarRootViewToWindow();
+        addTaskbarRootViewToWindow(getDefaultDisplayId());
     }
 
     /**
@@ -429,8 +452,9 @@
             mActivityOnDestroyCallback.run();
         }
         mRecentsViewContainer = recentsViewContainer;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setUIController(
+        TaskbarActivityContext taskbar = getCurrentActivityContext();
+        if (taskbar != null) {
+            taskbar.setUIController(
                     createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
@@ -472,11 +496,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 +520,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 +535,32 @@
                 }
             }
 
-            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);
+                FrameLayout taskbarRootLayout = getTaskbarRootLayoutForDisplay(displayId);
+                taskbarRootLayout.removeAllViews();
+                taskbarRootLayout.addView(taskbar.getDragLayer());
+                taskbar.notifyUpdateLayoutParams();
             }
+            addTaskbarToMap(displayId, taskbar);
         } finally {
             Trace.endSection();
         }
@@ -535,14 +572,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 +589,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 +644,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 +654,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,8 +717,7 @@
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
-        destroyExistingTaskbar();
-        removeTaskbarRootViewFromWindow();
+        destroyAllTaskbars();
         if (mUserUnlocked) {
             DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
         }
@@ -676,38 +726,112 @@
         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(getTaskbarRootLayoutForDisplay(displayId),
+                    taskbar.getWindowLayoutParams());
             mAddedWindow = true;
         }
     }
 
-    private void removeTaskbarRootViewFromWindow() {
-        if (enableTaskbarNoRecreate() && mAddedWindow) {
-            mWindowManager.removeViewImmediate(mTaskbarRootLayout);
+    private void removeTaskbarRootViewFromWindow(int displayId) {
+        FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
+        if (enableTaskbarNoRecreate() && mAddedWindow && rootLayout != null) {
+            mWindowManager.removeViewImmediate(rootLayout);
             mAddedWindow = false;
+            removeTaskbarRootLayoutFromMap(displayId);
         }
     }
 
+    /**
+     * 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);
+    }
+
+    /**
+     * Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
+     * map if there is not already a taskbar mapped to that displayId.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @param newTaskbar The new {@link TaskbarActivityContext} to add to the map.
+     */
+    private void addTaskbarToMap(int displayId, TaskbarActivityContext newTaskbar) {
+        if (!mTaskbars.contains(displayId)) {
+            mTaskbars.put(displayId, newTaskbar);
+        }
+    }
+
+    /**
+     * Removes the taskbar associated with the given display ID from the taskbar map.
+     *
+     * @param displayId The ID of the display for which to remove the taskbar.
+     */
+    private void removeTaskbarFromMap(int displayId) {
+        mTaskbars.delete(displayId);
+    }
+
+    /**
+     * Retrieves the root layout of the taskbar for the specified display.
+     *
+     * @param displayId The ID of the display for which to retrieve the taskbar root layout.
+     * @return The taskbar root layout {@link FrameLayout} for a given display or {@code null}.
+     */
+    private FrameLayout getTaskbarRootLayoutForDisplay(int displayId) {
+        return mRootLayouts.get(displayId);
+    }
+
+    /**
+     * Adds the taskbar root layout {@link FrameLayout} to taskbar map, mapped to display ID.
+     *
+     * @param displayId The ID of the display to associate with the taskbar root layout.
+     * @param rootLayout The taskbar root layout {@link FrameLayout} to add to the map.
+     */
+    private void addTaskbarRootLayoutToMap(int displayId, FrameLayout rootLayout) {
+        if (!mRootLayouts.contains(displayId)) {
+            mRootLayouts.put(displayId, rootLayout);
+        }
+    }
+
+    /**
+     * Removes taskbar root layout {@link FrameLayout} for given display ID from the taskbar map.
+     *
+     * @param displayId The ID of the display for which to remove the taskbar root layout.
+     */
+    private void removeTaskbarRootLayoutFromMap(int displayId) {
+        if (mRootLayouts.contains(displayId)) {
+            mRootLayouts.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/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 3c5d71e..f2355b8 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,9 @@
 
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
+
+        mNumStaticViews = taskbarRecentsLayoutTransition() && !mActivityContext.isPhoneMode()
+                ? addStaticViews() : 0;
     }
 
     /**
@@ -249,6 +261,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 +392,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 +424,105 @@
         }
         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() {
+        // Index where All Apps divider would be if it is already in Taskbar.
+        final int expectedAllAppsDividerIndex =
+                mIsRtl ? getChildCount() - mNumStaticViews - 1 : mNumStaticViews;
+        if (getChildAt(expectedAllAppsDividerIndex) == mTaskbarDividerContainer
+                && getChildCount() == mNumStaticViews + 1) {
+            // Only static views with divider so remove divider.
+            removeView(mTaskbarDividerContainer);
+        } else if (getChildAt(expectedAllAppsDividerIndex) != mTaskbarDividerContainer
+                && getChildCount() >= mNumStaticViews + 1) {
+            // Static views with at least one app icon so add divider. For RTL, add it after the
+            // icon that is at the expected index.
+            addView(
+                    mTaskbarDividerContainer,
+                    mIsRtl ? expectedAllAppsDividerIndex + 1 : expectedAllAppsDividerIndex);
+        }
+    }
+
+    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 +538,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 +580,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 +596,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 +612,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 +625,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 +636,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 +660,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 +675,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 +689,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. */
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/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 1f76bd1..fd08078 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -391,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/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index 7e3b362..e552b24 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -26,7 +26,6 @@
 import android.view.ViewConfiguration
 import androidx.annotation.DimenRes
 import androidx.annotation.DrawableRes
-import androidx.core.view.setPadding
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.dpToPx
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
@@ -68,7 +67,6 @@
             )
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         setIconDrawable(drawable)
-        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
         setForegroundTint(activityContext.getColor(R.color.all_apps_button_color))
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
index 344f163..df61d8a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -21,7 +21,6 @@
 import android.content.res.ColorStateList
 import android.graphics.Color.TRANSPARENT
 import android.util.AttributeSet
-import androidx.core.view.setPadding
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.dpToPx
 import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -51,7 +50,6 @@
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         val drawable = resources.getDrawable(R.drawable.taskbar_divider_button)
         setIconDrawable(drawable)
-        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
     }
 
     @SuppressLint("ClickableViewAccessibility")
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/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 1baba74..cc51adc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -264,6 +264,8 @@
 
     private boolean mIsOverlayVisible;
 
+    private final Runnable mOverviewTargetChangeRunnable = this::onOverviewTargetChanged;
+
     public static QuickstepLauncher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -550,6 +552,10 @@
             mUnfoldTransitionProgressProvider.destroy();
         }
 
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
+        }
         mTISBindHelper.onDestroy();
 
         if (mLauncherUnfoldAnimationController != null) {
@@ -1025,12 +1031,20 @@
         }
     }
 
+    private void onOverviewTargetChanged() {
+        QuickstepTransitionManager transitionManager = getAppTransitionManager();
+        if (transitionManager != null) {
+            transitionManager.onOverviewTargetChange();
+        }
+    }
+
     private void onTISConnected(TISBinder binder) {
         TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
         if (taskbarManager != null) {
             taskbarManager.setActivity(this);
         }
         mTISBindHelper.setPredictiveBackToHomeInProgress(mIsPredictiveBackToHomeInProgress);
+        binder.registerOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 27790ce..a58bb9b 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -159,6 +159,7 @@
 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;
@@ -785,7 +786,7 @@
                 && recentsAttachedToAppWindow) {
             // Only move running task if RecentsView has never been attached before, to avoid
             // TaskView jumping to new position as we move the tasks.
-            mRecentsView.moveRunningTaskToFront();
+            mRecentsView.moveRunningTaskToExpectedPosition();
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(
                 recentsAttachedToAppWindow, animate, updateRunningTaskAlpha);
@@ -2224,8 +2225,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/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 7abcfb8..9b56fd4 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -64,8 +64,6 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
-import com.android.quickstep.fallback.window.RecentsWindowManager;
-import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
 import com.android.quickstep.util.TransformParams;
@@ -172,16 +170,14 @@
     }
 
     @Override
-    protected boolean handleTaskAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget,
-            @NonNull ActiveGestureLog.CompoundString failureReason) {
-        if (mActiveAnimationFactory != null
-                && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
+    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
+        if (mActiveAnimationFactory != null && mActiveAnimationFactory.handleHomeTaskAppeared(
+                appearedTaskTargets)) {
             mActiveAnimationFactory = null;
-            failureReason.append("(FallbackSwipeHandler) should be handled as home task appeared");
-            return false;
+            return;
         }
 
-        return super.handleTaskAppeared(appearedTaskTarget, failureReason);
+        super.onTasksAppeared(appearedTaskTargets);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index f3ed491..4bd9ffb 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -390,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/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index a03c0f8..ef103c4 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.window.CachedDisplayInfo;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.Flags;
 
 import java.io.PrintWriter;
@@ -111,13 +112,13 @@
                 mNavBarGesturalHeight);
     }
 
-    private void refreshTouchRegion(Info info, Resources newRes) {
+    private void refreshTouchRegion(Info info, Resources newRes, String reason) {
         // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
         // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
         // It tries to cache and reuse swipe regions whenever possible based only on rotation
         mResources = newRes;
         mSwipeTouchRegions.clear();
-        resetSwipeRegions(info);
+        resetSwipeRegions(info, reason);
     }
 
     void setNavigationMode(NavigationMode newMode, Info info, Resources newRes) {
@@ -128,7 +129,7 @@
             return;
         }
         this.mMode = newMode;
-        refreshTouchRegion(info, newRes);
+        refreshTouchRegion(info, newRes, "setNavigationMode");
     }
 
     void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) {
@@ -136,7 +137,7 @@
             return;
         }
         mNavBarGesturalHeight = newGesturalHeight;
-        refreshTouchRegion(info, newRes);
+        refreshTouchRegion(info, newRes, "setGesturalHeight");
     }
 
     /**
@@ -147,14 +148,14 @@
      *
      * @see #enableMultipleRegions(boolean, Info)
      */
-    void createOrAddTouchRegion(Info info) {
+    void createOrAddTouchRegion(Info info, String reason) {
         mCachedDisplayInfo = new CachedDisplayInfo(info.currentSize, info.rotation);
 
         if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
                 && mCachedDisplayInfo.rotation == mQuickStepStartingRotation) {
             // User already was swiping and the current screen is same rotation as the starting one
             // Remove active nav bars in other rotations except for the one we started out in
-            resetSwipeRegions(info);
+            resetSwipeRegions(info, reason);
             return;
         }
         OrientationRectF region = mSwipeTouchRegions.get(mCachedDisplayInfo);
@@ -163,9 +164,9 @@
         }
 
         if (mEnableMultipleRegions) {
-            mSwipeTouchRegions.put(mCachedDisplayInfo, createRegionForDisplay(info));
+            mSwipeTouchRegions.put(mCachedDisplayInfo, createRegionForDisplay(info, reason));
         } else {
-            resetSwipeRegions(info);
+            resetSwipeRegions(info, reason);
         }
     }
 
@@ -184,7 +185,7 @@
             mActiveTouchRotation = 0;
             mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
         }
-        resetSwipeRegions(info);
+        resetSwipeRegions(info, "enableMultipleRegions");
     }
 
     /**
@@ -198,7 +199,7 @@
      */
     void setSingleActiveRegion(Info displayInfo) {
         mActiveTouchRotation = displayInfo.rotation;
-        resetSwipeRegions(displayInfo);
+        resetSwipeRegions(displayInfo, "setSingleActiveRegion");
     }
 
     /**
@@ -207,19 +208,21 @@
      * To be called whenever we want to stop tracking more than one swipe region.
      * Ok to call multiple times.
      */
-    private void resetSwipeRegions(Info region) {
+    private void resetSwipeRegions(Info region, String reason) {
         if (enableLog()) {
-            Log.d(TAG, "clearing all regions except rotation: " + mCachedDisplayInfo.rotation);
+            Log.d(TAG, "clearing all regions except rotation: " + mCachedDisplayInfo.rotation
+                    + " reason=" + reason);
         }
 
         mCachedDisplayInfo = new CachedDisplayInfo(region.currentSize, region.rotation);
         OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCachedDisplayInfo);
         if (regionToKeep == null) {
-            regionToKeep = createRegionForDisplay(region);
+            regionToKeep = createRegionForDisplay(region, reason);
         }
         mSwipeTouchRegions.clear();
         mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
         updateAssistantRegions(regionToKeep);
+        updateOneHandedRegions(regionToKeep);
     }
 
     private void resetSwipeRegions() {
@@ -228,15 +231,17 @@
         if (regionToKeep != null) {
             mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
             updateAssistantRegions(regionToKeep);
+            updateOneHandedRegions(regionToKeep);
         }
     }
 
-    private OrientationRectF createRegionForDisplay(Info display) {
+    private OrientationRectF createRegionForDisplay(Info display, String reason) {
         if (enableLog()) {
             Log.d(TAG, "creating rotation region for: " + mCachedDisplayInfo.rotation
             + " with mode: " + mMode + " displayRotation: " + display.rotation +
                     " displaySize: " + display.currentSize +
-                    " navBarHeight: " + mNavBarGesturalHeight);
+                    " navBarHeight: " + mNavBarGesturalHeight +
+                    " reason: " + reason);
         }
 
         Point size = display.currentSize;
@@ -264,9 +269,10 @@
                     orientationRectF.top = orientationRectF.bottom - touchHeight;
             }
         }
-        // One handed gestural only active on portrait mode
-        mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
-                size.x, size.y);
+        updateOneHandedRegions(orientationRectF);
+        ActiveGestureProtoLogProxy.logCreateTouchRegionForDisplay(rotation, size, orientationRectF,
+                mOneHandedModeRegion, mNavBarGesturalHeight, mNavBarLargerGesturalHeight,
+                reason);
 
         return orientationRectF;
     }
@@ -286,6 +292,12 @@
         mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
     }
 
+    private void updateOneHandedRegions(OrientationRectF orientationRectF) {
+        // One handed gestural only active on portrait mode
+        mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
+                orientationRectF.right, orientationRectF.bottom);
+    }
+
     boolean touchInAssistantRegion(MotionEvent ev) {
         return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
                 || mAssistantRightRegion.contains(ev.getX(), ev.getY());
@@ -411,9 +423,11 @@
             OrientationRectF rectF = mSwipeTouchRegions.get(key);
             regions.append(rectF).append(" ");
         }
-        pw.println(regions.toString());
+        pw.println(regions);
         pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
         pw.println("  mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight);
+        pw.println("  mAssistantLeftRegion=" + mAssistantLeftRegion);
+        pw.println("  mAssistantRightRegion=" + mAssistantRightRegion);
         pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 145773d..055aadb 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -56,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,
@@ -132,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.
@@ -164,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() {
@@ -241,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/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 79abc0f..909cc35 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -236,7 +236,8 @@
             return;
         }
 
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo());
+        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo(),
+                "RTH.updateGestureTouchRegions");
     }
 
     /**
@@ -273,7 +274,8 @@
 
             if (hasGestures(mMode)) {
                 updateGestureTouchRegions();
-                mOrientationTouchTransformer.createOrAddTouchRegion(info);
+                mOrientationTouchTransformer.createOrAddTouchRegion(info,
+                        "RTH.onDisplayInfoChanged");
                 mCurrentAppRotation = mDisplayRotation;
 
                 /* Update nav bars on the following:
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/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index d38eaf3..8edb16f 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -137,6 +137,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -163,7 +164,7 @@
 
         private final WeakReference<TouchInteractionService> mTis;
 
-        @Nullable private Runnable mOnOverviewTargetChangeListener = null;
+        private final Set<Runnable> mOnOverviewTargetChangeListeners = new HashSet<>();
 
         private TISBinder(TouchInteractionService tis) {
             mTis = new WeakReference<>(tis);
@@ -512,15 +513,19 @@
                     tis -> tis.mDeviceState.setGestureBlockingTaskId(taskId));
         }
 
-        /** Sets a listener to be run on Overview Target updates. */
-        public void setOverviewTargetChangeListener(@Nullable Runnable listener) {
-            mOnOverviewTargetChangeListener = listener;
+        /** Registers a listener to be run on Overview Target updates. */
+        public void registerOverviewTargetChangeListener(@NonNull Runnable listener) {
+            mOnOverviewTargetChangeListeners.add(listener);
+        }
+
+        /** Unregisters an OverviewTargetChange listener. */
+        public void unregisterOverviewTargetChangeListener(@NonNull Runnable listener) {
+            mOnOverviewTargetChangeListeners.remove(listener);
         }
 
         protected void onOverviewTargetChange() {
-            if (mOnOverviewTargetChangeListener != null) {
-                mOnOverviewTargetChangeListener.run();
-                mOnOverviewTargetChangeListener = null;
+            for (Runnable listener : mOnOverviewTargetChangeListeners) {
+                listener.run();
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index f7f3157..3eba9c0 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -121,6 +121,8 @@
 
     private TextView mHintView;
 
+    private final Runnable mOverviewTargetChangeRunnable = this::onOverviewTargetChanged;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -285,7 +287,7 @@
     private void onTISConnected(TISBinder binder) {
         setSetupUIVisible(isResumed());
         binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
-        binder.setOverviewTargetChangeListener(binder::preloadOverviewForSUWAllSet);
+        binder.registerOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
         binder.preloadOverviewForSUWAllSet();
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
@@ -293,6 +295,14 @@
         }
     }
 
+    private void onOverviewTargetChanged() {
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
+            binder.preloadOverviewForSUWAllSet();
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
+        }
+    }
+
     @Override
     protected void onPause() {
         super.onPause();
@@ -309,7 +319,7 @@
         if (binder != null) {
             setSetupUIVisible(false);
             binder.setSwipeUpProxy(null);
-            binder.setOverviewTargetChangeListener(null);
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
         }
     }
 
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/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index aac97bb..743fa40 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -689,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;
@@ -1563,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();
         }
@@ -1773,26 +1768,17 @@
     }
 
     /**
-     * Moves the running task to the front of the carousel in tablets, to minimize animation
-     * required to move the running task in grid.
+     * Moves the running task to the expected position in the carousel. In tablets, this minimize
+     * animation required to move the running task into focused task position.
      */
-    public void moveRunningTaskToFront() {
-        if (!mContainer.getDeviceProfile().isTablet) {
-            return;
-        }
-
+    public void moveRunningTaskToExpectedPosition() {
         TaskView runningTaskView = getRunningTaskView();
-        if (runningTaskView == null) {
+        if (runningTaskView == null || mCurrentPage != indexOfChild(runningTaskView)) {
             return;
         }
 
-        if (indexOfChild(runningTaskView) != mCurrentPage) {
-            return;
-        }
-
-        int frontIndex = enableLargeDesktopWindowingTile() ? getDesktopTaskViewCount() : 0;
-
-        if (mCurrentPage <= frontIndex) {
+        int runningTaskExpectedIndex = getRunningTaskExpectedIndex(runningTaskView);
+        if (mCurrentPage == runningTaskExpectedIndex) {
             return;
         }
 
@@ -1805,12 +1791,31 @@
         mMovingTaskView = null;
         runningTaskView.resetPersistentViewTransforms();
 
-        addView(runningTaskView, frontIndex);
-        setCurrentPage(frontIndex);
+        addView(runningTaskView, runningTaskExpectedIndex);
+        setCurrentPage(runningTaskExpectedIndex);
 
         updateTaskSize();
     }
 
+    private int getRunningTaskExpectedIndex(TaskView runningTaskView) {
+        if (mContainer.getDeviceProfile().isTablet) {
+            if (runningTaskView instanceof DesktopTaskView) {
+                return 0; // Desktop running task is always in front.
+            } else if (enableLargeDesktopWindowingTile()) {
+                return getDesktopTaskViewCount(); // Other running task is behind desktop tasks.
+            } else {
+                return 0;
+            }
+        } else {
+            int currentIndex = indexOfChild(runningTaskView);
+            if (currentIndex != -1) {
+                return currentIndex; // Keep the position if running task already in layout.
+            } else {
+                return 0; // New running task are added to the front to begin with.
+            }
+        }
+    }
+
     @Override
     protected void onScrollerAnimationAborted() {
         ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
@@ -2493,13 +2498,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());
@@ -2507,6 +2505,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
@@ -2515,21 +2514,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()) {
@@ -2962,25 +2968,20 @@
             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);
+            addView(taskView, getRunningTaskExpectedIndex(taskView));
             runningTaskViewId = taskView.getTaskViewId();
             if (wasEmpty) {
                 addView(mClearAllButton);
@@ -5698,43 +5699,6 @@
         updateCurrentTaskActionsVisibility();
         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
         updateEnabledOverlays();
-
-        if (enableRefactorTaskThumbnail()) {
-            int screenStart = 0;
-            int screenEnd = 0;
-            int centerPageIndex = 0;
-            if (showAsGrid()) {
-                screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
-                int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
-                screenEnd = screenStart + pageOrientedSize;
-            } else {
-                centerPageIndex = getPageNearestToCenterOfScreen();
-            }
-
-            Set<Integer> fullyVisibleTaskIds = new HashSet<>();
-
-            // Update the task data for the in/visible children
-            for (int i = 0; i < getTaskViewCount(); i++) {
-                TaskView taskView = requireTaskViewAt(i);
-                List<TaskContainer> containers = taskView.getTaskContainers();
-                if (containers.isEmpty()) {
-                    continue;
-                }
-                boolean isFullyVisible;
-                if (showAsGrid()) {
-                    isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart,
-                            screenEnd);
-                } else {
-                    isFullyVisible = i == centerPageIndex;
-                }
-                if (isFullyVisible) {
-                    List<Integer> taskIds = containers.stream().map(
-                            taskContainer -> taskContainer.getTask().key.id).toList();
-                    fullyVisibleTaskIds.addAll(taskIds);
-                }
-            }
-            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
-        }
     }
 
     @Override
@@ -5876,22 +5840,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?
@@ -5923,7 +5880,7 @@
                         tx, null /* overlay */);
             }
         }
-        mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
+        mRecentsAnimationController.finish(toRecents, () -> {
             if (onFinishComplete != null) {
                 onFinishComplete.run();
             }
@@ -6300,17 +6257,27 @@
     }
 
     private void updateEnabledOverlays() {
-        TaskView focusedTaskView = getFocusedTaskView();
-        for (TaskView taskView : getTaskViews()) {
-            if (taskView == focusedTaskView) {
-                continue;
+        if (enableRefactorTaskThumbnail()) {
+            Set<Integer> fullyVisibleTaskIds = new HashSet<>();
+            for (TaskView taskView : getTaskViews()) {
+                if (isTaskViewFullyVisible(taskView)) {
+                    fullyVisibleTaskIds.addAll(taskView.getTaskIdSet());
+                }
             }
-            taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
-        }
-        // Focus task overlay should be enabled and refreshed at last
-        if (focusedTaskView != null) {
-            focusedTaskView.setOverlayEnabled(
-                    mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
+            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
+        } else {
+            TaskView focusedTaskView = getFocusedTaskView();
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView == focusedTaskView) {
+                    continue;
+                }
+                taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
+            }
+            // Focus task overlay should be enabled and refreshed at last
+            if (focusedTaskView != null) {
+                focusedTaskView.setOverlayEnabled(
+                        mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 819ab05..7e489ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -147,11 +147,6 @@
         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
 
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
index 0091036..f25f6f4 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -38,6 +38,8 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
 import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG;
 
+import android.graphics.Point;
+import android.graphics.RectF;
 import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
@@ -505,4 +507,16 @@
                 taskId,
                 packageName);
     }
+
+    public static void logCreateTouchRegionForDisplay(int displayRotation,
+            @NonNull Point displaySize, @NonNull RectF swipeRegion, @NonNull RectF ohmRegion,
+            int gesturalHeight, int largerGesturalHeight, @NonNull String reason) {
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, 
+                "OrientationTouchTransformer.createRegionForDisplay: "
+                        + "dispRot=%d, dispSize=%s, swipeRegion=%s, ohmRegion=%s, "
+                        + "gesturalHeight=%d, largerGesturalHeight=%d, reason=%s",
+                displayRotation, displaySize.flattenToString(), swipeRegion.toShortString(),
+                ohmRegion.toShortString(), gesturalHeight, largerGesturalHeight, reason);
+    }
 }
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 6ebae49..c682990 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -353,7 +353,7 @@
         mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
         verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
         verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
-        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, false);
     }
 
     @Test
@@ -364,7 +364,7 @@
         mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true);
         verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
         verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
-        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_UP, true);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, true);
     }
 
     @Test
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..78d8e5d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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_addHotseatItemWithoutRecents_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), emptyList())
+            taskbarView.updateItems(createHotseatItems(2), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addHotseatItemWithRecents_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/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index f5d082d..ff0ad53 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -90,7 +90,7 @@
         float landscapeRegionY =
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertFalse(100, landscapeRegionY,
@@ -102,7 +102,8 @@
 
         // Override region
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -113,7 +114,7 @@
                 event -> mTouchTransformer.touchInAssistantRegion(event));
 
         // Override region again
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertFalse(100, landscapeRegionY,
@@ -132,7 +133,8 @@
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -144,7 +146,7 @@
         // We have to add 0 rotation second so that gets set as the current rotation, otherwise
         // matrix transform will fail (tests only work in Portrait at the moment)
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
 
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -165,8 +167,9 @@
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
         tapAndAssertFalse(0, landscapeRegionY,
@@ -181,9 +184,10 @@
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         mTouchTransformer.enableMultipleRegions(false, mInfo);
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -213,14 +217,14 @@
 
     @Test
     public void applyTransform_taskNotFrozen_notInRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertFalse(100, 100,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
     public void applyTransform_taskFrozen_noRotate_outOfRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         tapAndAssertFalse(100, 100,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -228,7 +232,7 @@
 
     @Test
     public void applyTransform_taskFrozen_noRotate_inRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
@@ -237,7 +241,7 @@
 
     @Test
     public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -246,7 +250,8 @@
     @Test
     public void applyTransform_taskNotFrozen_90Rotate_inRegion() {
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -254,10 +259,11 @@
 
     @Test
     public void applyTransform_taskNotFrozen_90Rotate_withTwoRegions() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         // Landscape point
         float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1);
@@ -278,10 +284,11 @@
     @Test
     public void applyTransform_90Rotate_inRotatedRegion() {
         // Create regions for both 0 Rotation and 90 Rotation
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-                .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+                .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                        "test");
         // Portrait point in landscape orientation axis
         float x1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0);
         // bottom of screen, from landscape perspective right side of screen
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0d4e79b..326ee06 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -568,4 +568,8 @@
 
     <!-- WindowManagerProxy -->
     <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
+
+    <!-- App Title Pill -->
+    <dimen name="app_title_pill_horizontal_padding">4dp</dimen>
+    <dimen name="app_title_pill_round_rect_padding">2dp</dimen>
 </resources>
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/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 817cc40..ed2ab81 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -127,8 +127,6 @@
 
     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
 
-    private static final int APP_PILL_TITLE_PADDING = 8;
-
     private float mScaleForReorderBounce = 1f;
 
     private IntArray mBreakPointsIntArray;
@@ -734,12 +732,18 @@
         getDrawingRect(tmpRect);
         CharSequence text = getText();
 
+        int mAppTitleHorizontalPadding = getResources().getDimensionPixelSize(
+                R.dimen.app_title_pill_horizontal_padding);
+        int mRoundRectPadding = getResources().getDimensionPixelSize(
+                R.dimen.app_title_pill_round_rect_padding);
+
         float titleLength = (getPaint().measureText(text, 0, text.length())
-                + APP_PILL_TITLE_PADDING * 2);
+                + (mAppTitleHorizontalPadding + mRoundRectPadding) * 2);
         titleLength = Math.min(titleLength, tmpRect.width());
         appTitleBounds = new RectF((tmpRect.width() - titleLength) / 2.f - getCompoundPaddingLeft(),
                 0, (tmpRect.width() + titleLength) / 2.f + getCompoundPaddingRight(),
                 (int) Math.ceil(fm.bottom - fm.top));
+        appTitleBounds.inset(mRoundRectPadding * 2, 0);
 
 
         if (mIcon != null) {
@@ -859,8 +863,13 @@
                     getPaddingBottom());
         }
         if (shouldDrawAppContrastTile()) {
-            setPadding(getPaddingLeft() + APP_PILL_TITLE_PADDING, getPaddingTop(),
-                    getPaddingRight() + APP_PILL_TITLE_PADDING,
+            int mAppTitleHorizontalPadding = getResources().getDimensionPixelSize(
+                    R.dimen.app_title_pill_horizontal_padding);
+            int mRoundRectPadding = getResources().getDimensionPixelSize(
+                    R.dimen.app_title_pill_round_rect_padding);
+
+            setPadding(mAppTitleHorizontalPadding + mRoundRectPadding, getPaddingTop(),
+                    mAppTitleHorizontalPadding + mRoundRectPadding,
                     getPaddingBottom());
         }
         // Only apply two line for all_apps and device search only if necessary.
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 867bf98..18619f5 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -65,7 +65,7 @@
     protected final ActivityContext mActivityContext;
     protected final DropTargetHandler mDropTargetHandler;
     protected DropTargetBar mDropTargetBar;
-    private final MSDLPlayerWrapper mMSDLPlayerWrapper;
+    private MSDLPlayerWrapper mMSDLPlayerWrapper;
 
     /** Whether this drop target is active for the current drag */
     protected boolean mActive;
@@ -438,6 +438,11 @@
         return textHeight + getPaddingTop() + getPaddingBottom() >= availableHeight;
     }
 
+    @VisibleForTesting
+    public void setMSDLPlayerWrapper(MSDLPlayerWrapper wrapper) {
+        mMSDLPlayerWrapper = wrapper;
+    }
+
     /**
      * Reduce the size of the text until it fits the measured width or reaches a minimum.
      *
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 58789fd..425f277 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -130,7 +130,6 @@
     public void completeDrop(DragObject d) {
         ItemInfo item = d.dragInfo;
         if (canRemove(item)) {
-            onAccessibilityDrop(null, item);
             mDropTargetHandler.onDeleteComplete(item);
         }
     }
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index f1029b1..4d3fe52 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -65,6 +65,7 @@
     }
 
     fun onDeleteComplete(item: ItemInfo) {
+        removeItemAndStripEmptyScreens(null /* view */, item)
         var pageItem: ItemInfo = item
         if (item.container <= 0) {
             val v = mLauncher.workspace.getHomescreenIconByItemId(item.container)
@@ -90,11 +91,7 @@
     }
 
     fun onAccessibilityDelete(view: View?, item: ItemInfo, announcement: CharSequence) {
-        // Remove the item from launcher and the db, we can ignore the containerInfo in this call
-        // because we already remove the drag view from the folder (if the drag originated from
-        // a folder) in Folder.beginDrag()
-        mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop")
-        mLauncher.workspace.stripEmptyScreens()
+        removeItemAndStripEmptyScreens(view, item)
         mLauncher.dragLayer.announceForAccessibility(announcement)
     }
 
@@ -105,4 +102,12 @@
     fun onClick(buttonDropTarget: ButtonDropTarget) {
         mLauncher.accessibilityDelegate.handleAccessibleDrop(buttonDropTarget, null, null)
     }
+
+    private fun removeItemAndStripEmptyScreens(view: View?, item: ItemInfo) {
+        // Remove the item from launcher and the db, we can ignore the containerInfo in this call
+        // because we already remove the drag view from the folder (if the drag originated from
+        // a folder) in Folder.beginDrag()
+        mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop")
+        mLauncher.workspace.stripEmptyScreens()
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index f61d3f0..8981024 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -785,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(
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/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 3f52d8a..75fd31e 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -58,12 +58,17 @@
         }
 
         val isFirstLoad = get(context).get(LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE)
-        Log.d(TAG, "Begin grid migration. First load: $isFirstLoad")
+        Log.d(
+            TAG,
+            "Begin grid migration. First load: $isFirstLoad\n srcDeviceState: " +
+                "$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb",
+        )
 
         // This is a special case where if the grid is the same amount of columns but a larger
         // amount of rows we simply copy over the source grid to the destination grid, rather
         // than undergoing the general grid migration.
         if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
+            Log.d(TAG, "Migrating to strictly taller grid")
             copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
             if (oneGridSpecs()) {
                 val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
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 eccccc7..8a1d923 100644
--- a/src/com/android/launcher3/util/MSDLPlayerWrapper.java
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
@@ -69,6 +69,7 @@
 
     /** Print the latest history of MSDL tokens played */
     public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + mMSDLPlayer.toString());
         writer.println(prefix + "MSDLPlayerWrapper history of latest events:");
         List<MSDLEvent> events = getHistory();
         for (MSDLEvent event: events) {
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/DeleteDropTargetTest.kt b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
index 42374a5..fa368e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
@@ -16,7 +16,12 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -24,6 +29,8 @@
 
     @get:Rule val mSetFlagsRule = SetFlagsRule()
 
+    @Mock private val msdlPlayerWrapper = mock<MSDLPlayerWrapper>()
+
     private var mContext: Context = ActivityContextWrapper(getApplicationContext())
 
     // Use a non-abstract class implementation
@@ -50,13 +57,12 @@
     @Test
     @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun onDragEnter_performsMSDLSwipeThresholdFeedback() {
+        buttonDropTarget.setMSDLPlayerWrapper(msdlPlayerWrapper)
         val target = DropTarget.DragObject(mContext)
         target.dragView = mock<DragView<*>>()
         buttonDropTarget.onDragEnter(target)
-        val wrapper = MSDLPlayerWrapper.INSTANCE.get(mContext)
 
-        val history = wrapper.history
-        assertThat(history.size).isEqualTo(1)
-        assertThat(history[0].tokenName).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR.name)
+        verify(msdlPlayerWrapper, times(1)).playToken(eq(MSDLToken.SWIPE_THRESHOLD_INDICATOR))
+        verifyNoMoreInteractions(msdlPlayerWrapper)
     }
 }
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>