Merge "[CD Taskbar] Force 3 Button Mode on External Display by Default" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index ed370ec..d4cea8d 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -559,6 +559,13 @@
 }
 
 flag {
+  name: "enable_launcher_visual_refresh"
+  namespace: "launcher"
+  description: "Adds refresh for font family, app longpress menu icons, and pagination dots"
+  bug: "395145453"
+}
+
+flag {
   name: "restore_archived_shortcuts"
   namespace: "launcher"
   description: "Makes sure pre-archived pinned shortcuts also get restored"
@@ -608,3 +615,10 @@
   description: "Enable Strict Mode for the Launcher app"
   bug: "394651876"
 }
+
+flag {
+  name: "extendible_theme_manager"
+  namespace: "launcher"
+  description: "Enables custom theme manager in Launcher"
+  bug: "381897614"
+}
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index f2f1ebd..05f0695 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -88,6 +88,9 @@
     <dimen name="task_thumbnail_header_icon_size">18dp</dimen>
     <dimen name="task_thumbnail_header_round_corner_radius">16dp</dimen>
 
+    <!--  How much a task being dragged for dismissal can undershoot the origin when dragged back to its start position.  -->
+    <dimen name="task_dismiss_max_undershoot">25dp</dimen>
+
     <dimen name="task_icon_cache_default_icon_size">72dp</dimen>
     <item name="overview_modal_max_scale" format="float" type="dimen">1.1</item>
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 23065b5..5afc5ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
+
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.view.MotionEvent;
@@ -272,11 +274,26 @@
     }
 
     private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
-        // Find the single desktop task that contains a grouping of desktop tasks
-        DesktopTask desktopTask = findDesktopTask(tasks);
+        // Find all desktop tasks.
+        List<DesktopTask> desktopTasks = tasks.stream()
+                .filter(t -> t instanceof DesktopTask)
+                .map(t -> (DesktopTask) t)
+                .toList();
 
-        if (desktopTask != null) {
-            mTasks = desktopTask.getTasks().stream()
+        // Apps on the connected displays seem to be in different Desktop tasks even with the
+        // multiple desktops flag disabled. So, until multiple desktops is implemented the following
+        // should help with team-fooding Alt+tab on connected displays. Post multiple desktop,
+        // further changes maybe required to support launching selected desktops.
+        if (enableAltTabKqsOnConnectedDisplays()) {
+            mTasks = desktopTasks.stream()
+                    .flatMap(t -> t.getTasks().stream())
+                    .map(SingleTask::new)
+                    .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
+                    .collect(Collectors.toList());
+
+            mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size());
+        } else if (!desktopTasks.isEmpty()) {
+            mTasks = desktopTasks.get(0).getTasks().stream()
                     .map(SingleTask::new)
                     .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
                     .collect(Collectors.toList());
@@ -289,14 +306,6 @@
         }
     }
 
-    @Nullable
-    private DesktopTask findDesktopTask(List<GroupTask> tasks) {
-        return (DesktopTask) tasks.stream()
-                .filter(t -> t instanceof DesktopTask)
-                .findFirst()
-                .orElse(null);
-    }
-
     void closeQuickSwitchView() {
         closeQuickSwitchView(true);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 78c8e4b..8880abd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
 
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
@@ -2011,7 +2013,8 @@
         return mControllers.uiController.isIconAlignedWithHotseat();
     }
 
-    @VisibleForTesting
+    // TODO(b/395061396): Remove `otherwise` when overview in widow is enabled.
+    @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
     public TaskbarControllers getControllers() {
         return mControllers;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ea2dec1..66f19eb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,6 +32,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
+import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
 
 import android.annotation.SuppressLint;
 import android.app.PendingIntent;
@@ -70,7 +71,9 @@
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -82,6 +85,7 @@
 
 import java.io.PrintWriter;
 import java.util.StringJoiner;
+import java.util.function.LongConsumer;
 
 /**
  * Class to manage taskbar lifecycle
@@ -115,7 +119,10 @@
     private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
-    private final Context mParentContext;
+    private final Context mBaseContext;
+    // TODO: Remove this during the connected displays lifecycle refactor.
+    private final Context mPrimaryWindowContext;
+    private WindowManager mPrimaryWindowManager;
     private final TaskbarNavButtonController mDefaultNavButtonController;
     private final ComponentCallbacks mDefaultComponentCallbacks;
 
@@ -182,6 +189,7 @@
             new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
 
     private final AllAppsActionManager mAllAppsActionManager;
+    private final RecentsDisplayModel mRecentsDisplayModel;
 
     private final Runnable mActivityOnDestroyCallback = new Runnable() {
         @Override
@@ -238,37 +246,46 @@
                 }
             };
 
+    private final LongConsumer mSysUiFlagChangeReceiver = this::onSystemUiFlagsChanged;
+
     @SuppressLint("WrongConstant")
     public TaskbarManager(
             Context context,
             AllAppsActionManager allAppsActionManager,
-            TaskbarNavButtonCallbacks navCallbacks) {
-        mParentContext = context;
-        createWindowContext(context.getDisplayId());
+            TaskbarNavButtonCallbacks navCallbacks,
+            RecentsDisplayModel recentsDisplayModel) {
+        mBaseContext = context;
         mAllAppsActionManager = allAppsActionManager;
+        mRecentsDisplayModel = recentsDisplayModel;
+        mPrimaryWindowContext = createWindowContext(getDefaultDisplayId());
         if (enableTaskbarNoRecreate()) {
+            mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
             createTaskbarRootLayout(getDefaultDisplayId());
         }
         mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
         mDefaultComponentCallbacks = createDefaultComponentCallbacks();
-        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+        SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+        SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        getPrimaryWindowContext().registerComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.register(getPrimaryWindowContext(), Intent.ACTION_SHUTDOWN);
+        mPrimaryWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.register(mPrimaryWindowContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
-                    getPrimaryWindowContext(),
+                    mPrimaryWindowContext,
                     SYSTEM_ACTION_ID_TASKBAR,
                     new Intent(ACTION_SHOW_TASKBAR).setPackage(
-                            getPrimaryWindowContext().getPackageName()),
+                            mPrimaryWindowContext.getPackageName()),
                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
             mTaskbarBroadcastReceiver.register(
-                    getPrimaryWindowContext(), RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+                    mPrimaryWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
         });
 
+        RecentsAnimationDeviceState deviceState = RecentsAnimationDeviceState.INSTANCE.get(context);
+        onSystemUiFlagsChanged(deviceState.getSystemUiStateFlags());
+        deviceState.addSysUiFlagChangeCallback(mSysUiFlagChangeReceiver);
+
         debugWhyTaskbarNotDestroyed("TaskbarManager created");
         recreateTaskbar();
     }
@@ -279,15 +296,15 @@
         return new TaskbarNavButtonController(
                 context,
                 navCallbacks,
-                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()),
+                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext),
                 new Handler(),
-                new ContextualSearchInvoker(getPrimaryWindowContext()));
+                new ContextualSearchInvoker(mPrimaryWindowContext));
     }
 
     private ComponentCallbacks createDefaultComponentCallbacks() {
         return new ComponentCallbacks() {
             private Configuration mOldConfig =
-                    getPrimaryWindowContext().getResources().getConfiguration();
+                    mPrimaryWindowContext.getResources().getConfiguration();
 
             @Override
             public void onConfigurationChanged(Configuration newConfig) {
@@ -297,8 +314,8 @@
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
                 // TODO: adapt this logic to be specific to different displays.
                 DeviceProfile dp = mUserUnlocked
-                        ? LauncherAppState.getIDP(getPrimaryWindowContext()).getDeviceProfile(
-                        getPrimaryWindowContext())
+                        ? LauncherAppState.getIDP(mPrimaryWindowContext).getDeviceProfile(
+                        mPrimaryWindowContext)
                         : null;
                 int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
 
@@ -350,7 +367,6 @@
             int displayId = mTaskbars.keyAt(i);
             destroyTaskbarForDisplay(displayId);
             removeTaskbarRootViewFromWindow(displayId);
-            removeWindowContextFromMap(displayId);
         }
     }
 
@@ -417,7 +433,7 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
-        DisplayController.INSTANCE.get(getPrimaryWindowContext()).addChangeListener(
+        DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
                 mRecreationListener);
         recreateTaskbar();
         addTaskbarRootViewToWindow(getDefaultDisplayId());
@@ -481,12 +497,23 @@
                 return ql.getUnfoldTransitionProgressProvider();
             }
         } else {
-            return SystemUiProxy.INSTANCE.get(
-                    getPrimaryWindowContext()).getUnfoldTransitionProvider();
+            return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
         }
         return null;
     }
 
+    /** Creates a {@link TaskbarUIController} to use with non default displays. */
+    private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
+        if (RecentsDisplayModel.enableOverviewInWindow()) {
+            RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
+            if (rvc != null) {
+                return createTaskbarUIControllerForRecentsViewContainer(rvc);
+            }
+        }
+
+        return new TaskbarUIController();
+    }
+
     /**
      * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
      */
@@ -543,7 +570,7 @@
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
             if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
-                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext())
+                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext)
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
                 if (!isTaskbarEnabled) {
                     return;
@@ -561,7 +588,11 @@
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             taskbar.init(mSharedState);
 
-            if (mRecentsViewContainer != null) {
+            // Non default displays should not use LauncherTaskbarUIController as they shouldn't
+            // have access to the Launcher activity.
+            if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+                taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
+            } else if (mRecentsViewContainer != null) {
                 taskbar.setUIController(
                         createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
@@ -583,7 +614,7 @@
         }
     }
 
-    public void onSystemUiFlagsChanged(@SystemUiStateFlags long systemUiStateFlags) {
+    private void onSystemUiFlagsChanged(@SystemUiStateFlags long systemUiStateFlags) {
         if (DEBUG) {
             Log.d(TAG, "SysUI flags changed: " + formatFlagChange(systemUiStateFlags,
                     mSharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString));
@@ -719,6 +750,14 @@
      * primary device or a previously mirroring display is switched to extended mode.
      */
     public void onDisplayAddSystemDecorations(int displayId) {
+        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+            return;
+        }
+
+        Context newWindowContext = createWindowContext(displayId);
+        if (newWindowContext != null) {
+            addWindowContextToMap(displayId, newWindowContext);
+        }
     }
 
     /**
@@ -726,6 +765,14 @@
      * removed from the primary device.
      */
     public void onDisplayRemoved(int displayId) {
+        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+            return;
+        }
+
+        Context windowContext = getWindowContext(displayId);
+        if (windowContext != null) {
+            removeWindowContextFromMap(displayId);
+        }
     }
 
     /**
@@ -755,19 +802,21 @@
         mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
-        mTaskbarBroadcastReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
 
         if (mUserUnlocked) {
-            DisplayController.INSTANCE.get(getPrimaryWindowContext()).removeChangeListener(
+            DisplayController.INSTANCE.get(mPrimaryWindowContext).removeChangeListener(
                     mRecreationListener);
         }
-        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+        SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+        SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        RecentsAnimationDeviceState.INSTANCE.get(getPrimaryWindowContext())
+                .removeSysUiFlagChangeCallback(mSysUiFlagChangeReceiver);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        getPrimaryWindowContext().unregisterComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+        mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
         destroyAllTaskbars();
     }
 
@@ -829,6 +878,23 @@
     }
 
     /**
+     * Returns the {@link TaskbarUIController} associated with the given display ID.
+     * TODO(b/395061396): Remove this method when overview in widow is enabled.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @return The {@link TaskbarUIController} for the specified display, or
+     *         {@code null} if no taskbar is associated with that display.
+     */
+    @Nullable
+    public TaskbarUIController getUIControllerForDisplay(int displayId) {
+        if (!mTaskbars.contains(displayId)) {
+            return null;
+        }
+
+        return getTaskbarForDisplay(displayId).getControllers().uiController;
+    }
+
+    /**
      * Retrieves whether RootLayout was added to window for specific display, or false if no
      * such mapping has been made.
      *
@@ -855,16 +921,16 @@
      * Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
      */
     private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
-        Display display = mParentContext.getSystemService(DisplayManager.class).getDisplay(
+        Display display = mBaseContext.getSystemService(DisplayManager.class).getDisplay(
                 displayId);
         Context navigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
-                ? mParentContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
+                ? mBaseContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
 
         TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
                 navigationBarPanelContext, dp, mDefaultNavButtonController,
                 mUnfoldProgressProvider, isDefaultDisplay(displayId),
-                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()));
+                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext));
 
         addTaskbarToMap(displayId, newTaskbar);
         return newTaskbar;
@@ -965,22 +1031,26 @@
     }
 
     /**
-     * Creates {@link Context} for the taskbar on the specified display and›› adds it to map.
+     * Creates {@link Context} for the taskbar on the specified display.
      * @param displayId The ID of the display for which to create the window context.
      */
-    private void createWindowContext(int displayId) {
-        DisplayManager displayManager = mParentContext.getSystemService(DisplayManager.class);
+    private @Nullable Context createWindowContext(int displayId) {
+        DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
         if (displayManager == null) {
-            return;
+            return null;
         }
 
         Display display = displayManager.getDisplay(displayId);
-        if (display != null) {
-            int windowType = (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId))
-                    ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL;
-            Context newContext = mParentContext.createWindowContext(display, windowType, null);
-            addWindowContextToMap(displayId, newContext);
+        if (display == null) {
+            return null;
         }
+
+        int windowType = TYPE_NAVIGATION_BAR_PANEL;
+        if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId)) {
+            windowType = TYPE_NAVIGATION_BAR;
+        }
+
+        return mBaseContext.createWindowContext(display, windowType, null);
     }
 
     /**
@@ -990,12 +1060,13 @@
      * @return The Window Context {@link Context} for a given display or {@code null}.
      */
     private Context getWindowContext(int displayId) {
-        return mWindowContexts.get(displayId);
+        return (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays())
+                ? mPrimaryWindowContext : mWindowContexts.get(displayId);
     }
 
     @VisibleForTesting
     public Context getPrimaryWindowContext() {
-        return getWindowContext(getDefaultDisplayId());
+        return mPrimaryWindowContext;
     }
 
     /**
@@ -1004,8 +1075,17 @@
      * @param displayId The ID of the display for which to retrieve the window manager.
      * @return The window manager {@link WindowManager} for a given display or {@code null}.
      */
-    private WindowManager getWindowManager(int displayId) {
-        return getWindowContext(displayId).getSystemService(WindowManager.class);
+    private @Nullable WindowManager getWindowManager(int displayId) {
+        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+            return mPrimaryWindowManager;
+        }
+
+        Context externalDisplayContext = getWindowContext(displayId);
+        if (externalDisplayContext == null) {
+            return null;
+        }
+
+        return externalDisplayContext.getSystemService(WindowManager.class);
     }
 
     /**
@@ -1032,7 +1112,7 @@
     }
 
     private int getDefaultDisplayId() {
-        return mParentContext.getDisplayId();
+        return mBaseContext.getDisplayId();
     }
 
     /** Temp logs for b/254119092. */
@@ -1076,7 +1156,7 @@
         log.add("\t\tWindowContext.getResources().getConfiguration()="
                 + windowContext.getResources().getConfiguration());
         if (mUserUnlocked) {
-            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(getPrimaryWindowContext())"
+            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mPrimaryWindowContext)"
                     + ".isTaskbarPresent=" + contextTaskbarPresent);
         } else {
             log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 23f4f67..6eec7d5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -172,10 +172,12 @@
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationDeviceState.AssistantVisibilityChangeListener;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TISBinder;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.TouchInteractionService.TISBinder;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AsyncClockEventDelegate;
 import com.android.quickstep.util.LauncherUnfoldAnimationController;
@@ -222,7 +224,7 @@
 import java.util.stream.Stream;
 
 public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
-        SystemShortcut.BubbleActivityStarter {
+        SystemShortcut.BubbleActivityStarter, AssistantVisibilityChangeListener {
     private static final boolean TRACE_LAYOUTS =
             SystemProperties.getBoolean("persist.debug.trace_layouts", false);
     private static final String TRACE_RELAYOUT_CLASS =
@@ -593,6 +595,8 @@
         mHotseatPredictionController.destroy();
         if (mViewCapture != null) mViewCapture.close();
         removeBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
+        RecentsAnimationDeviceState.INSTANCE.get(this)
+                .removeAssistantVisibilityChangeListener(this);
     }
 
     @Override
@@ -726,6 +730,7 @@
         View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
         OverviewComponentObserver.INSTANCE.get(this)
                 .addOverviewChangeListener(mOverviewChangeListener);
+        RecentsAnimationDeviceState.INSTANCE.get(this).addAssistantVisibilityChangeListener(this);
     }
 
     @Override
@@ -1085,7 +1090,6 @@
         if (taskbarManager != null) {
             taskbarManager.setActivity(this);
         }
-        mTISBindHelper.setPredictiveBackToHomeInProgress(mIsPredictiveBackToHomeInProgress);
     }
 
     @Override
@@ -1359,7 +1363,8 @@
      */
     public void setPredictiveBackToHomeInProgress(boolean isInProgress) {
         mIsPredictiveBackToHomeInProgress = isInProgress;
-        mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
+        RecentsAnimationDeviceState.INSTANCE.get(this)
+                .setPredictiveBackToHomeInProgress(isInProgress);
     }
 
     public boolean getPredictiveBackToHomeInProgress() {
@@ -1501,6 +1506,11 @@
         return getStateManager().getState().isRecentsViewVisible;
     }
 
+    @Override
+    public void onAssistantVisibilityChanged(float visibility) {
+        mHotseat.getQsb().setAlpha(1f - visibility);
+    }
+
     public boolean isCanShowAllAppsEducationView() {
         return mCanShowAllAppsEducationView;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index ff726e6..865e16b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -28,8 +28,6 @@
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -47,7 +45,7 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.OverviewToHomeAnim;
@@ -113,8 +111,8 @@
             return false;
         }
         mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
-        boolean isOneHandedModeActive = (SystemUiProxy.INSTANCE.get(mLauncher)
-                .getLastSystemUiStateFlags() & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
+        boolean isOneHandedModeActive =
+                RecentsAnimationDeviceState.INSTANCE.get(mLauncher).isOneHandedModeActive();
         // Reset touch slop multiplier to default 1.0f if one-handed-mode is not active
         mDetector.setTouchSlopMultiplier(
                 isOneHandedModeActive ? ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER : 1f /* default */);
@@ -250,9 +248,8 @@
     }
 
     private boolean handlingOverviewAnim() {
-        long stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         return mDidTouchStartInNavBar && mStartState == NORMAL
-                && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
+                && !RecentsAnimationDeviceState.INSTANCE.get(mLauncher).isOverviewDisabled();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 05d12c3..6bca4c7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -55,7 +55,6 @@
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
@@ -79,7 +78,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.MotionPauseDetector;
@@ -178,8 +177,7 @@
         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
             return false;
         }
-        long stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
-        if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+        if (RecentsAnimationDeviceState.INSTANCE.get(mLauncher).isOverviewDisabled()) {
             return false;
         }
         if (isTrackpadMultiFingerSwipe(ev)) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index f582324..7547f15 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -34,7 +34,6 @@
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.view.MotionEvent;
 
@@ -46,7 +45,7 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
-import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -84,8 +83,7 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        long stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
-        if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+        if (RecentsAnimationDeviceState.INSTANCE.get(mLauncher).isOverviewDisabled()) {
             return NORMAL;
         }
         return isDragTowardPositive ? QUICK_SWITCH_FROM_HOME : NORMAL;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 99b962b..77a05c1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -20,6 +20,7 @@
 import androidx.dynamicanimation.animation.SpringAnimation
 import com.android.app.animation.Interpolators.DECELERATE
 import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
 import com.android.launcher3.Utilities.EDGE_NAV_BAR
 import com.android.launcher3.Utilities.boundToRange
 import com.android.launcher3.Utilities.isRtl
@@ -144,7 +145,7 @@
                     0f,
                     dismissLength.toFloat(),
                     0f,
-                    DISMISS_MAX_UNDERSHOOT,
+                    container.resources.getDimension(R.dimen.task_dismiss_max_undershoot),
                     DECELERATE,
                 )
         taskBeingDragged.secondaryDismissTranslationProperty.setValue(
@@ -207,6 +208,5 @@
 
     companion object {
         private const val DISMISS_THRESHOLD_FRACTION = 0.5f
-        private const val DISMISS_MAX_UNDERSHOOT = 25f
     }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index 6d588d9..82fbaa6 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -83,8 +83,6 @@
 
     public abstract boolean isInLiveTileMode();
 
-    public abstract void onAssistantVisibilityChanged(float assistantVisibility);
-
     public abstract boolean isResumed();
 
     public abstract boolean isStarted();
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index d8e0296..d122d24 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -69,14 +69,6 @@
         }
     }
 
-    /** 5 */
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        // This class becomes active when the screen is locked.
-        // Rather than having it handle assistant visibility changes, the assistant visibility is
-        // set to zero prior to this class becoming active.
-    }
-
     /** 6 */
     @Override
     public AnimationFactory prepareRecentsUI(
diff --git a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
index 35630ef..33fe02e 100644
--- a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
@@ -70,14 +70,6 @@
         }
     }
 
-    /** 5 */
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        // This class becomes active when the screen is locked.
-        // Rather than having it handle assistant visibility changes, the assistant visibility is
-        // set to zero prior to this class becoming active.
-    }
-
     /** 6 */
     @Override
     public BaseWindowInterface.AnimationFactory prepareRecentsUI(boolean activityVisible,
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
index ba3991f..7c6aa5b 100644
--- a/quickstep/src/com/android/quickstep/FocusState.kt
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -27,7 +27,10 @@
 class FocusState {
 
     var focusedDisplayId = DEFAULT_DISPLAY
-        private set
+        private set(value) {
+            field = value
+            listeners.forEach { it.onFocusedDisplayChanged(value) }
+        }
 
     private var listeners = mutableSetOf<FocusChangeListener>()
 
@@ -40,9 +43,7 @@
             transitions?.setFocusTransitionListener(
                 object : Stub() {
                     override fun onFocusedDisplayChanged(displayId: Int) {
-                        Executors.MAIN_EXECUTOR.execute {
-                            listeners.forEach { it.onFocusedDisplayChanged(displayId) }
-                        }
+                        Executors.MAIN_EXECUTOR.execute { focusedDisplayId = displayId }
                     }
                 }
             )
diff --git a/quickstep/src/com/android/quickstep/InputConsumerUtils.kt b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
index c340c92..1a82d3b 100644
--- a/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
+++ b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
@@ -18,7 +18,6 @@
 import android.content.Context
 import android.view.MotionEvent
 import androidx.annotation.VisibleForTesting
-import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.statemanager.BaseState
 import com.android.launcher3.statemanager.StatefulContainer
 import com.android.launcher3.taskbar.TaskbarManager
@@ -47,7 +46,6 @@
 import com.android.systemui.shared.system.InputMonitorCompat
 import com.android.wm.shell.Flags
 import java.util.function.Consumer
-import java.util.function.Function
 
 /** Utility class for creating input consumers. */
 object InputConsumerUtils {
@@ -68,7 +66,6 @@
         onCompleteCallback: Consumer<OtherActivityInputConsumer>,
         inputEventReceiver: InputChannelCompat.InputEventReceiver,
         taskbarManager: TaskbarManager,
-        swipeUpProxyProvider: Function<GestureState?, AnimatedFloat?>,
         overviewCommandHelper: OverviewCommandHelper,
         event: MotionEvent,
     ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
@@ -83,7 +80,7 @@
             )
             return consumer
         }
-        val progressProxy = swipeUpProxyProvider.apply(gestureState)
+        val progressProxy = deviceState.getSwipeUpProxy(gestureState)
         if (progressProxy != null) {
             val consumer: InputConsumer =
                 ProgressDelegateInputConsumer(
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index ac0aa76..2c37470 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -97,15 +97,6 @@
     }
 
     @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        QuickstepLauncher launcher = getCreatedContainer();
-        if (launcher == null) {
-            return;
-        }
-        launcher.onAssistantVisibilityChanged(visibility);
-    }
-
-    @Override
     public AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         notifyRecentsOfOrientation();
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 94d115b..42aa86e 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -28,6 +28,7 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
 import com.android.launcher3.Flags.enableFallbackOverviewInWindow
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableLauncherOverviewInWindow
@@ -38,6 +39,8 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarUIController
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.coroutines.DispatcherProvider
@@ -48,6 +51,7 @@
 import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
 import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
 import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
+import com.android.quickstep.fallback.window.RecentsDisplayModel
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.ActiveGestureProtoLogProxy
 import com.android.quickstep.views.RecentsView
@@ -72,6 +76,9 @@
     private val overviewComponentObserver: OverviewComponentObserver,
     private val taskAnimationManager: TaskAnimationManager,
     private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
+    private val recentsDisplayModel: RecentsDisplayModel,
+    private val focusState: FocusState,
+    private val taskbarManager: TaskbarManager,
 ) {
     private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.background)
 
@@ -291,15 +298,51 @@
                 deviceProfile != null &&
                 (deviceProfile.isTablet || deviceProfile.isTwoPanels)
 
+        val focusedDisplayId = focusState.focusedDisplayId
+        val focusedDisplayUIController: TaskbarUIController? =
+            if (RecentsDisplayModel.enableOverviewInWindow()) {
+                Log.d(
+                    TAG,
+                    "Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
+                )
+                recentsDisplayModel.getRecentsWindowManager(focusedDisplayId)?.taskbarUIController
+            } else {
+                Log.d(
+                    TAG,
+                    "Querying TaskbarManager for TaskbarUIController for display: $focusedDisplayId",
+                )
+                // TODO(b/395061396): Remove this path when overview in widow is enabled.
+                taskbarManager.getUIControllerForDisplay(focusedDisplayId)
+            }
+        Log.d(
+            TAG,
+            "TaskbarUIController for display $focusedDisplayId was" +
+                "${if (focusedDisplayUIController == null) " not" else ""} found",
+        )
+
         when (command.type) {
             HIDE -> {
                 if (!allowQuickSwitch) return true
-                keyboardTaskFocusIndex = uiController!!.launchFocusedTask()
+                keyboardTaskFocusIndex =
+                    if (
+                        enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+                    ) {
+                        focusedDisplayUIController.launchFocusedTask()
+                    } else {
+                        uiController!!.launchFocusedTask()
+                    }
+
                 if (keyboardTaskFocusIndex == -1) return true
             }
             KEYBOARD_INPUT ->
                 if (allowQuickSwitch) {
-                    uiController!!.openQuickSwitchView()
+                    if (
+                        enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+                    ) {
+                        focusedDisplayUIController.openQuickSwitchView()
+                    } else {
+                        uiController!!.openQuickSwitchView()
+                    }
                     return true
                 } else {
                     keyboardTaskFocusIndex = 0
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 1f95c41..a34b239 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -57,6 +57,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.LongConsumer;
 
 import javax.inject.Inject;
 
@@ -79,6 +80,7 @@
 
     private final Context mContext;
     private final RecentsDisplayModel mRecentsDisplayModel;
+    private final RecentsAnimationDeviceState mDeviceState;
 
     private final Intent mCurrentHomeIntent;
     private final Intent mMyHomeIntent;
@@ -100,9 +102,11 @@
     public OverviewComponentObserver(
             @ApplicationContext Context context,
             RecentsDisplayModel recentsDisplayModel,
+            RecentsAnimationDeviceState deviceState,
             DaggerSingletonTracker lifecycleTracker) {
         mContext = context;
         mRecentsDisplayModel = recentsDisplayModel;
+        mDeviceState = deviceState;
         mCurrentHomeIntent = createHomeIntent();
         mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName());
         ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0);
@@ -128,6 +132,12 @@
         updateOverviewTargets();
 
         lifecycleTracker.addCloseable(this::onDestroy);
+
+        setHomeDisabled(deviceState.isHomeDisabled());
+        LongConsumer flagChangeCallback = l -> setHomeDisabled(deviceState.isHomeDisabled());
+        deviceState.addSysUiFlagChangeCallback(flagChangeCallback);
+        lifecycleTracker.addCloseable(
+                () -> deviceState.removeSysUiFlagChangeCallback(flagChangeCallback));
     }
 
     /** Adds a listener for changes in {@link #isHomeAndOverviewSame()} */
@@ -140,11 +150,7 @@
         mOverviewChangeListeners.remove(overviewChangeListener);
     }
 
-    /**
-     * Called to set home enabled/disabled state via systemUI
-     * @param isHomeDisabled
-     */
-    public void setHomeDisabled(boolean isHomeDisabled) {
+    private void setHomeDisabled(boolean isHomeDisabled) {
         if (isHomeDisabled != mIsHomeDisabled) {
             mIsHomeDisabled = isHomeDisabled;
             updateOverviewTargets();
@@ -175,9 +181,7 @@
         // Set assistant visibility to 0 from launcher's perspective, ensures any elements that
         // launcher made invisible become visible again before the new activity control helper
         // becomes active.
-        if (mContainerInterface != null) {
-            mContainerInterface.onAssistantVisibilityChanged(0.f);
-        }
+        mDeviceState.setAssistantVisibility(0f);
 
         if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
             mIsDefaultHome = false;
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 9b0e75c..90221f3 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -172,7 +172,7 @@
                 return response;
 
             case TestProtocol.REQUEST_REFRESH_OVERVIEW_TARGET:
-                runOnTISBinder(TouchInteractionService.TISBinder::refreshOverviewTarget);
+                runOnTISBinder(TISBinder::refreshOverviewTarget);
                 return response;
 
             case TestProtocol.REQUEST_RECREATE_TASKBAR:
@@ -220,7 +220,7 @@
     }
 
     private void enableBlockingTimeout(
-            TouchInteractionService.TISBinder tisBinder, boolean enable) {
+            TISBinder tisBinder, boolean enable) {
         TaskbarActivityContext context = tisBinder.getTaskbarManager().getCurrentActivityContext();
         if (context == null) {
             return;
@@ -236,7 +236,7 @@
      * Runs the given command on the UI thread, after ensuring we are connected to
      * TouchInteractionService.
      */
-    protected void runOnTISBinder(Consumer<TouchInteractionService.TISBinder> connectionCallback) {
+    protected void runOnTISBinder(Consumer<TISBinder> connectionCallback) {
         try {
             CountDownLatch countDownLatch = new CountDownLatch(1);
             TISBindHelper helper = MAIN_EXECUTOR.submit(() ->
@@ -252,7 +252,7 @@
     }
 
     private <T> Bundle getTISBinderUIProperty(
-            BundleSetter<T> bundleSetter, Function<TouchInteractionService.TISBinder, T> provider) {
+            BundleSetter<T> bundleSetter, Function<TISBinder, T> provider) {
         Bundle response = new Bundle();
 
         runOnTISBinder(tisBinder -> bundleSetter.set(
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index fca67c3..760130e 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -165,7 +165,7 @@
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
     }
 
-    private void onTISConnected(TouchInteractionService.TISBinder binder) {
+    private void onTISConnected(TISBinder binder) {
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
             taskbarManager.setActivity(this);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 090ccdc..322e7c4 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -64,6 +64,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppSingleton;
@@ -88,6 +89,10 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
 import java.io.PrintWriter;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Function;
+import java.util.function.LongConsumer;
 
 import javax.inject.Inject;
 
@@ -118,12 +123,16 @@
     private final boolean mCanImeRenderGesturalNavButtons =
             InputMethodService.canImeRenderGesturalNavButtons();
 
+    private final List<LongConsumer> mFlagChangeCallbacks = new CopyOnWriteArrayList<>();
     private @SystemUiStateFlags long mSystemUiStateFlags = QuickStepContract.SYSUI_STATE_AWAKE;
     private NavigationMode mMode = THREE_BUTTONS;
     private NavBarPosition mNavBarPosition;
 
     private final Region mDeferredGestureRegion = new Region();
     private boolean mAssistantAvailable;
+
+    private final List<AssistantVisibilityChangeListener> mAssistantVisibilityChangeListeners =
+            new CopyOnWriteArrayList<>();
     private float mAssistantVisibility;
     private boolean mIsUserSetupComplete;
     private boolean mIsOneHandedModeEnabled;
@@ -136,6 +145,8 @@
     private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
     private boolean mExclusionListenerRegistered;
 
+    private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = null;
+
     @VisibleForTesting
     @Inject
     RecentsAnimationDeviceState(
@@ -355,8 +366,9 @@
     /**
      * Updates the system ui state flags from SystemUI.
      */
-    public void setSystemUiFlags(@SystemUiStateFlags long stateFlags) {
+    public void setSystemUiStateFlags(@SystemUiStateFlags long stateFlags) {
         mSystemUiStateFlags = stateFlags;
+        mFlagChangeCallbacks.forEach(c -> c.accept(stateFlags));
     }
 
     /**
@@ -369,6 +381,20 @@
     }
 
     /**
+     * Adds a callback to receiver sysui flag changes
+     */
+    public void addSysUiFlagChangeCallback(LongConsumer callback) {
+        mFlagChangeCallbacks.add(callback);
+    }
+
+    /**
+     * Removes a previously added sysui flag change callback
+     */
+    public void removeSysUiFlagChangeCallback(LongConsumer callback) {
+        mFlagChangeCallbacks.remove(callback);
+    }
+
+    /**
      * Sets the flag that indicates whether a predictive back-to-home animation is in progress
      */
     public void setPredictiveBackToHomeInProgress(boolean isInProgress) {
@@ -383,6 +409,22 @@
     }
 
     /**
+     * Sets or clears a function to proxy swipe up transition behavior
+     */
+    public void setSwipeUpProxyProvider(
+            @Nullable Function<GestureState, AnimatedFloat> swipeUpProxyProvider) {
+        mSwipeUpProxyProvider = swipeUpProxyProvider;
+    }
+
+    /**
+     * Returns a proxy animation for swipe up transition if a proxy function was previously set
+     */
+    public AnimatedFloat getSwipeUpProxy(GestureState state) {
+        Function<GestureState, AnimatedFloat> provider = mSwipeUpProxyProvider;
+        return provider != null ? provider.apply(state) : null;
+    }
+
+    /**
      * @return whether SystemUI is in a state where we can start a system gesture.
      */
     public boolean canStartSystemGesture() {
@@ -535,6 +577,8 @@
      */
     public void setAssistantVisibility(float visibility) {
         mAssistantVisibility = visibility;
+        mAssistantVisibilityChangeListeners.forEach(
+                l -> l.onAssistantVisibilityChanged(visibility));
     }
 
     /**
@@ -544,6 +588,16 @@
         return mAssistantVisibility;
     }
 
+    /** Add a listener for assistant visibility changes */
+    public void addAssistantVisibilityChangeListener(AssistantVisibilityChangeListener l) {
+        mAssistantVisibilityChangeListeners.add(l);
+    }
+
+    /** Removes a previously added visibility change listener */
+    public void removeAssistantVisibilityChangeListener(AssistantVisibilityChangeListener l) {
+        mAssistantVisibilityChangeListeners.remove(l);
+    }
+
     /**
      * @return whether the Assistant gesture can be used in 3 button navigation mode.
      */
@@ -633,6 +687,13 @@
         return  QuickStepContract.getSystemUiStateString(mSystemUiStateFlags);
     }
 
+    /** Interface for listening assistant visibility change */
+    @FunctionalInterface
+    public interface AssistantVisibilityChangeListener {
+        /** Called when assistant visibility changes */
+        void onAssistantVisibilityChanged(float visibility);
+    }
+
     public void dump(PrintWriter pw) {
         pw.println("DeviceState:");
         pw.println("  canStartSystemGesture=" + canStartSystemGesture());
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 87b58e6..1d83d42 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -38,13 +38,18 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.LockedUserState;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
 import com.android.quickstep.recents.data.TaskVisualsChangeNotifier;
 import com.android.quickstep.util.DesktopTask;
@@ -65,19 +70,22 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
-import javax.inject.Provider;
+import javax.inject.Inject;
+
+import dagger.Lazy;
 
 /**
  * Singleton class to load and manage recents model.
  */
 @TargetApi(Build.VERSION_CODES.O)
+@LauncherAppSingleton
 public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListener,
         TaskVisualsChangeListener, TaskVisualsChangeNotifier,
-        ThemeChangeListener, SafeCloseable {
+        ThemeChangeListener {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
-    public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
-            new MainThreadInitializedObject<>(RecentsModel::new);
+    public static final DaggerSingletonObject<RecentsModel> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getRecentsModel);
 
     private static final Executor RECENTS_MODEL_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("TaskThumbnailIconCache-", THREAD_PRIORITY_BACKGROUND));
@@ -85,53 +93,67 @@
     private final ConcurrentLinkedQueue<TaskVisualsChangeListener> mThumbnailChangeListeners =
             new ConcurrentLinkedQueue<>();
     private final Context mContext;
-
     private final RecentTasksList mTaskList;
     private final TaskIconCache mIconCache;
     private final TaskThumbnailCache mThumbnailCache;
-    private final ComponentCallbacks mCallbacks;
 
-    private final TaskStackChangeListeners mTaskStackChangeListeners;
-    private final SafeCloseable mIconChangeCloseable;
-
-    private final LockedUserState mLockedUserState;
-    private final Provider<ThemeManager> mThemeManagerProvider;
-    private final Runnable mUnlockCallback;
-
-    private RecentsModel(Context context) {
-        this(context, new IconProvider(context));
+    @Inject
+     public RecentsModel(@ApplicationContext Context context,
+            SystemUiProxy systemUiProxy,
+            TopTaskTracker topTaskTracker,
+            DisplayController displayController,
+            LockedUserState lockedUserState,
+            Lazy<ThemeManager> themeManagerLazy,
+            DaggerSingletonTracker tracker
+            ) {
+        // Lazily inject the ThemeManager and access themeManager once the device is
+        // unlocked. See b/393248495 for details.
+        this(context, new IconProvider(context), systemUiProxy, topTaskTracker,
+                displayController, lockedUserState,themeManagerLazy, tracker);
     }
 
     @SuppressLint("VisibleForTests")
-    private RecentsModel(Context context, IconProvider iconProvider) {
+    private RecentsModel(@ApplicationContext Context context,
+            IconProvider iconProvider,
+            SystemUiProxy systemUiProxy,
+            TopTaskTracker topTaskTracker,
+            DisplayController displayController,
+            LockedUserState lockedUserState,
+            Lazy<ThemeManager> themeManagerLazy,
+            DaggerSingletonTracker tracker) {
         this(context,
                 new RecentTasksList(
                         context,
                         MAIN_EXECUTOR,
                         context.getSystemService(KeyguardManager.class),
-                        SystemUiProxy.INSTANCE.get(context),
-                        TopTaskTracker.INSTANCE.get(context)),
-                new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider),
+                        systemUiProxy,
+                        topTaskTracker),
+                new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider, displayController),
                 new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
                 iconProvider,
                 TaskStackChangeListeners.getInstance(),
-                LockedUserState.get(context),
-                () -> ThemeManager.INSTANCE.get(context));
+                lockedUserState,
+                themeManagerLazy,
+                tracker);
     }
 
     @VisibleForTesting
-    RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
-            TaskThumbnailCache thumbnailCache, IconProvider iconProvider,
+    RecentsModel(@ApplicationContext Context context,
+            RecentTasksList taskList,
+            TaskIconCache iconCache,
+            TaskThumbnailCache thumbnailCache,
+            IconProvider iconProvider,
             TaskStackChangeListeners taskStackChangeListeners,
             LockedUserState lockedUserState,
-            Provider<ThemeManager> themeManagerProvider) {
+            Lazy<ThemeManager> themeManagerLazy,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mTaskList = taskList;
         mIconCache = iconCache;
         mIconCache.registerTaskVisualsChangeListener(this);
         mThumbnailCache = thumbnailCache;
         if (isCachePreloadingEnabled()) {
-            mCallbacks = new ComponentCallbacks() {
+            ComponentCallbacks componentCallbacks = new ComponentCallbacks() {
                 @Override
                 public void onConfigurationChanged(Configuration configuration) {
                     updateCacheSizeAndPreloadIfNeeded();
@@ -141,20 +163,27 @@
                 public void onLowMemory() {
                 }
             };
-            context.registerComponentCallbacks(mCallbacks);
-        } else {
-            mCallbacks = null;
+            context.registerComponentCallbacks(componentCallbacks);
+            tracker.addCloseable(() -> context.unregisterComponentCallbacks(componentCallbacks));
         }
 
-        mTaskStackChangeListeners = taskStackChangeListeners;
-        mTaskStackChangeListeners.registerTaskStackListener(this);
-        mIconChangeCloseable = iconProvider.registerIconChangeListener(
+        taskStackChangeListeners.registerTaskStackListener(this);
+        SafeCloseable iconChangeCloseable = iconProvider.registerIconChangeListener(
                 this::onAppIconChanged, MAIN_EXECUTOR.getHandler());
 
-        mLockedUserState = lockedUserState;
-        mThemeManagerProvider = themeManagerProvider;
-        mUnlockCallback = () -> mThemeManagerProvider.get().addChangeListener(this);
-        lockedUserState.runOnUserUnlocked(mUnlockCallback);
+        Runnable unlockCallback = () -> themeManagerLazy.get().addChangeListener(this);
+        lockedUserState.runOnUserUnlocked(unlockCallback);
+
+        tracker.addCloseable(() -> {
+            taskStackChangeListeners.unregisterTaskStackListener(this);
+            iconChangeCloseable.close();
+            mIconCache.removeTaskVisualsChangeListener();
+            if (lockedUserState.isUserUnlocked()) {
+                themeManagerLazy.get().removeChangeListener(this);
+            } else {
+                lockedUserState.removeOnUserUnlockedRunnable(unlockCallback);
+            }
+        });
     }
 
     public TaskIconCache getIconCache() {
@@ -407,22 +436,6 @@
         }
     }
 
-    @Override
-    public void close() {
-        if (mCallbacks != null) {
-            mContext.unregisterComponentCallbacks(mCallbacks);
-        }
-        mIconCache.removeTaskVisualsChangeListener();
-        mTaskStackChangeListeners.unregisterTaskStackListener(this);
-        mIconChangeCloseable.close();
-
-        if (mLockedUserState.isUserUnlocked()) {
-            mThemeManagerProvider.get().removeChangeListener(this);
-        } else {
-            mLockedUserState.removeOnUserUnlockedRunnable(mUnlockCallback);
-        }
-    }
-
     private boolean isCachePreloadingEnabled() {
         return enableGridOnlyOverview() || enableRefactorTaskThumbnail();
     }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d5382ad..32bdcdb 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -65,7 +65,6 @@
 import com.android.systemui.shared.recents.ISystemUiProxy
 import com.android.systemui.shared.recents.model.ThumbnailData.Companion.wrap
 import com.android.systemui.shared.system.QuickStepContract
-import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat
 import com.android.systemui.shared.system.RecentsAnimationListener
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
@@ -149,7 +148,7 @@
     private var backToLauncherRunner: IRemoteAnimationRunner? = null
     private var dragAndDrop: IDragAndDrop? = null
     val homeVisibilityState = HomeVisibilityState()
-    private val focusState = FocusState()
+    val focusState = FocusState()
 
     // Used to dedupe calls to SystemUI
     private var lastShelfHeight = 0
@@ -162,9 +161,6 @@
     private val asyncHandler =
         Handler(Executors.UI_HELPER_EXECUTOR.looper) { handleMessageAsync(it) }
 
-    // TODO(141886704): Find a way to remove this
-    @SystemUiStateFlags var lastSystemUiStateFlags: Long = 0
-
     /**
      * This is a singleton pending intent that is used to start recents via Shell (which is a
      * different process). It is bare-bones, so it's expected that the component and options will be
@@ -194,7 +190,7 @@
     private inline fun executeWithErrorLog(
         errorMsg: () -> String,
         tag: String = TAG,
-        callback: () -> Any?,
+        callback: () -> Unit,
     ) {
         try {
             callback.invoke()
@@ -232,36 +228,30 @@
      * Sets proxy state, including death linkage, various listeners, and other configuration objects
      */
     @MainThread
-    fun setProxy(
-        proxy: ISystemUiProxy?,
-        pip: IPip?,
-        bubbles: IBubbles?,
-        splitScreen: ISplitScreen?,
-        oneHanded: IOneHanded?,
-        shellTransitions: IShellTransitions?,
-        startingWindow: IStartingWindow?,
-        recentTasks: IRecentTasks?,
-        sysuiUnlockAnimationController: ISysuiUnlockAnimationController?,
-        backAnimation: IBackAnimation?,
-        desktopMode: IDesktopMode?,
-        unfoldAnimation: IUnfoldAnimation?,
-        dragAndDrop: IDragAndDrop?,
-    ) {
+    fun setProxy(bundle: Bundle) {
         Preconditions.assertUIThread()
         unlinkToDeath()
-        systemUiProxy = proxy
-        this.pip = pip
-        this.bubbles = bubbles
-        this.splitScreen = splitScreen
-        this.oneHanded = oneHanded
-        this.shellTransitions = shellTransitions
-        this.startingWindow = startingWindow
-        this.sysuiUnlockAnimationController = sysuiUnlockAnimationController
-        this.recentTasks = recentTasks
-        this.backAnimation = backAnimation
-        this.desktopMode = desktopMode
-        this.unfoldAnimation = if (Flags.enableUnfoldStateAnimation()) null else unfoldAnimation
-        this.dragAndDrop = dragAndDrop
+        systemUiProxy = ISystemUiProxy.Stub.asInterface(bundle.getBinder(ISystemUiProxy.DESCRIPTOR))
+        pip = IPip.Stub.asInterface(bundle.getBinder(IPip.DESCRIPTOR))
+        bubbles = IBubbles.Stub.asInterface(bundle.getBinder(IBubbles.DESCRIPTOR))
+        splitScreen = ISplitScreen.Stub.asInterface(bundle.getBinder(ISplitScreen.DESCRIPTOR))
+        oneHanded = IOneHanded.Stub.asInterface(bundle.getBinder(IOneHanded.DESCRIPTOR))
+        shellTransitions =
+            IShellTransitions.Stub.asInterface(bundle.getBinder(IShellTransitions.DESCRIPTOR))
+        startingWindow =
+            IStartingWindow.Stub.asInterface(bundle.getBinder(IStartingWindow.DESCRIPTOR))
+        sysuiUnlockAnimationController =
+            ISysuiUnlockAnimationController.Stub.asInterface(
+                bundle.getBinder(ISysuiUnlockAnimationController.DESCRIPTOR)
+            )
+        recentTasks = IRecentTasks.Stub.asInterface(bundle.getBinder(IRecentTasks.DESCRIPTOR))
+        backAnimation = IBackAnimation.Stub.asInterface(bundle.getBinder(IBackAnimation.DESCRIPTOR))
+        desktopMode = IDesktopMode.Stub.asInterface(bundle.getBinder(IDesktopMode.DESCRIPTOR))
+        unfoldAnimation =
+            if (Flags.enableUnfoldStateAnimation()) null
+            else IUnfoldAnimation.Stub.asInterface(bundle.getBinder(IUnfoldAnimation.DESCRIPTOR))
+        dragAndDrop = IDragAndDrop.Stub.asInterface(bundle.getBinder(IDragAndDrop.DESCRIPTOR))
+
         linkToDeath()
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
         setPipAnimationListener(pipAnimationListener)
@@ -289,15 +279,12 @@
         stateChangeCallbacks.forEach { it.run() }
 
         if (unfoldTransitionProvider != null) {
-            if (unfoldAnimation != null) {
-                try {
-                    unfoldAnimation.setListener(unfoldTransitionProvider)
+            unfoldTransitionProvider.isActive = false
+            executeWithErrorLog({ "Failed to set unfold anim listener" }) {
+                unfoldAnimation?.apply {
+                    setListener(unfoldTransitionProvider)
                     unfoldTransitionProvider.isActive = true
-                } catch (e: RemoteException) {
-                    // Ignore
                 }
-            } else {
-                unfoldTransitionProvider.isActive = false
             }
         }
     }
@@ -305,9 +292,7 @@
     /**
      * Clear the proxy to release held resources and turn the majority of its operations into no-ops
      */
-    @MainThread
-    fun clearProxy() =
-        setProxy(null, null, null, null, null, null, null, null, null, null, null, null, null)
+    @MainThread fun clearProxy() = setProxy(Bundle.EMPTY)
 
     /** Adds a callback to be notified whenever the active state changes */
     fun addOnStateChangeListener(callback: Runnable) = stateChangeCallbacks.add(callback)
diff --git a/quickstep/src/com/android/quickstep/TISBinder.kt b/quickstep/src/com/android/quickstep/TISBinder.kt
new file mode 100644
index 0000000..683012c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TISBinder.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep
+
+import android.graphics.Region
+import android.os.Bundle
+import android.os.IRemoteCallback
+import android.os.RemoteException
+import android.util.Log
+import androidx.annotation.BinderThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
+import com.android.launcher3.util.Executors
+import com.android.quickstep.OverviewCommandHelper.CommandType.HIDE
+import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
+import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
+import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
+import com.android.quickstep.util.ActivityPreloadUtil.preloadOverviewForTIS
+import com.android.quickstep.util.ContextualSearchInvoker
+import com.android.systemui.shared.recents.ILauncherProxy
+import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode
+import com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags
+import java.lang.ref.WeakReference
+
+/** Local ILauncherProxy implementation with some methods for local components */
+private const val TAG = "TISBinder"
+
+class TISBinder internal constructor(tis: TouchInteractionService) : ILauncherProxy.Stub() {
+
+    private val mTis = WeakReference(tis)
+
+    /** Returns the [TaskbarManager] or `null` if TouchInteractionService is not connected */
+    val taskbarManager: TaskbarManager?
+        get() = mTis.get()?.taskbarManager
+
+    /** Returns the [OverviewCommandHelper] or `null` if TouchInteractionService is not connected */
+    val overviewCommandHelper: OverviewCommandHelper?
+        get() = mTis.get()?.overviewCommandHelper
+
+    private val deviceState: RecentsAnimationDeviceState?
+        get() = mTis.get()?.let { RecentsAnimationDeviceState.INSTANCE[it] }
+
+    private inline fun executeForTaskbarManagerOnMain(
+        crossinline callback: TaskbarManager.() -> Unit
+    ) {
+        Executors.MAIN_EXECUTOR.execute { taskbarManager?.let { callback.invoke(it) } }
+    }
+
+    @BinderThread
+    override fun onInitialize(bundle: Bundle) {
+        Executors.MAIN_EXECUTOR.execute {
+            mTis.get()?.let {
+                SystemUiProxy.INSTANCE[it].setProxy(bundle)
+                preloadOverviewForTIS(it, true /* fromInit */)
+            }
+        }
+    }
+
+    @BinderThread
+    override fun onTaskbarToggled() {
+        executeForTaskbarManagerOnMain { currentActivityContext?.toggleTaskbarStash() }
+    }
+
+    @BinderThread
+    override fun onOverviewToggle() {
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle")
+        mTis.get()?.let { tis ->
+            // If currently screen pinning, do not enter overview
+            if (RecentsAnimationDeviceState.INSTANCE[tis].isScreenPinningActive) {
+                return@let
+            }
+            TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS)
+            tis.overviewCommandHelper.addCommand(TOGGLE)
+        }
+    }
+
+    @BinderThread
+    override fun onOverviewShown(triggeredFromAltTab: Boolean) {
+        overviewCommandHelper?.apply {
+            if (triggeredFromAltTab) {
+                TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS)
+                addCommand(KEYBOARD_INPUT)
+            } else {
+                addCommand(SHOW)
+            }
+        }
+    }
+
+    @BinderThread
+    override fun onOverviewHidden(triggeredFromAltTab: Boolean, triggeredFromHomeKey: Boolean) {
+        overviewCommandHelper?.apply {
+            if (triggeredFromAltTab && !triggeredFromHomeKey) {
+                // onOverviewShownFromAltTab hides the overview and ends at the target app
+                addCommand(HIDE)
+            }
+        }
+    }
+
+    @BinderThread
+    override fun onAssistantAvailable(available: Boolean, longPressHomeEnabled: Boolean) {
+        Executors.MAIN_EXECUTOR.execute {
+            deviceState?.setAssistantAvailable(available)
+            taskbarManager?.onLongPressHomeEnabled(longPressHomeEnabled)
+        }
+    }
+
+    @BinderThread
+    override fun onAssistantVisibilityChanged(visibility: Float) {
+        Executors.MAIN_EXECUTOR.execute { deviceState?.assistantVisibility = visibility }
+    }
+
+    /**
+     * Sent when the assistant has been invoked with the given type (defined in AssistManager) and
+     * should be shown. This method is used if SystemUiProxy#setAssistantOverridesRequested was
+     * previously called including this invocation type.
+     */
+    override fun onAssistantOverrideInvoked(invocationType: Int) {
+        mTis.get()?.let { tis ->
+            if (!ContextualSearchInvoker(tis).tryStartAssistOverride(invocationType)) {
+                Log.w(TAG, "Failed to invoke Assist override")
+            }
+        }
+    }
+
+    @BinderThread
+    override fun onSystemUiStateChanged(@SystemUiStateFlags stateFlags: Long) {
+        Executors.MAIN_EXECUTOR.execute { deviceState?.systemUiStateFlags = stateFlags }
+    }
+
+    @BinderThread
+    override fun onActiveNavBarRegionChanges(region: Region) {
+        Executors.MAIN_EXECUTOR.execute { deviceState?.setDeferredGestureRegion(region) }
+    }
+
+    @BinderThread
+    override fun enterStageSplitFromRunningApp(leftOrTop: Boolean) {
+        mTis.get()?.let { tis ->
+            OverviewComponentObserver.INSTANCE[tis]
+                .containerInterface
+                .createdContainer
+                ?.enterStageSplitFromRunningApp(leftOrTop)
+        }
+    }
+
+    @BinderThread
+    override fun onDisplayAddSystemDecorations(displayId: Int) {
+        executeForTaskbarManagerOnMain { onDisplayAddSystemDecorations(displayId) }
+    }
+
+    @BinderThread
+    override fun onDisplayRemoved(displayId: Int) {
+        executeForTaskbarManagerOnMain { onDisplayRemoved(displayId) }
+    }
+
+    @BinderThread
+    override fun onDisplayRemoveSystemDecorations(displayId: Int) {
+        executeForTaskbarManagerOnMain { onDisplayRemoveSystemDecorations(displayId) }
+    }
+
+    @BinderThread
+    override fun updateWallpaperVisibility(displayId: Int, visible: Boolean) {
+        executeForTaskbarManagerOnMain { setWallpaperVisible(displayId, visible) }
+    }
+
+    @BinderThread
+    override fun checkNavBarModes(displayId: Int) {
+        executeForTaskbarManagerOnMain { checkNavBarModes(displayId) }
+    }
+
+    @BinderThread
+    override fun finishBarAnimations(displayId: Int) {
+        executeForTaskbarManagerOnMain { finishBarAnimations(displayId) }
+    }
+
+    @BinderThread
+    override fun touchAutoDim(displayId: Int, reset: Boolean) {
+        executeForTaskbarManagerOnMain { touchAutoDim(displayId, reset) }
+    }
+
+    @BinderThread
+    override fun transitionTo(displayId: Int, @TransitionMode barMode: Int, animate: Boolean) {
+        executeForTaskbarManagerOnMain { transitionTo(displayId, barMode, animate) }
+    }
+
+    @BinderThread
+    override fun appTransitionPending(pending: Boolean) {
+        executeForTaskbarManagerOnMain { appTransitionPending(pending) }
+    }
+
+    override fun onRotationProposal(rotation: Int, isValid: Boolean) {
+        executeForTaskbarManagerOnMain { onRotationProposal(rotation, isValid) }
+    }
+
+    override fun disable(displayId: Int, state1: Int, state2: Int, animate: Boolean) {
+        executeForTaskbarManagerOnMain { disableNavBarElements(displayId, state1, state2, animate) }
+    }
+
+    override fun onSystemBarAttributesChanged(displayId: Int, behavior: Int) {
+        executeForTaskbarManagerOnMain { onSystemBarAttributesChanged(displayId, behavior) }
+    }
+
+    override fun onTransitionModeUpdated(barMode: Int, checkBarModes: Boolean) {
+        executeForTaskbarManagerOnMain { onTransitionModeUpdated(barMode, checkBarModes) }
+    }
+
+    override fun onNavButtonsDarkIntensityChanged(darkIntensity: Float) {
+        executeForTaskbarManagerOnMain { onNavButtonsDarkIntensityChanged(darkIntensity) }
+    }
+
+    override fun onNavigationBarLumaSamplingEnabled(displayId: Int, enable: Boolean) {
+        executeForTaskbarManagerOnMain { onNavigationBarLumaSamplingEnabled(displayId, enable) }
+    }
+
+    override fun onUnbind(reply: IRemoteCallback) {
+        // Run everything in the same main thread block to ensure the cleanup happens before
+        // sending the reply.
+        Executors.MAIN_EXECUTOR.execute {
+            taskbarManager?.destroy()
+            try {
+                reply.sendResult(null)
+            } catch (e: RemoteException) {
+                Log.w(TAG, "onUnbind: Failed to reply to LauncherProxyService", e)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    fun injectFakeTrackpadForTesting() = mTis.get()?.injectFakeTrackpadForTesting()
+
+    @VisibleForTesting fun ejectFakeTrackpadForTesting() = mTis.get()?.ejectFakeTrackpadForTesting()
+
+    @VisibleForTesting fun refreshOverviewTarget() = mTis.get()?.refreshOverviewTarget()
+}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 63b8aaf..e449048 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -54,22 +54,25 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
+import java.util.function.LongConsumer;
 
 public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
     public static final boolean SHELL_TRANSITIONS_ROTATION =
             SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
 
     private final Context mCtx;
+    private final RecentsAnimationDeviceState mDeviceState;
+    private final SystemUiProxy mSystemUiProxy;
     private RecentsAnimationController mController;
     private RecentsAnimationCallbacks mCallbacks;
     private RecentsAnimationTargets mTargets;
     private TransitionInfo mTransitionInfo;
-    private RecentsAnimationDeviceState mDeviceState;
 
     // Temporary until we can hook into gesture state events
     private GestureState mLastGestureState;
@@ -102,12 +105,22 @@
         }
     };
 
+
+    private @SystemUiStateFlags long mLastSysuiFlags;
+    private final LongConsumer mSysUiFlagChangeCallback = this::onSystemUiFlagsChanged;
+
     TaskAnimationManager(Context ctx, RecentsAnimationDeviceState deviceState) {
+        this(ctx, deviceState, SystemUiProxy.INSTANCE.get(ctx));
+    }
+
+    TaskAnimationManager(Context ctx, RecentsAnimationDeviceState deviceState,
+            SystemUiProxy systemUiProxy) {
         mCtx = ctx;
         mDeviceState = deviceState;
-    }
-    SystemUiProxy getSystemUiProxy() {
-        return SystemUiProxy.INSTANCE.get(mCtx);
+        mSystemUiProxy = systemUiProxy;
+        mLastSysuiFlags = QuickStepContract.SYSUI_STATE_AWAKE;
+        mDeviceState.addSysUiFlagChangeCallback(mSysUiFlagChangeCallback);
+        onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags());
     }
 
     boolean shouldIgnoreMotionEvents() {
@@ -151,7 +164,7 @@
 
         final BaseContainerInterface containerInterface = gestureState.getContainerInterface();
         mLastGestureState = gestureState;
-        RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(getSystemUiProxy());
+        RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(mSystemUiProxy);
         mCallbacks = newCallbacks;
         mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
             @Override
@@ -289,7 +302,7 @@
         if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
                 && (Flags.enableFallbackOverviewInWindow()
                         || Flags.enableLauncherOverviewInWindow())) {
-            mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
+            mRecentsAnimationStartPending = mSystemUiProxy.startRecentsActivity(intent, options,
                     mCallbacks, gestureState.useSyntheticRecentsTransition());
             RecentsDisplayModel.getINSTANCE().get(mCtx)
                     .getRecentsWindowManager(mDeviceState.getDisplayId())
@@ -321,7 +334,7 @@
                 });
             }
 
-            mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
+            mRecentsAnimationStartPending = mSystemUiProxy.startRecentsActivity(intent,
                     options, mCallbacks, false /* useSyntheticRecentsTransition */);
         }
 
@@ -347,8 +360,13 @@
         return mCallbacks;
     }
 
-    public void onSystemUiFlagsChanged(@QuickStepContract.SystemUiStateFlags long lastSysUIFlags,
-            @QuickStepContract.SystemUiStateFlags long newSysUIFlags) {
+    private void onSystemUiFlagsChanged(@SystemUiStateFlags long newSysUIFlags) {
+        onSystemUiFlagsChanged(mLastSysuiFlags, newSysUIFlags);
+        mLastSysuiFlags = newSysUIFlags;
+    }
+
+    private void onSystemUiFlagsChanged(@SystemUiStateFlags long lastSysUIFlags,
+            @SystemUiStateFlags long newSysUIFlags) {
         long isShadeExpandedFlagMask =
                 SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
         boolean wasExpanded = hasAnyFlag(lastSysUIFlags, isShadeExpandedFlagMask);
@@ -509,4 +527,8 @@
             mLastGestureState.dump(prefix + '\t', pw);
         }
     }
+
+    public void onDestroy() {
+        mDeviceState.removeSysUiFlagChangeCallback(mSysUiFlagChangeCallback);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index b82c110..6a7f1af 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.kt
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -53,6 +53,7 @@
     private val context: Context,
     private val bgExecutor: Executor,
     private val iconProvider: IconProvider,
+    displayController: DisplayController,
 ) : TaskIconDataSource, DisplayInfoChangeListener {
     private val iconCache =
         TaskKeyLruCache<TaskCacheEntry>(
@@ -71,7 +72,7 @@
     var taskVisualsChangeListener: TaskVisualsChangeListener? = null
 
     init {
-        DisplayController.INSTANCE.get(context).addChangeListener(this)
+        displayController.addChangeListener(this)
     }
 
     override fun onDisplayInfoChanged(context: Context, info: DisplayController.Info, flags: Int) {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2df4a45..22fc2ea 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -36,7 +36,6 @@
 import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.InputConsumerUtils.newConsumer;
 import static com.android.quickstep.InputConsumerUtils.tryCreateAssistantInputConsumer;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.app.PendingIntent;
 import android.app.Service;
@@ -44,12 +43,9 @@
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.res.Configuration;
-import android.graphics.Region;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.os.IRemoteCallback;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.Choreographer;
@@ -57,7 +53,6 @@
 import android.view.InputEvent;
 import android.view.MotionEvent;
 
-import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -67,7 +62,6 @@
 import com.android.launcher3.EncryptionType;
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -88,6 +82,7 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.OverviewCommandHelper.CommandType;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -97,34 +92,14 @@
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.ActiveTrackpadList;
 import com.android.quickstep.util.ActivityPreloadUtil;
-import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.views.RecentsViewContainer;
-import com.android.systemui.shared.recents.ILauncherProxy;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
-import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
-import com.android.systemui.unfold.progress.IUnfoldAnimation;
-import com.android.wm.shell.back.IBackAnimation;
-import com.android.wm.shell.bubbles.IBubbles;
-import com.android.wm.shell.common.pip.IPip;
-import com.android.wm.shell.desktopmode.IDesktopMode;
-import com.android.wm.shell.draganddrop.IDragAndDrop;
-import com.android.wm.shell.onehanded.IOneHanded;
-import com.android.wm.shell.recents.IRecentTasks;
-import com.android.wm.shell.shared.IShellTransitions;
-import com.android.wm.shell.splitscreen.ISplitScreen;
-import com.android.wm.shell.startingsurface.IStartingWindow;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.function.Consumer;
-import java.util.function.Function;
 
 /**
  * Service connected by system-UI for handling touch interaction.
@@ -140,371 +115,6 @@
 
     private final TISBinder mTISBinder = new TISBinder(this);
 
-    /**
-     * Local ILauncherProxy implementation with some methods for local components
-     */
-    public static class TISBinder extends ILauncherProxy.Stub {
-
-        private final WeakReference<TouchInteractionService> mTis;
-
-        private TISBinder(TouchInteractionService tis) {
-            mTis = new WeakReference<>(tis);
-        }
-
-        @BinderThread
-        public void onInitialize(Bundle bundle) {
-            ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
-                    bundle.getBinder(ISystemUiProxy.DESCRIPTOR));
-            IPip pip = IPip.Stub.asInterface(bundle.getBinder(IPip.DESCRIPTOR));
-            IBubbles bubbles = IBubbles.Stub.asInterface(bundle.getBinder(IBubbles.DESCRIPTOR));
-            ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
-                    ISplitScreen.DESCRIPTOR));
-            IOneHanded onehanded = IOneHanded.Stub.asInterface(
-                    bundle.getBinder(IOneHanded.DESCRIPTOR));
-            IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
-                    bundle.getBinder(IShellTransitions.DESCRIPTOR));
-            IStartingWindow startingWindow = IStartingWindow.Stub.asInterface(
-                    bundle.getBinder(IStartingWindow.DESCRIPTOR));
-            ISysuiUnlockAnimationController launcherUnlockAnimationController =
-                    ISysuiUnlockAnimationController.Stub.asInterface(
-                            bundle.getBinder(ISysuiUnlockAnimationController.DESCRIPTOR));
-            IRecentTasks recentTasks = IRecentTasks.Stub.asInterface(
-                    bundle.getBinder(IRecentTasks.DESCRIPTOR));
-            IBackAnimation backAnimation = IBackAnimation.Stub.asInterface(
-                    bundle.getBinder(IBackAnimation.DESCRIPTOR));
-            IDesktopMode desktopMode = IDesktopMode.Stub.asInterface(
-                    bundle.getBinder(IDesktopMode.DESCRIPTOR));
-            IUnfoldAnimation unfoldTransition = IUnfoldAnimation.Stub.asInterface(
-                    bundle.getBinder(IUnfoldAnimation.DESCRIPTOR));
-            IDragAndDrop dragAndDrop = IDragAndDrop.Stub.asInterface(
-                    bundle.getBinder(IDragAndDrop.DESCRIPTOR));
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                SystemUiProxy.INSTANCE.get(tis).setProxy(proxy, pip,
-                        bubbles, splitscreen, onehanded, shellTransitions, startingWindow,
-                        recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
-                        unfoldTransition, dragAndDrop);
-                tis.initInputMonitor("TISBinder#onInitialize()");
-                ActivityPreloadUtil.preloadOverviewForTIS(tis, true /* fromInit */);
-            }));
-        }
-
-        @BinderThread
-        @Override
-        public void onTaskbarToggled() {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                TaskbarActivityContext activityContext =
-                        tis.mTaskbarManager.getCurrentActivityContext();
-
-                if (activityContext != null) {
-                    activityContext.toggleTaskbarStash();
-                }
-            }));
-        }
-
-        @BinderThread
-        public void onOverviewToggle() {
-            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
-            executeForTouchInteractionService(tis -> {
-                // If currently screen pinning, do not enter overview
-                if (tis.mDeviceState.isScreenPinningActive()) {
-                    return;
-                }
-                TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                tis.mOverviewCommandHelper.addCommand(CommandType.TOGGLE);
-            });
-        }
-
-        @BinderThread
-        @Override
-        public void onOverviewShown(boolean triggeredFromAltTab) {
-            executeForTouchInteractionService(tis -> {
-                if (triggeredFromAltTab) {
-                    TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                    tis.mOverviewCommandHelper.addCommand(CommandType.KEYBOARD_INPUT);
-                } else {
-                    tis.mOverviewCommandHelper.addCommand(CommandType.SHOW);
-                }
-            });
-        }
-
-        @BinderThread
-        @Override
-        public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
-            executeForTouchInteractionService(tis -> {
-                if (triggeredFromAltTab && !triggeredFromHomeKey) {
-                    // onOverviewShownFromAltTab hides the overview and ends at the target app
-                    tis.mOverviewCommandHelper.addCommand(CommandType.HIDE);
-                }
-            });
-        }
-
-        @BinderThread
-        @Override
-        public void onAssistantAvailable(boolean available, boolean longPressHomeEnabled) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                tis.mDeviceState.setAssistantAvailable(available);
-                tis.onAssistantVisibilityChanged();
-                executeForTaskbarManager(taskbarManager -> taskbarManager
-                        .onLongPressHomeEnabled(longPressHomeEnabled));
-            }));
-        }
-
-        @BinderThread
-        @Override
-        public void onAssistantVisibilityChanged(float visibility) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                tis.mDeviceState.setAssistantVisibility(visibility);
-                tis.onAssistantVisibilityChanged();
-            }));
-        }
-
-        /**
-         * Sent when the assistant has been invoked with the given type (defined in AssistManager)
-         * and should be shown. This method is used if SystemUiProxy#setAssistantOverridesRequested
-         * was previously called including this invocation type.
-         */
-        @Override
-        public void onAssistantOverrideInvoked(int invocationType) {
-            executeForTouchInteractionService(tis -> {
-                if (!new ContextualSearchInvoker(tis).tryStartAssistOverride(invocationType)) {
-                    Log.w(TAG, "Failed to invoke Assist override");
-                }
-            });
-        }
-
-        @BinderThread
-        public void onSystemUiStateChanged(@SystemUiStateFlags long stateFlags) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                long lastFlags = tis.mDeviceState.getSystemUiStateFlags();
-                tis.mDeviceState.setSystemUiFlags(stateFlags);
-                tis.onSystemUiFlagsChanged(lastFlags);
-            }));
-        }
-
-        @BinderThread
-        public void onActiveNavBarRegionChanges(Region region) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(
-                    tis -> tis.mDeviceState.setDeferredGestureRegion(region)));
-        }
-
-        @BinderThread
-        @Override
-        public void enterStageSplitFromRunningApp(boolean leftOrTop) {
-            executeForTouchInteractionService(tis -> {
-                RecentsViewContainer container = tis.mOverviewComponentObserver
-                        .getContainerInterface().getCreatedContainer();
-                if (container != null) {
-                    container.enterStageSplitFromRunningApp(leftOrTop);
-                }
-            });
-        }
-
-        @BinderThread
-        @Override
-        public void onDisplayAddSystemDecorations(int displayId) {
-            executeForTaskbarManager(taskbarManager ->
-                            taskbarManager.onDisplayAddSystemDecorations(displayId));
-        }
-
-        @BinderThread
-        @Override
-        public void onDisplayRemoved(int displayId) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onDisplayRemoved(displayId));
-        }
-
-        @BinderThread
-        @Override
-        public void onDisplayRemoveSystemDecorations(int displayId) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onDisplayRemoveSystemDecorations(displayId));
-        }
-
-        @BinderThread
-        @Override
-        public void updateWallpaperVisibility(int displayId, boolean visible) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.setWallpaperVisible(displayId, visible));
-        }
-
-        @BinderThread
-        @Override
-        public void checkNavBarModes(int displayId) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.checkNavBarModes(displayId));
-        }
-
-        @BinderThread
-        @Override
-        public void finishBarAnimations(int displayId) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.finishBarAnimations(displayId));
-        }
-
-        @BinderThread
-        @Override
-        public void touchAutoDim(int displayId, boolean reset) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.touchAutoDim(displayId, reset));
-        }
-
-        @BinderThread
-        @Override
-        public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
-                boolean animate) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.transitionTo(displayId, barMode, animate));
-        }
-
-        @BinderThread
-        @Override
-        public void appTransitionPending(boolean pending) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.appTransitionPending(pending));
-        }
-
-        @Override
-        public void onRotationProposal(int rotation, boolean isValid) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onRotationProposal(rotation, isValid));
-        }
-
-        @Override
-        public void disable(int displayId, int state1, int state2, boolean animate) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.disableNavBarElements(displayId, state1, state2, animate));
-        }
-
-        @Override
-        public void onSystemBarAttributesChanged(int displayId, int behavior) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onSystemBarAttributesChanged(displayId, behavior));
-        }
-
-        @Override
-        public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onTransitionModeUpdated(barMode, checkBarModes));
-        }
-
-        @Override
-        public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onNavButtonsDarkIntensityChanged(darkIntensity));
-        }
-
-        @Override
-        public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
-            executeForTaskbarManager(taskbarManager ->
-                    taskbarManager.onNavigationBarLumaSamplingEnabled(displayId, enable));
-        }
-
-        @Override
-        public void onUnbind(IRemoteCallback reply) {
-            // Run everything in the same main thread block to ensure the cleanup happens before
-            // sending the reply.
-            MAIN_EXECUTOR.execute(() -> {
-                executeForTaskbarManager(TaskbarManager::destroy);
-                try {
-                    reply.sendResult(null);
-                } catch (RemoteException e) {
-                    Log.w(TAG, "onUnbind: Failed to reply to LauncherProxyService", e);
-                }
-            });
-        }
-
-        private void executeForTouchInteractionService(
-                @NonNull Consumer<TouchInteractionService> tisConsumer) {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return;
-            tisConsumer.accept(tis);
-        }
-
-        private void executeForTaskbarManager(
-                @NonNull Consumer<TaskbarManager> taskbarManagerConsumer) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
-                TaskbarManager taskbarManager = tis.mTaskbarManager;
-                if (taskbarManager == null) return;
-                taskbarManagerConsumer.accept(taskbarManager);
-            }));
-        }
-
-        /**
-         * Returns the {@link TaskbarManager}.
-         * <p>
-         * Returns {@code null} if TouchInteractionService is not connected
-         */
-        @Nullable
-        public TaskbarManager getTaskbarManager() {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return null;
-            return tis.mTaskbarManager;
-        }
-
-        @VisibleForTesting
-        public void injectFakeTrackpadForTesting() {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return;
-            tis.mTrackpadsConnected.add(1000);
-            tis.initInputMonitor("tapl testing");
-        }
-
-        @VisibleForTesting
-        public void ejectFakeTrackpadForTesting() {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return;
-            tis.mTrackpadsConnected.clear();
-            // This method destroys the current input monitor if set up, and only init a new one
-            // in 3-button mode if {@code mTrackpadsConnected} is not empty. So in other words,
-            // it will destroy the input monitor.
-            tis.initInputMonitor("tapl testing");
-        }
-
-        /**
-         * Sets whether a predictive back-to-home animation is in progress in the device state
-         */
-        public void setPredictiveBackToHomeInProgress(boolean isInProgress) {
-            executeForTouchInteractionService(tis ->
-                    tis.mDeviceState.setPredictiveBackToHomeInProgress(isInProgress));
-        }
-
-        /**
-         * Returns the {@link OverviewCommandHelper}.
-         * <p>
-         * Returns {@code null} if TouchInteractionService is not connected
-         */
-        @Nullable
-        public OverviewCommandHelper getOverviewCommandHelper() {
-            TouchInteractionService tis = mTis.get();
-            if (tis == null) return null;
-            return tis.mOverviewCommandHelper;
-        }
-
-        /**
-         * Sets a proxy to bypass swipe up behavior
-         */
-        public void setSwipeUpProxy(Function<GestureState, AnimatedFloat> proxy) {
-            executeForTouchInteractionService(
-                    tis -> tis.mSwipeUpProxyProvider = proxy != null ? proxy : (i -> null));
-        }
-
-        /**
-         * Sets the task id where gestures should be blocked
-         */
-        public void setGestureBlockedTaskId(int taskId) {
-            executeForTouchInteractionService(
-                    tis -> tis.mDeviceState.setGestureBlockingTaskId(taskId));
-        }
-
-        /** Refreshes the current overview target. */
-        public void refreshOverviewTarget() {
-            executeForTouchInteractionService(tis -> {
-                tis.mAllAppsActionManager.onDestroy();
-                tis.onOverviewTargetChanged(tis.mOverviewComponentObserver.isHomeAndOverviewSame());
-            });
-        }
-    }
-
     private RotationTouchHelper mRotationTouchHelper;
 
     private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
@@ -520,6 +130,13 @@
     private final ScreenOnTracker.ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
     private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChanged;
 
+    private final Runnable mSysUiProxyStateChangeCallback =
+            () -> {
+                if (SystemUiProxy.INSTANCE.get(this).isActive()) {
+                    initInputMonitor("TISBinder#onInitialize()");
+                }
+            };
+
     private final TaskbarNavButtonCallbacks mNavCallbacks = new TaskbarNavButtonCallbacks() {
         @Override
         public void onNavigateHome() {
@@ -553,7 +170,6 @@
     private InputEventReceiver mInputEventReceiver;
 
     private TaskbarManager mTaskbarManager;
-    private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
     private AllAppsActionManager mAllAppsActionManager;
     private ActiveTrackpadList mTrackpadsConnected;
 
@@ -584,7 +200,8 @@
             initInputMonitor("onTrackpadConnected()");
         });
 
-        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
+        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks,
+                RecentsDisplayModel.getINSTANCE().get(this));
         mDesktopAppLaunchTransitionManager =
                 new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
         mDesktopAppLaunchTransitionManager.registerTransitions();
@@ -595,6 +212,7 @@
         mDisplayInfoChangeListener =
                 mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         ScreenOnTracker.INSTANCE.get(this).addListener(mScreenOnListener);
+        SystemUiProxy.INSTANCE.get(this).addOnStateChangeListener(mSysUiProxyStateChangeCallback);
     }
 
     private void disposeEventHandlers(String reason) {
@@ -641,12 +259,12 @@
         mTaskAnimationManager = new TaskAnimationManager(this, mDeviceState);
         mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(this);
         mOverviewCommandHelper = new OverviewCommandHelper(this,
-                mOverviewComponentObserver, mTaskAnimationManager);
+                mOverviewComponentObserver, mTaskAnimationManager,
+                RecentsDisplayModel.getINSTANCE().get(this),
+                SystemUiProxy.INSTANCE.get(this).getFocusState(), mTaskbarManager);
         mResetGestureInputConsumer = new ResetGestureInputConsumer(
                 mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext);
         mInputConsumer.registerInputConsumer();
-        onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags());
-        onAssistantVisibilityChanged();
 
         // Initialize the task tracker
         TopTaskTracker.INSTANCE.get(this);
@@ -665,6 +283,10 @@
         return mOverviewCommandHelper;
     }
 
+    public TaskbarManager getTaskbarManager() {
+        return mTaskbarManager;
+    }
+
     private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
         if (!LockedUserState.get(this).isUserUnlocked() || mDeviceState.isButtonNavMode()) {
             // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
@@ -706,34 +328,18 @@
         });
     }
 
-    @UiThread
-    private void onSystemUiFlagsChanged(@SystemUiStateFlags long lastSysUIFlags) {
-        if (LockedUserState.get(this).isUserUnlocked()) {
-            long systemUiStateFlags = mDeviceState.getSystemUiStateFlags();
-            SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags);
-            mOverviewComponentObserver.setHomeDisabled(mDeviceState.isHomeDisabled());
-            mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags);
-            mTaskAnimationManager.onSystemUiFlagsChanged(lastSysUIFlags, systemUiStateFlags);
-        }
-    }
-
-    @UiThread
-    private void onAssistantVisibilityChanged() {
-        if (LockedUserState.get(this).isUserUnlocked()) {
-            mOverviewComponentObserver.getContainerInterface().onAssistantVisibilityChanged(
-                    mDeviceState.getAssistantVisibility());
-        }
-    }
-
     @Override
     public void onDestroy() {
         Log.d(TAG, "onDestroy: user=" + getUserId()
                 + " instance=" + System.identityHashCode(this));
         if (LockedUserState.get(this).isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
-            mOverviewComponentObserver.setHomeDisabled(false);
             mOverviewComponentObserver.removeOverviewChangeListener(mOverviewChangeListener);
         }
+        if (mTaskAnimationManager != null) {
+            mTaskAnimationManager.onDestroy();
+        }
+
         disposeEventHandlers("TouchInteractionService onDestroy()");
         SystemUiProxy.INSTANCE.get(this).clearProxy();
 
@@ -748,6 +354,8 @@
         mDeviceState.removeDisplayInfoChangeListener(mDisplayInfoChangeListener);
         LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable);
         ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
+        SystemUiProxy.INSTANCE.get(this)
+                .removeOnStateChangeListener(mSysUiProxyStateChangeCallback);
         super.onDestroy();
     }
 
@@ -875,7 +483,6 @@
                         this::onConsumerInactive,
                         mInputEventReceiver,
                         mTaskbarManager,
-                        mSwipeUpProxyProvider,
                         mOverviewCommandHelper,
                         event);
                 mUncheckedConsumer = mConsumer;
@@ -1154,4 +761,26 @@
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this));
     }
+
+    @VisibleForTesting
+    public void injectFakeTrackpadForTesting() {
+        mTrackpadsConnected.add(1000);
+        initInputMonitor("tapl testing");
+    }
+
+    @VisibleForTesting
+    public void ejectFakeTrackpadForTesting() {
+        mTrackpadsConnected.clear();
+        // This method destroys the current input monitor if set up, and only init a new one
+        // in 3-button mode if {@code mTrackpadsConnected} is not empty. So in other words,
+        // it will destroy the input monitor.
+        initInputMonitor("tapl testing");
+    }
+
+    /** Refreshes the current overview target. */
+    @VisibleForTesting
+    public void refreshOverviewTarget() {
+        mAllAppsActionManager.onDestroy();
+        onOverviewTargetChanged(mOverviewComponentObserver.isHomeAndOverviewSame());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index adc45ae..d79a8ea 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -22,6 +22,7 @@
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SimpleOrientationTouchTransformer;
 import com.android.quickstep.SystemUiProxy;
@@ -29,6 +30,7 @@
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.logging.SettingsChangeLogger;
 import com.android.quickstep.util.AsyncClockEventDelegate;
+import com.android.quickstep.util.ContextualSearchHapticManager;
 import com.android.quickstep.util.ContextualSearchStateManager;
 
 /**
@@ -57,10 +59,14 @@
 
     RotationTouchHelper getRotationTouchHelper();
 
+    ContextualSearchHapticManager getContextualSearchHapticManager();
+
     ContextualSearchStateManager getContextualSearchStateManager();
 
     RecentsAnimationDeviceState getRecentsAnimationDeviceState();
 
+    RecentsModel getRecentsModel();
+
     SettingsChangeLogger getSettingsChangeLogger();
 
     SimpleOrientationTouchTransformer getSimpleOrientationTouchTransformer();
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 31a1be8..95a3ec2 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -50,10 +50,14 @@
             DaggerSingletonObject<RecentsDisplayModel>(
                 QuickstepBaseAppComponent::getRecentsDisplayModel
             )
+
+        @JvmStatic
+        fun enableOverviewInWindow() =
+            Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()
     }
 
     init {
-        if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+        if (enableOverviewInWindow()) {
             displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
             // In the scenario where displays were added before this display listener was
             // registered, we should store the RecentsDisplayResources for those displays
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index c986b88..39a9dff 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -72,7 +72,8 @@
 import com.android.quickstep.GestureState;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
-import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.TISBinder;
 import com.android.quickstep.util.ActivityPreloadUtil;
 import com.android.quickstep.util.LottieAnimationColorUtils;
 import com.android.quickstep.util.TISBindHelper;
@@ -283,16 +284,13 @@
     protected void onResume() {
         super.onResume();
         maybeResumeOrPauseBackgroundAnimation();
-        TISBinder binder = mTISBindHelper.getBinder();
-        if (binder != null) {
-            setSetupUIVisible(true);
-            binder.setSwipeUpProxy(this::createSwipeUpProxy);
-        }
+        RecentsAnimationDeviceState.INSTANCE.get(this)
+                .setSwipeUpProxyProvider(this::createSwipeUpProxy);
+        setSetupUIVisible(true);
     }
 
     private void onTISConnected(TISBinder binder) {
         setSetupUIVisible(isResumed());
-        binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
             mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
@@ -306,7 +304,8 @@
     @Override
     protected void onPause() {
         super.onPause();
-        clearBinderOverride();
+        setSetupUIVisible(false);
+        RecentsAnimationDeviceState.INSTANCE.get(this).setSwipeUpProxyProvider(null);
         maybeResumeOrPauseBackgroundAnimation();
         if (mSwipeProgress.value >= 1) {
             finishAndRemoveTask();
@@ -314,14 +313,6 @@
         }
     }
 
-    private void clearBinderOverride() {
-        TISBinder binder = mTISBindHelper.getBinder();
-        if (binder != null) {
-            setSetupUIVisible(false);
-            binder.setSwipeUpProxy(null);
-        }
-    }
-
     /**
      * Should be called when we have successfully reached Launcher, so we dispatch to animation
      * listeners to ensure the state matches the visual animation that just occurred.
@@ -338,8 +329,9 @@
     protected void onDestroy() {
         super.onDestroy();
         getIDP().removeOnChangeListener(mOnIDPChangeListener);
+        // In case bindHelper was not connected during onPause, clear UIVisible flag again
+        setSetupUIVisible(false);
         mTISBindHelper.onDestroy();
-        clearBinderOverride();
         if (mBackgroundAnimatorListener != null) {
             mAnimatedBackground.removeAnimatorListener(mBackgroundAnimatorListener);
         }
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 0365f89..2114c30 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -38,10 +38,9 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.StatsLogManager;
-import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.interaction.TutorialController.TutorialType;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.TISBindHelper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -64,7 +63,6 @@
 
     private SharedPreferences mSharedPrefs;
     private StatsLogManager mStatsLogManager;
-    private TISBindHelper mTISBindHelper;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -99,7 +97,6 @@
                 .commit();
 
         correctUserOrientation();
-        mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
 
         initWindowInsets();
     }
@@ -349,10 +346,6 @@
         updateServiceState(true);
     }
 
-    private void onTISConnected(TISBinder binder) {
-        updateServiceState(isResumed());
-    }
-
     @Override
     protected void onPause() {
         super.onPause();
@@ -360,16 +353,13 @@
     }
 
     private void updateServiceState(boolean isEnabled) {
-        TISBinder binder = mTISBindHelper.getBinder();
-        if (binder != null) {
-            binder.setGestureBlockedTaskId(isEnabled ? getTaskId() : -1);
-        }
+        RecentsAnimationDeviceState.INSTANCE.get(this)
+                .setGestureBlockingTaskId(isEnabled ? getTaskId() : -1);
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        mTISBindHelper.onDestroy();
         updateServiceState(false);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 002a4e8..b1a5920 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -66,14 +66,10 @@
                 )
                 tasks.value = MapForStateFlow(recentTasks)
 
-                // Request data for completed tasks to prevent stale data.
-                // This will prevent thumbnail and icon from being replaced and
-                // null due to race condition.
-                taskRequests.values.forEach { (taskKey, job) ->
-                    if (job.isCompleted) {
-                        requestTaskData(taskKey.id)
-                    }
-                }
+                // Request data for tasks to prevent stale data.
+                // This will prevent thumbnail and icon from being replaced and null due to
+                // race condition. The new request will hit the cache and return immediately.
+                taskRequests.keys.forEach(::requestTaskData)
             }
         }
         return tasks.map { it.values.toList() }
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 162d14d..0b299ee 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -77,8 +77,7 @@
             .flowOn(dispatcherProvider.background)
 
     fun bind(vararg taskId: TaskId) {
-        Log.d(TAG, "bind: $taskId")
-        taskIds.value = taskId.toSet()
+        taskIds.value = taskId.toSet().also { Log.d(TAG, "bind: $it") }
     }
 
     fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
index 286b77a..7ec605d 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
@@ -21,18 +21,25 @@
 import android.os.VibrationEffect.Composition
 import android.os.Vibrator
 import com.android.launcher3.dagger.ApplicationContext
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.VibratorWrapper
 import com.android.quickstep.DeviceConfigWrapper.Companion.get
+import com.android.quickstep.dagger.QuickstepBaseAppComponent
+import javax.inject.Inject
 import kotlin.math.pow
 
 /** Manages haptics relating to Contextual Search invocations. */
+@LauncherAppSingleton
 class ContextualSearchHapticManager
-internal constructor(@ApplicationContext private val context: Context) : SafeCloseable {
+@Inject
+internal constructor(
+    @ApplicationContext private val context: Context,
+    private val contextualSearchStateManager: ContextualSearchStateManager,
+    private val vibratorWrapper: VibratorWrapper,
+) {
 
     private var searchEffect = createSearchEffect()
-    private var contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[context]
 
     private fun createSearchEffect() =
         if (
@@ -50,7 +57,7 @@
 
     /** Indicates that search has been invoked. */
     fun vibrateForSearch() {
-        searchEffect.let { VibratorWrapper.INSTANCE[context].vibrate(it) }
+        searchEffect.let { vibratorWrapper.vibrate(it) }
     }
 
     /** Indicates that search will be invoked if the current gesture is maintained. */
@@ -93,13 +100,13 @@
                     composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale)
                 }
             }
-            VibratorWrapper.INSTANCE[context].vibrate(composition.compose())
+            vibratorWrapper.vibrate(composition.compose())
         }
     }
 
-    override fun close() {}
-
     companion object {
-        @JvmField val INSTANCE = MainThreadInitializedObject { ContextualSearchHapticManager(it) }
+        @JvmField
+        val INSTANCE =
+            DaggerSingletonObject(QuickstepBaseAppComponent::getContextualSearchHapticManager)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
index d00a39c..969aa0f 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
@@ -21,7 +21,6 @@
 import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH
 import android.content.Context
 import android.util.Log
-import androidx.annotation.VisibleForTesting
 import com.android.internal.app.AssistUtils
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR
@@ -34,7 +33,7 @@
 import com.android.quickstep.BaseContainerInterface
 import com.android.quickstep.DeviceConfigWrapper
 import com.android.quickstep.OverviewComponentObserver
-import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.RecentsAnimationDeviceState
 import com.android.quickstep.TopTaskTracker
 import com.android.quickstep.views.RecentsView
 import com.android.systemui.shared.system.QuickStepContract
@@ -45,9 +44,10 @@
     private val context: Context,
     private val contextualSearchStateManager: ContextualSearchStateManager,
     private val topTaskTracker: TopTaskTracker,
-    private val systemUiProxy: SystemUiProxy,
+    private val deviceState: RecentsAnimationDeviceState,
     private val statsLogManager: StatsLogManager,
     private val contextualSearchHapticManager: ContextualSearchHapticManager,
+    private val overviewComponentObserver: OverviewComponentObserver,
     private val contextualSearchManager: ContextualSearchManager?,
 ) {
     constructor(
@@ -56,9 +56,10 @@
         context,
         ContextualSearchStateManager.INSTANCE[context],
         TopTaskTracker.INSTANCE[context],
-        SystemUiProxy.INSTANCE[context],
+        RecentsAnimationDeviceState.INSTANCE[context],
         StatsLogManager.newInstance(context),
         ContextualSearchHapticManager.INSTANCE[context],
+        OverviewComponentObserver.INSTANCE[context],
         context.getSystemService(ContextualSearchManager::class.java),
     )
 
@@ -189,7 +190,7 @@
         if (contextualSearchManager == null) {
             return false
         }
-        val recentsContainerInterface = getRecentsContainerInterface()
+        val recentsContainerInterface = overviewComponentObserver.containerInterface
         if (recentsContainerInterface?.isInLiveTileMode() == true) {
             Log.i(TAG, "Contextual Search invocation attempted: live tile")
             endLiveTileMode(recentsContainerInterface) {
@@ -202,27 +203,20 @@
     }
 
     private fun isFakeLandscape(): Boolean =
-        getRecentsContainerInterface()
-            ?.getCreatedContainer()
+        overviewComponentObserver.containerInterface
+            ?.createdContainer
             ?.getOverviewPanel<RecentsView<*, *>>()
-            ?.getPagedOrientationHandler()
+            ?.pagedOrientationHandler
             ?.isLayoutNaturalToLauncher == false
 
-    private fun isInSplitscreen(): Boolean {
-        return topTaskTracker.getRunningSplitTaskIds().isNotEmpty()
-    }
+    private fun isInSplitscreen(): Boolean = topTaskTracker.runningSplitTaskIds.isNotEmpty()
 
     private fun isNotificationShadeShowing(): Boolean {
-        return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L
+        return deviceState.systemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L
     }
 
     private fun isKeyguardShowing(): Boolean {
-        return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L
-    }
-
-    @VisibleForTesting
-    fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? {
-        return OverviewComponentObserver.INSTANCE.get(context).containerInterface
+        return deviceState.systemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 99255e8..88d14b7 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -65,6 +65,7 @@
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
 import com.android.launcher3.views.BaseDragLayer
 import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.util.SplitScreenUtils.Companion.extractTopParentAndChildren
 import com.android.quickstep.views.FloatingAppPairView
 import com.android.quickstep.views.FloatingTaskView
 import com.android.quickstep.views.GroupedTaskView
@@ -981,35 +982,15 @@
         progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION)
         progressUpdater.interpolator = Interpolators.EMPHASIZED
 
-        var rootCandidate: Change? = null
-
-        for (change in transitionInfo.changes) {
-            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
-
-            // TODO (b/316490565): Replace this logic when SplitBounds is available to
-            //  startAnimation() and we can know the precise taskIds of launching tasks.
-            if (
-                taskInfo.windowingMode == windowingMode &&
-                    (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
-            ) {
-                // Found one!
-                rootCandidate = change
-                break
-            }
-        }
-
-        // If we could not find a proper root candidate, something went wrong.
-        check(rootCandidate != null) { "Could not find a split root candidate" }
-
-        // Recurse up the tree until parent is null, then we've found our root.
-        var parentToken: WindowContainerToken? = rootCandidate.parent
-        while (parentToken != null) {
-            rootCandidate = transitionInfo.getChange(parentToken) ?: break
-            parentToken = rootCandidate.parent
-        }
-
-        // Make sure nothing weird happened, like getChange() returning null.
-        check(rootCandidate != null) { "Failed to find a root leash" }
+        val splitTree: Pair<Change, List<Change>>? = extractTopParentAndChildren(transitionInfo)
+        check(splitTree != null) { "Could not find a split root candidate" }
+        val rootCandidate = splitTree.first
+        val stageRootTaskIds: Set<Int> = splitTree.second
+            .map { it.taskInfo!!.taskId }
+            .toSet()
+        val leafTasks: List<Change> = transitionInfo.changes
+            .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds}
+            .toList()
 
         // Starting position is a 34% size tile centered in the middle of the screen.
         // Ending position is the full device screen.
@@ -1043,6 +1024,29 @@
                 override fun onAnimationEnd(animation: Animator) {
                     finishCallback.run()
                 }
+
+                override fun onAnimationStart(animation: Animator) {
+                    // Reset leaf and stage root tasks, animation can begin from freeform windows
+                    for (leaf in leafTasks) {
+                        val endAbsBounds = leaf.endAbsBounds
+
+                        t.setAlpha(leaf.leash, 1f)
+                        t.setCrop(leaf.leash, 0f, 0f,
+                            endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
+                        t.setPosition(leaf.leash, 0f, 0f)
+                    }
+
+                    for (stageRoot in splitTree.second) {
+                        val endAbsBounds = stageRoot.endAbsBounds
+
+                        t.setAlpha(stageRoot.leash, 1f)
+                        t.setCrop(stageRoot.leash, 0f, 0f,
+                            endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
+                        t.setPosition(stageRoot.leash, endAbsBounds.left.toFloat(),
+                            endAbsBounds.top.toFloat())
+                    }
+                    t.apply()
+                }
             }
         )
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 4005c5a..7787e30 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -46,8 +46,8 @@
          * Given a TransitionInfo, generates the tree structure for those changes and extracts out
          * the top most root and it's two immediate children. Changes can be provided in any order.
          *
-         * @return a [Pair] where first -> top most split root, second -> [List] of 2,
-         *   leftTop/bottomRight stage roots
+         * @return null if no root is found, otherwise a [Pair] where first -> top most split root,
+         *         second -> [List] of 2, leftTop/bottomRight stage roots
          */
         fun extractTopParentAndChildren(
             transitionInfo: TransitionInfo
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 828322b..7d5b471 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -169,9 +169,6 @@
 
         if (sourceRectHint.isEmpty()) {
             mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio));
-            // Create a new overlay layer. We do not call detach on this instance, it's propagated
-            // to other classes like PipTaskOrganizer / RecentsAnimationController to complete
-            // the cleanup.
             mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
                     mAppBounds, mDestinationBounds,
                     new IconProvider(context).getIcon(mActivityInfo), appIconSizePx);
diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
index 027dc08..3716efa 100644
--- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java
@@ -28,8 +28,8 @@
 
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.TISBinder;
 import com.android.quickstep.TouchInteractionService;
-import com.android.quickstep.TouchInteractionService.TISBinder;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -109,15 +109,6 @@
         return mBinder == null ? null : mBinder.getTaskbarManager();
     }
 
-    /**
-     * Sets flag whether a predictive back-to-home animation is in progress
-     */
-    public void setPredictiveBackToHomeInProgress(boolean isInProgress) {
-        if (mBinder != null) {
-            mBinder.setPredictiveBackToHomeInProgress(isInProgress);
-        }
-    }
-
     @Nullable
     public OverviewCommandHelper getOverviewCommandHelper() {
         return mBinder == null ? null : mBinder.getOverviewCommandHelper();
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
index ceffbe4..da26622 100644
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -103,23 +103,28 @@
             }
             case DIRECTION_RIGHT: {
                 int boundedIndex =
-                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max(
-                                nextIndex, 0);
+                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
+                                : Math.max(nextIndex, 0);
                 boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
                 return inOriginalTop ? mTopRowIds.get(boundedIndex)
                         : mBottomRowIds.get(boundedIndex);
             }
             case DIRECTION_TAB: {
                 int boundedIndex =
-                        cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min(
-                                nextIndex, maxSize - 1);
+                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
+                                : Math.min(nextIndex, maxSize - 1);
                 if (delta >= 0) {
                     return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
                             ? mBottomRowIds.get(index)
                             : mTopRowIds.get(boundedIndex);
                 } else {
                     if (mTopRowIds.contains(currentPageTaskViewId)) {
-                        return mBottomRowIds.get(boundedIndex);
+                        if (boundedIndex < 0) {
+                            // If no cycling, always return the first task.
+                            return mTopRowIds.get(0);
+                        } else {
+                            return mBottomRowIds.get(boundedIndex);
+                        }
                     } else {
                         // Go up to top if there is task above
                         return mTopRowIds.get(index) != mBottomRowIds.get(index)
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index bb6829a..02be373 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -53,6 +53,7 @@
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
 import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
+import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.systemui.shared.recents.model.Task
@@ -364,6 +365,10 @@
         taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
     }
 
+    override fun setIconState(container: TaskContainer, state: TaskData?) {
+        container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription
+    }
+
     // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
     override fun onIconUnloaded(taskContainer: TaskContainer) {}
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a76ebdb..51980f0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -6379,7 +6379,7 @@
     }
 
     /**
-     * @return true if the task in on the top of the grid
+     * @return true if the task in on the bottom of the grid
      */
     public boolean isOnGridBottomRow(TaskView taskView) {
         return showAsGrid()
@@ -6942,7 +6942,8 @@
      * Creates the spring animations which run as a task settles back into its place in overview.
      *
      * <p>When a task dismiss is cancelled, the task will return to its original position via a
-     * spring animation.
+     * spring animation. As it passes the threshold of its settling state, its neighbors will
+     * spring in response to the perceived impact of the settling task.
      */
     public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
             float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index f610335..d37a3f9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -20,6 +20,7 @@
 import android.view.View
 import androidx.core.view.children
 import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
@@ -29,6 +30,7 @@
 import com.android.launcher3.util.DynamicResource
 import com.android.launcher3.util.IntArray
 import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.TaskGridNavHelper
 import com.android.quickstep.util.isExternalDisplay
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -305,7 +307,8 @@
      * Creates the spring animations which run when a dragged task view in overview is released.
      *
      * <p>When a task dismiss is cancelled, the task will return to its original position via a
-     * spring animation.
+     * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+     * in response to the perceived impact of the settling task.
      */
     fun createTaskDismissSettlingSpringAnimation(
         draggedTaskView: TaskView?,
@@ -320,37 +323,181 @@
             FloatPropertyCompat.createFloatPropertyCompat(
                 draggedTaskView.secondaryDismissTranslationProperty
             )
-        val rp = DynamicResource.provider(recentsView.mContainer)
-        return SpringAnimation(draggedTaskView, taskDismissFloatProperty)
-            .setSpring(
-                SpringForce()
-                    .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
-                    .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness))
-            )
-            .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
-            .addUpdateListener { animation, value, _ ->
-                if (isDismissing && abs(value) >= abs(dismissLength)) {
-                    // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
-                    draggedTaskView.alpha = 0f
-                    animation.cancel()
-                } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
-                    recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
-                        remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
-                            taskDismissFloatProperty.getValue(draggedTaskView)
+        // Animate dragged task towards dismissal or rest state.
+        val draggedTaskViewSpringAnimation =
+            SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+                .setSpring(createExpressiveDismissSpringForce())
+                .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+                .addUpdateListener { animation, value, _ ->
+                    if (isDismissing && abs(value) >= abs(dismissLength)) {
+                        // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
+                        draggedTaskView.alpha = 0f
+                        animation.cancel()
+                    } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+                        recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                            remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                                taskDismissFloatProperty.getValue(draggedTaskView)
+                        }
+                        recentsView.redrawLiveTile()
                     }
-                    recentsView.redrawLiveTile()
                 }
+                .addEndListener { _, _, _, _ ->
+                    if (isDismissing) {
+                        recentsView.dismissTask(
+                            draggedTaskView,
+                            /* animateTaskView = */ false,
+                            /* removeTask = */ true,
+                        )
+                    } else {
+                        recentsView.onDismissAnimationEnds()
+                    }
+                    onEndRunnable()
+                }
+        if (!isDismissing) {
+            addNeighboringSpringAnimationsForDismissCancel(
+                draggedTaskView,
+                draggedTaskViewSpringAnimation,
+                recentsView.pageCount,
+            )
+        }
+        return draggedTaskViewSpringAnimation
+    }
+
+    private fun addNeighboringSpringAnimationsForDismissCancel(
+        draggedTaskView: TaskView,
+        draggedTaskViewSpringAnimation: SpringAnimation,
+        taskCount: Int,
+    ) {
+        // Empty spring animation exists for conditional start, and to drive neighboring springs.
+        val neighborsToSettle =
+            SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+        var lastPosition = 0f
+        var startSettling = false
+        draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
+            // Start the settling animation the first time the dragged task passes the origin (from
+            // negative displacement to positive displacement). We do not check for an exact value
+            // to compare to, as the update listener does not necessarily hit every value (e.g. a
+            // value of zero). Do not check again once it has started settling, as a spring can
+            // bounce past the origin multiple times depending on the stifness and damping ratio.
+            if (startSettling) return@addUpdateListener
+            if (lastPosition < 0 && value >= 0) {
+                startSettling = true
             }
-            .addEndListener { _, _, _, _ ->
-                if (isDismissing) {
-                    recentsView.dismissTask(
-                        draggedTaskView,
-                        /* animateTaskView = */ false,
-                        /* removeTask = */ true,
+            lastPosition = value
+            if (startSettling) {
+                neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+            }
+        }
+
+        // Add tasks before dragged index, fanning out from the dragged task.
+        // The order they are added matters, as each spring drives the next.
+        var previousNeighbor = neighborsToSettle
+        getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
+            previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+        }
+        // Add tasks after dragged index, fanning out from the dragged task.
+        // The order they are added matters, as each spring drives the next.
+        previousNeighbor = neighborsToSettle
+        getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
+            previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+        }
+    }
+
+    /** Gets adjacent tasks either before or after the dragged task in visual order. */
+    private fun getTasksAdjacentToDraggedTask(
+        draggedTaskView: TaskView,
+        towardsStart: Boolean,
+    ): Sequence<TaskView> {
+        if (recentsView.showAsGrid()) {
+            return gridTaskViewInTabOrderSequence(draggedTaskView, towardsStart)
+        } else {
+            val taskViewList = taskViews.toList()
+            val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+            return if (towardsStart) {
+                taskViewList.take(draggedTaskViewIndex).reversed().asSequence()
+            } else {
+                taskViewList.takeLast(taskViewList.size - draggedTaskViewIndex - 1).asSequence()
+            }
+        }
+    }
+
+    /**
+     * Returns a sequence of TaskViews in the grid, ordered according to tab navigation, starting
+     * from the dragged TaskView, in the direction of the provided delta.
+     *
+     * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+     * negative value moves backward towards the beginning.
+     */
+    private fun gridTaskViewInTabOrderSequence(
+        draggedTaskView: TaskView,
+        towardsStart: Boolean,
+    ): Sequence<TaskView> = sequence {
+        val taskGridNavHelper =
+            TaskGridNavHelper(
+                recentsView.topRowIdArray,
+                recentsView.bottomRowIdArray,
+                getLargeTaskViewIds(),
+                /* hasAddDesktopButton= */ false,
+            )
+        var nextTaskView: TaskView? = draggedTaskView
+        var previousTaskView: TaskView? = null
+        while (nextTaskView != previousTaskView && nextTaskView != null) {
+            previousTaskView = nextTaskView
+            nextTaskView =
+                recentsView.getTaskViewFromTaskViewId(
+                    taskGridNavHelper.getNextGridPage(
+                        nextTaskView.taskViewId,
+                        if (towardsStart) -1 else 1,
+                        TaskGridNavHelper.DIRECTION_TAB,
+                        /* cycle = */ false,
                     )
-                }
-                onEndRunnable()
+                )
+            if (nextTaskView != null && nextTaskView != previousTaskView) {
+                yield(nextTaskView)
             }
+        }
+    }
+
+    /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+    private fun createNeighboringTaskViewSpringAnimation(
+        taskView: TaskView,
+        previousNeighborSpringAnimation: SpringAnimation,
+    ): SpringAnimation {
+        val neighboringTaskViewSpringAnimation =
+            SpringAnimation(
+                    taskView,
+                    FloatPropertyCompat.createFloatPropertyCompat(
+                        taskView.secondaryDismissTranslationProperty
+                    ),
+                )
+                .setSpring(createExpressiveDismissSpringForce())
+        // Update live tile on spring animation.
+        if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+            neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+                recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                    remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                        taskView.secondaryDismissTranslationProperty.get(taskView)
+                }
+                recentsView.redrawLiveTile()
+            }
+        }
+        // Drive current neighbor's spring with the previous neighbor's.
+        previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+            neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+        }
+        return neighboringTaskViewSpringAnimation
+    }
+
+    private fun createExpressiveDismissSpringForce(): SpringForce {
+        val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+        return SpringForce()
+            .setDampingRatio(
+                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio)
+            )
+            .setStiffness(
+                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+            )
     }
 
     companion object {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 5093259..609262f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -81,6 +81,7 @@
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
 import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
@@ -773,12 +774,17 @@
         // Updating containers
         val mapOfTasks = state.tasks.associateBy { it.taskId }
         taskContainers.forEach { container ->
+            val containerState = mapOfTasks[container.task.key.id]
             container.setState(
-                state = mapOfTasks[container.task.key.id],
+                state = containerState,
                 liveTile = state.isLiveTile,
                 hasHeader = type == TaskViewType.DESKTOP,
             )
             updateThumbnailValidity(container)
+
+            if (enableOverviewIconMenu()) {
+                setIconState(container, containerState)
+            }
         }
     }
 
@@ -1028,7 +1034,7 @@
                 }
             }
         }
-        if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+        if (needsUpdate(changes, FLAG_UPDATE_ICON) && !enableOverviewIconMenu()) {
             taskContainers.forEach {
                 if (visible) {
                     recentsModel.iconCache
@@ -1059,10 +1065,23 @@
         pendingIconLoadRequests.clear()
     }
 
+    protected open fun setIconState(container: TaskContainer, state: TaskData?) {
+        if (enableOverviewIconMenu()) {
+            if (state is TaskData.Data) {
+                setIcon(container.iconView, state.icon)
+                container.iconView.setText(state.title)
+                container.digitalWellBeingToast?.initialize()
+            } else {
+                setIcon(container.iconView, null)
+                container.iconView.setText(null)
+            }
+        }
+    }
+
     protected open fun onIconLoaded(taskContainer: TaskContainer) {
         setIcon(taskContainer.iconView, taskContainer.task.icon)
         if (enableOverviewIconMenu()) {
-            setText(taskContainer.iconView, taskContainer.task.title)
+            taskContainer.iconView.setText(taskContainer.task.title)
         }
         taskContainer.digitalWellBeingToast?.initialize()
     }
@@ -1070,7 +1089,7 @@
     protected open fun onIconUnloaded(taskContainer: TaskContainer) {
         setIcon(taskContainer.iconView, null)
         if (enableOverviewIconMenu()) {
-            setText(taskContainer.iconView, null)
+            taskContainer.iconView.setText(null)
         }
     }
 
@@ -1095,10 +1114,6 @@
         }
     }
 
-    protected fun setText(iconView: TaskViewIcon, text: CharSequence?) {
-        iconView.setText(text)
-    }
-
     @JvmOverloads
     open fun setShouldShowScreenshot(
         shouldShowScreenshot: Boolean,
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
index 785e585..50d6aff 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
@@ -22,6 +22,7 @@
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING
+import com.android.launcher3.taskbar.rules.SandboxParams
 import com.android.launcher3.taskbar.rules.TaskbarModeRule
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
@@ -46,15 +47,15 @@
 
     @get:Rule(order = 0)
     val context =
-        TaskbarWindowSandboxContext.create { builder ->
-            builder.bindSystemUiProxy(
+        TaskbarWindowSandboxContext.create(
+            SandboxParams({
                 spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy ->
                     doAnswer { latestSuspendNotification = it.getArgument(0) }
                         .whenever(proxy)
                         .notifyTaskbarAutohideSuspend(anyOrNull())
                 }
-            )
-        }
+            })
+        )
     @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
     @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
     @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index 9ca8a1b..bfd53ef 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -26,21 +26,29 @@
 import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR
 import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
 import com.android.launcher3.R
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
 import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.DisplayControllerModule
+import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper
 import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule
+import com.android.launcher3.taskbar.rules.SandboxParams
 import com.android.launcher3.taskbar.rules.TaskbarModeRule
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.AllModulesForTest
+import com.android.launcher3.util.FakePrefsModule
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.TestUtil.getOnUiThread
+import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.util.DesktopTask
 import com.android.systemui.shared.recents.model.Task
@@ -49,6 +57,8 @@
 import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
 import com.android.wm.shell.desktopmode.IDesktopTaskListener
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -70,19 +80,25 @@
 class TaskbarOverflowTest {
     @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
 
+    val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper()
+
     @get:Rule(order = 1)
     val context =
-        TaskbarWindowSandboxContext.create { builder ->
-            builder.bindSystemUiProxy(
-                spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy ->
-                    doAnswer { desktopTaskListener = it.getArgument(0) }
-                        .whenever(proxy)
-                        .setDesktopTaskListener(anyOrNull())
-                }
+        TaskbarWindowSandboxContext.create(
+            SandboxParams(
+                {
+                    spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy ->
+                        doAnswer { desktopTaskListener = it.getArgument(0) }
+                            .whenever(proxy)
+                            .setDesktopTaskListener(anyOrNull())
+                    }
+                },
+                DaggerTaskbarOverflowComponent.builder()
+                    .bindRecentsModel(mockRecentsModelHelper.mockRecentsModel),
             )
-        }
+        )
 
-    @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(context)
+    @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper)
 
     @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
 
@@ -404,3 +420,18 @@
         return maxNumIconViews
     }
 }
+
+/** TaskbarOverflowComponent used to bind the RecentsModel. */
+@LauncherAppSingleton
+@Component(
+    modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class]
+)
+interface TaskbarOverflowComponent : TaskbarSandboxComponent {
+
+    @Component.Builder
+    interface Builder : TaskbarSandboxComponent.Builder {
+        @BindsInstance fun bindRecentsModel(model: RecentsModel): Builder
+
+        override fun build(): TaskbarOverflowComponent
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
index 360f019..ba53dcd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.SandboxParams
 import com.android.launcher3.taskbar.rules.TaskbarModeRule
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
@@ -55,13 +56,14 @@
     @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
     @get:Rule(order = 1)
     val context =
-        TaskbarWindowSandboxContext.create { builder ->
-            builder.bindSystemUiProxy(
+        TaskbarWindowSandboxContext.create(
+            SandboxParams({
                 spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) {
                     doAnswer { backPressed = true }.whenever(it).onBackEvent(anyOrNull())
                 }
-            )
-        }
+            })
+        )
+
     @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
     @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this)
     @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
index 021e1e4..8395d79 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -50,8 +50,11 @@
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.TestUtil
+import com.android.quickstep.RecentsAnimationDeviceState
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE
 import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
@@ -60,6 +63,8 @@
 import org.junit.After
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
@@ -68,7 +73,16 @@
 class TaskbarStashControllerTest {
     @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
     @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
-    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 2)
+    val initSingleton =
+        object : TestWatcher() {
+            override fun starting(description: Description?) {
+                TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {
+                    RecentsAnimationDeviceState.INSTANCE[context].systemUiStateFlags = 0
+                }
+            }
+        }
+    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
     @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this)
     @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt
new file mode 100644
index 0000000..a7bfa9a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.rules
+
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.GroupTask
+import java.util.function.Consumer
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+/** Helper class to mock the {@link RecentsModel} object in test */
+class MockedRecentsModelHelper {
+    private val mockIconCache: TaskIconCache = mock()
+    var taskListId = 0
+    var recentTasksChangedListener: RecentTasksChangedListener? = null
+    var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
+
+    val mockRecentsModel: RecentsModel = mock {
+        on { iconCache } doReturn mockIconCache
+
+        on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
+
+        on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
+            {
+                recentTasksChangedListener = it.getArgument<RecentTasksChangedListener>(0)
+            }
+
+        on { getTasks(anyOrNull(), anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { getTasks(anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
index ed1443d..359b876 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
@@ -16,64 +16,17 @@
 
 package com.android.launcher3.taskbar.rules
 
-import com.android.quickstep.RecentsModel
-import com.android.quickstep.RecentsModel.RecentTasksChangedListener
-import com.android.quickstep.TaskIconCache
 import com.android.quickstep.util.GroupTask
-import java.util.function.Consumer
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
-import org.mockito.kotlin.any
-import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.doAnswer
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
 
-class MockedRecentsModelTestRule(private val context: TaskbarWindowSandboxContext) : TestRule {
-
-    private val mockIconCache: TaskIconCache = mock()
-
-    private val mockRecentsModel: RecentsModel = mock {
-        on { iconCache } doReturn mockIconCache
-
-        on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
-
-        on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
-            {
-                recentTasksChangedListener = it.getArgument<RecentTasksChangedListener>(0)
-            }
-
-        on { getTasks(anyOrNull(), anyOrNull()) } doAnswer
-            {
-                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
-                if (request != null) {
-                    taskRequests.add { response -> request.accept(response) }
-                }
-                taskListId
-            }
-
-        on { getTasks(anyOrNull()) } doAnswer
-            {
-                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
-                if (request != null) {
-                    taskRequests.add { response -> request.accept(response) }
-                }
-                taskListId
-            }
-
-        on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) }
-    }
-
+class MockedRecentsModelTestRule(private val modelHelper: MockedRecentsModelHelper) : TestRule {
     private var recentTasks: List<GroupTask> = emptyList()
-    private var taskListId = 0
-    private var recentTasksChangedListener: RecentTasksChangedListener? = null
-    private var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
 
     override fun apply(base: Statement?, description: Description?): Statement {
         return object : Statement() {
             override fun evaluate() {
-                context.putObject(RecentsModel.INSTANCE, mockRecentsModel)
                 base?.evaluate()
             }
         }
@@ -82,15 +35,15 @@
     // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so
     // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list.
     fun updateRecentTasks(tasks: List<GroupTask>) {
-        ++taskListId
+        ++modelHelper.taskListId
         recentTasks = tasks
-        recentTasksChangedListener?.onRecentTasksChanged()
+        modelHelper.recentTasksChangedListener?.onRecentTasksChanged()
     }
 
     fun resolvePendingTaskRequests() {
         val requests = mutableListOf<(List<GroupTask>) -> Unit>()
-        requests.addAll(taskRequests)
-        taskRequests.clear()
+        requests.addAll(modelHelper.taskRequests)
+        modelHelper.taskRequests.clear()
 
         requests.forEach { it(recentTasks) }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index 90c9553..0204b2d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -33,6 +33,7 @@
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.android.launcher3.util.TestUtil
 import com.android.quickstep.AllAppsActionManager
+import com.android.quickstep.fallback.window.RecentsDisplayModel
 import java.lang.reflect.Field
 import java.lang.reflect.ParameterizedType
 import java.util.Locale
@@ -110,6 +111,7 @@
                                     PendingIntent(IIntentSender.Default())
                                 },
                                 object : TaskbarNavButtonCallbacks {},
+                                RecentsDisplayModel.INSTANCE.get(context),
                             ) {
                             override fun recreateTaskbar() {
                                 super.recreateTaskbar()
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index e6dc2a2..95e8980 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -47,10 +47,6 @@
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
-/** Include additional bindings when building a [TaskbarSandboxComponent]. */
-typealias TaskbarComponentBinder =
-    TaskbarWindowSandboxContext.(TaskbarSandboxComponent.Builder) -> Unit
-
 /**
  * [SandboxApplication] for running Taskbar tests.
  *
@@ -61,7 +57,7 @@
 private constructor(
     private val base: SandboxApplication,
     val virtualDisplay: VirtualDisplay,
-    private val componentBinder: TaskbarComponentBinder?,
+    private val params: SandboxParams,
 ) : ContextWrapper(base), ObjectSandbox by base, TestRule {
 
     val settingsCacheSandbox = SettingsCacheSandbox()
@@ -76,10 +72,9 @@
             override fun before() {
                 val context = this@TaskbarWindowSandboxContext
                 val builder =
-                    DaggerTaskbarSandboxComponent.builder()
-                        .bindSystemUiProxy(SystemUiProxy(context))
+                    params.builderBase
+                        .bindSystemUiProxy(params.systemUiProxyProvider.invoke(context))
                         .bindSettingsCache(settingsCacheSandbox.cache)
-                componentBinder?.invoke(context, builder)
                 base.initDaggerComponent(builder)
             }
         }
@@ -95,10 +90,9 @@
         private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay"
 
         /** Creates a [SandboxApplication] for Taskbar tests. */
-        fun create(componentBinder: TaskbarComponentBinder? = null): TaskbarWindowSandboxContext {
+        fun create(params: SandboxParams = SandboxParams()): TaskbarWindowSandboxContext {
             val base = ApplicationProvider.getApplicationContext<Context>()
             val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java))
-
             // Create virtual display to avoid clashing with Taskbar on default display.
             val virtualDisplay =
                 base.resources.displayMetrics.let {
@@ -115,7 +109,7 @@
             return TaskbarWindowSandboxContext(
                 SandboxApplication(base.createDisplayContext(virtualDisplay.display)),
                 virtualDisplay,
-                componentBinder,
+                params,
             )
         }
     }
@@ -157,3 +151,9 @@
         override fun build(): TaskbarSandboxComponent
     }
 }
+
+/** Include additional bindings when building a [TaskbarSandboxComponent]. */
+data class SandboxParams(
+    val systemUiProxyProvider: (Context) -> SystemUiProxy = { SystemUiProxy(it) },
+    val builderBase: TaskbarSandboxComponent.Builder = DaggerTaskbarSandboxComponent.builder(),
+)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
index 0ae710f..56c01f9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -68,7 +68,10 @@
                     touchInteractionService = mock(),
                     overviewComponentObserver = mock(),
                     taskAnimationManager = mock(),
-                    dispatcherProvider = TestDispatcherProvider(dispatcher)
+                    dispatcherProvider = TestDispatcherProvider(dispatcher),
+                    recentsDisplayModel = mock(),
+                    focusState = mock(),
+                    taskbarManager = mock(),
                 )
             )
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index c399bdb..8b17958 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -97,6 +97,8 @@
         // Set desktop mode supported
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops))
+                .thenReturn(true);
 
         mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
                 mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index b652ee8..63115bf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -150,7 +150,7 @@
 
         allSysUiStates().forEach { state ->
             val canStartGesture = !disablingStates.contains(state)
-            underTest.setSystemUiFlags(state)
+            underTest.systemUiStateFlags = state
             assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture)
         }
     }
@@ -166,7 +166,7 @@
             )
 
         stateToExpectedResult.forEach { (state, allowed) ->
-            underTest.setSystemUiFlags(state)
+            underTest.systemUiStateFlags = state
             assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed)
         }
     }
@@ -177,7 +177,7 @@
 
         allSysUiStates().forEach { state ->
             val canStartGesture = !disablingStates.contains(state)
-            underTest.setSystemUiFlags(state)
+            underTest.systemUiStateFlags = state
             assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture)
         }
     }
@@ -197,7 +197,7 @@
             )
 
         stateToExpectedResult.forEach { (state, gestureAllowed) ->
-            underTest.setSystemUiFlags(state)
+            underTest.systemUiStateFlags = state
             assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed)
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
index 722e1da..2eb2e4c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.GroupTask;
@@ -109,7 +110,7 @@
 
         mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
                 mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class),
-                mLockedUserState, () -> mThemeManager);
+                mLockedUserState, () -> mThemeManager, mock(DaggerSingletonTracker.class));
 
         mResource = mock(Resources.class);
         when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 6e9885a..b730f5b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -54,12 +54,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mTaskAnimationManager = new TaskAnimationManager(mContext,
-                RecentsAnimationDeviceState.INSTANCE.get(mContext)) {
-            @Override
-            SystemUiProxy getSystemUiProxy() {
-                return mSystemUiProxy;
-            }
-        };
+                RecentsAnimationDeviceState.INSTANCE.get(mContext), mSystemUiProxy);
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
index 61971b1..4e49aec 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
@@ -50,7 +50,8 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.DeviceConfigWrapper;
-import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.RecentsView;
@@ -75,7 +76,7 @@
     private @Mock PackageManager mMockPackageManager;
     private @Mock ContextualSearchStateManager mMockStateManager;
     private @Mock TopTaskTracker mMockTopTaskTracker;
-    private @Mock SystemUiProxy mMockSystemUiProxy;
+    private @Mock RecentsAnimationDeviceState mMockDeviceState;
     private @Mock StatsLogManager mMockStatsLogManager;
     private @Mock StatsLogManager.StatsLogger mMockStatsLogger;
     private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager;
@@ -84,6 +85,7 @@
     private @Mock RecentsViewContainer mMockRecentsViewContainer;
     private @Mock RecentsView mMockRecentsView;
     private @Mock RecentsPagedOrientationHandler mMockOrientationHandler;
+    private @Mock OverviewComponentObserver mMockOverviewComponentObserver;
     private ContextualSearchInvoker mContextualSearchInvoker;
 
     @Before
@@ -92,20 +94,21 @@
         when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true);
         Context context = spy(getApplicationContext());
         doReturn(mMockPackageManager).when(context).getPackageManager();
-        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L);
+        when(mMockDeviceState.getSystemUiStateFlags()).thenReturn(0L);
         when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{});
         when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true);
         when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true);
         when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger);
+
+        doReturn(mMockContainerInterface).when(mMockOverviewComponentObserver)
+                .getContainerInterface();
         when(mMockContainerInterface.getCreatedContainer()).thenReturn(mMockRecentsViewContainer);
         when(mMockRecentsViewContainer.getOverviewPanel()).thenReturn(mMockRecentsView);
 
-        mContextualSearchInvoker = spy(new ContextualSearchInvoker(context, mMockStateManager,
-                mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager,
-                mMockContextualSearchHapticManager, mMockContextualSearchManager
-        ));
-        doReturn(mMockContainerInterface).when(mContextualSearchInvoker)
-                .getRecentsContainerInterface();
+        mContextualSearchInvoker = new ContextualSearchInvoker(context, mMockStateManager,
+                mMockTopTaskTracker, mMockDeviceState, mMockStatsLogManager,
+                mMockContextualSearchHapticManager, mMockOverviewComponentObserver,
+                mMockContextualSearchManager);
     }
 
     @Test
@@ -148,7 +151,7 @@
 
     @Test
     public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() {
-        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS);
+        when(mMockDeviceState.getSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS);
 
         assertFalse("Expected invocation checks to fail when notification shade is showing",
                 mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
@@ -158,7 +161,7 @@
 
     @Test
     public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() {
-        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(
+        when(mMockDeviceState.getSystemUiStateFlags()).thenReturn(
                 KEYGUARD_SHOWING_SYSUI_FLAGS);
 
         assertFalse("Expected invocation checks to fail when keyguard is showing",
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index 7066d21..f2fa0c5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -447,6 +447,37 @@
     }
 
     /*
+                   5   3  [1]
+        CLEAR_ALL
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
+        assertThat(
+                getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
+            )
+            .isEqualTo(1)
+    }
+
+    /*
+                   5   3   1
+       [CLEAR_ALL]
+                   6   4   2
+    */
+    @Test
+    fun equalLengthRows_noFocused_onClearAll_pressTab_noCycle_staysOnClearAll() {
+        assertThat(
+                getNextGridPage(
+                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+                    DIRECTION_TAB,
+                    delta = 1,
+                    cycle = false,
+                )
+            )
+            .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+    }
+
+    /*
                         5   3   1
            CLEAR_ALL                FOCUSED_TASK←--DESKTOP
                         6   4   2
@@ -783,10 +814,11 @@
         bottomIds: IntArray = IntArray.wrap(2, 4, 6),
         largeTileIds: List<Int> = emptyList(),
         hasAddDesktopButton: Boolean = false,
+        cycle: Boolean = true,
     ): Int {
         val taskGridNavHelper =
             TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
-        return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
+        return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
     }
 
     private companion object {
diff --git a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
index e2ca91a..a164dad 100644
--- a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
@@ -89,7 +89,6 @@
 import org.mockito.junit.MockitoRule;
 
 import java.util.Optional;
-import java.util.function.Function;
 
 import javax.inject.Provider;
 
@@ -104,7 +103,6 @@
     private TaskAnimationManager mTaskAnimationManager;
     private InputChannelCompat.InputEventReceiver mInputEventReceiver;
     @Nullable private ResetGestureInputConsumer mResetGestureInputConsumer;
-    @NonNull private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = (state) -> null;
 
     @NonNull @Mock private TaskbarActivityContext mTaskbarActivityContext;
     @NonNull @Mock private OverviewComponentObserver mOverviewComponentObserver;
@@ -446,7 +444,7 @@
 
     @Test
     public void testNewConsumer_withSwipeUpProxyProvider_returnsProgressDelegateInputConsumer() {
-        mSwipeUpProxyProvider = (state) -> new AnimatedFloat();
+        doReturn(new AnimatedFloat()).when(mDeviceState).getSwipeUpProxy(any());
 
         assertCorrectInputConsumer(
                 this::createInputConsumer,
@@ -496,7 +494,6 @@
                 otherActivityInputConsumer -> {},
                 mInputEventReceiver,
                 mTaskbarManager,
-                mSwipeUpProxyProvider,
                 mOverviewCommandHelper,
                 event);
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index e0560e2..79d3c19 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -539,6 +539,24 @@
         }
     }
 
+    @Test
+    @PortraitLandscape
+    public void testDismissCancel() throws Exception {
+        startTestAppsWithCheck();
+        Overview overview = mLauncher.goHome().switchToOverview();
+        assertIsInState("Launcher internal state didn't switch to Overview",
+                ExpectedState.OVERVIEW);
+        final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+        OverviewTask task = overview.getCurrentTask();
+        assertNotNull("overview.getCurrentTask() returned null (2)", task);
+
+        task.dismissCancel();
+
+        runOnRecentsView(recentsView -> assertEquals(
+                "Canceling dismissing a task removed a task from Overview",
+                numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
+    }
+
     private void startTestAppsWithCheck() throws Exception {
         startTestApps();
         expectLaunchedAppState();
diff --git a/res/values/config.xml b/res/values/config.xml
index a545f0c..07f97bc 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -117,6 +117,10 @@
     <item name="swipe_up_rect_y_damping_ratio" type="dimen" format="float">0.95</item>
     <item name="swipe_up_rect_y_stiffness" type="dimen" format="float">400</item>
 
+    <!-- Expressive Dismiss -->
+    <item name="expressive_dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.6</item>
+    <item name="expressive_dismiss_task_trans_y_stiffness" type="dimen" format="float">900</item>
+
     <!-- Taskbar -->
     <!-- This is a float because it is converted to dp later in DeviceProfile -->
     <item name="taskbar_icon_size" type="dimen" format="float">0</item>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c48f140..c3cb31d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -480,6 +480,7 @@
     <dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
     <dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
     <dimen name="task_menu_edge_padding">0dp</dimen>
+    <dimen name="task_dismiss_max_undershoot">0dp</dimen>
     <dimen name="overview_task_margin">0dp</dimen>
     <dimen name="overview_actions_height">0dp</dimen>
     <dimen name="overview_actions_button_spacing">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index d3684b2..fb847f9 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -77,7 +77,6 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.PreloadIconDrawable;
-import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -484,9 +483,7 @@
     }
 
     private void setNonPendingIcon(ItemInfoWithIcon info) {
-        ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
-        int flags = (shouldUseTheme()
-                && themeManager.isMonoThemeEnabled()) ? FLAG_THEMED : 0;
+        int flags = shouldUseTheme() ? FLAG_THEMED : 0;
         // Remove badge on icons smaller than 48dp.
         if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
             flags |= FLAG_NO_BADGE;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index add8a05..900f74d 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -409,8 +409,9 @@
 
     private List<DisplayOption> filterByColumnCount(
             List<DisplayOption> allOptions, int numColumns) {
-        return allOptions.stream().filter(
-                option -> option.grid.numColumns == numColumns).toList();
+        return allOptions.stream()
+                .filter(option -> option.grid.numColumns == numColumns)
+                .collect(Collectors.toList());
     }
 
     /**
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 58fd154..04e699c 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -288,6 +288,7 @@
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
@@ -348,8 +349,7 @@
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
 
-    @Thunk
-    Hotseat mHotseat;
+    protected Hotseat mHotseat;
 
     private DropTargetBar mDropTargetBar;
 
@@ -806,10 +806,6 @@
         );
     }
 
-    public void onAssistantVisibilityChanged(float visibility) {
-        mHotseat.getQsb().setAlpha(1f - visibility);
-    }
-
     /**
      * Returns {@code true} if a new DeviceProfile is initialized, and {@code false} otherwise.
      */
@@ -2256,8 +2252,9 @@
      */
     @Override
     public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
-        bindInflatedItems(items.stream().map(i -> Pair.create(
-                i, getItemInflater().inflateItem(i, getModelWriter()))).toList(),
+        bindInflatedItems(items.stream()
+                .map(i -> Pair.create(i, getItemInflater().inflateItem(i, getModelWriter())))
+                .collect(Collectors.toList()),
                 forceAnimateIcons ? new AnimatorSet() : null);
     }
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index d93c07f..cb3a0bc 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -658,9 +658,9 @@
                         appState.getInvariantDeviceProfile().fillResIconDpi);
                 // Only fetch badge if the icon is on workspace
                 if (info.id != ItemInfo.NO_ID && badge == null) {
-                    badge = appState.getIconCache().getShortcutInfoBadge(si)
-                            .newIcon(context, ThemeManager.INSTANCE.get(context)
-                                    .isMonoThemeEnabled() ? FLAG_THEMED : 0);
+                    badge = appState.getIconCache().getShortcutInfoBadge(si).newIcon(
+                            context, ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
+                                    ? FLAG_THEMED : 0);
                 }
             }
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index b0001af..260ff9f 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -273,12 +273,7 @@
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
         mFastScrollLetterLayout = findViewById(R.id.scroll_letter_layout);
-        if (Flags.letterFastScroller()) {
-            // Set clip children to false otherwise the scroller letters will be clipped.
-            setClipChildren(false);
-        } else {
-            setClipChildren(true);
-        }
+        setClipChildren(false);
 
         mSearchContainer = inflateSearchBar();
         if (!isSearchBarFloating()) {
diff --git a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
index c303783..043c3be 100644
--- a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
+++ b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
@@ -26,6 +26,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map.Entry;
+import java.util.stream.Collectors;
 
 /**
  * Contains the logic of a reorder.
@@ -143,12 +144,14 @@
         // and not by the views hash which is "random".
         // The views are sorted twice, once for the X position and a second time for the Y position
         // to ensure same order everytime.
-        Comparator comparator = Comparator.comparing(
-                view -> ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellX()
+        Comparator<View> comparator = Comparator.comparing(
+                (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellX()
         ).thenComparing(
-                view -> ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellY()
+                (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellY()
         );
-        List<View> views = solution.map.keySet().stream().sorted(comparator).toList();
+        List<View> views = solution.map.keySet().stream()
+                .sorted(comparator)
+                .collect(Collectors.toList());
         for (View child : views) {
             if (child == ignoreView) continue;
             CellAndSpan c = solution.map.get(child);
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 9f35e4a..242220a 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -40,8 +40,8 @@
 open class ThemeManager
 @Inject
 constructor(
-    @ApplicationContext private val context: Context,
-    private val prefs: LauncherPrefs,
+    @ApplicationContext protected val context: Context,
+    protected val prefs: LauncherPrefs,
     lifecycle: DaggerSingletonTracker,
 ) {
 
@@ -53,9 +53,11 @@
         set(value) = prefs.put(THEMED_ICONS, value)
         get() = prefs.get(THEMED_ICONS)
 
-    var themeController: IconThemeController? =
-        if (isMonoThemeEnabled) MonoIconThemeController() else null
-        private set
+    val themeController: IconThemeController?
+        get() = iconState.themeController
+
+    val isIconThemeEnabled: Boolean
+        get() = themeController != null
 
     private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
 
@@ -77,12 +79,10 @@
         }
     }
 
-    private fun verifyIconState() {
+    protected fun verifyIconState() {
         val newState = parseIconState()
         if (newState == iconState) return
-
         iconState = newState
-        themeController = if (isMonoThemeEnabled) MonoIconThemeController() else null
 
         listeners.forEach { it.onThemeChanged() }
     }
@@ -105,15 +105,19 @@
         return IconState(
             iconMask = iconMask,
             folderShapeMask = shapeModel?.folderPathString ?: iconMask,
-            isMonoTheme = isMonoThemeEnabled,
+            themeController = createThemeController(),
         )
     }
 
+    protected open fun createThemeController(): IconThemeController? {
+        return if (isMonoThemeEnabled) MONO_THEME_CONTROLLER else null
+    }
+
     data class IconState(
         val iconMask: String,
         val folderShapeMask: String,
-        val isMonoTheme: Boolean,
-        val themeCode: String = if (isMonoTheme) "with-theme" else "no-theme",
+        val themeController: IconThemeController?,
+        val themeCode: String = themeController?.themeID ?: "no-theme",
     ) {
         fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
     }
@@ -135,5 +139,8 @@
         private const val ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"
         private val CONFIG_ICON_MASK_RES_ID: Int =
             Resources.getSystem().getIdentifier("config_icon_mask", "string", "android")
+
+        // Use a constant to allow equality check in verifyIconState
+        private val MONO_THEME_CONTROLLER = MonoIconThemeController()
     }
 }
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index de74ae8..003bef3 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -390,8 +390,9 @@
 
             ModelWriter writer = mApp.getModel()
                     .getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null);
-            List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
-                    Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
+            List<Pair<ItemInfo, View>> bindItems = items.stream()
+                    .map(i -> Pair.create(i, inflater.inflateItem(i, writer, null)))
+                    .collect(Collectors.toList());
             executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
         }
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index ddc775d..eab28b7 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -210,7 +211,7 @@
     }
 
     /**
-     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * Updates the deep shortcuts state in system to match out internal model, pinning any missing
      * shortcuts and unpinning any extra shortcuts.
      */
     public void updateShortcutPinnedState(Context context) {
@@ -266,6 +267,8 @@
                     || !systemShortcuts.containsAll(modelShortcuts)) {
                 // Update system state for this package
                 try {
+                    FileLog.d(TAG, "updateShortcutPinnedState:"
+                            + " Pinning Shortcuts: " + entry.getKey() + ": " + modelShortcuts);
                     context.getSystemService(LauncherApps.class).pinShortcuts(
                             entry.getKey(), new ArrayList<>(modelShortcuts), user);
                 } catch (SecurityException | IllegalStateException e) {
@@ -278,6 +281,9 @@
         systemMap.keySet().forEach(packageName -> {
             // Update system state
             try {
+                FileLog.d(TAG, "updateShortcutPinnedState:"
+                        + " Unpinning extra Shortcuts for package: " + packageName
+                        + ": " + systemMap.get(packageName));
                 context.getSystemService(LauncherApps.class).pinShortcuts(
                         packageName, Collections.emptyList(), user);
             } catch (SecurityException | IllegalStateException e) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index b291421..47f13bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -193,7 +193,8 @@
         List<DbEntry> filteredDstHotseatItems = dstHotseatItems;
         if (srcHotseatSize < destHotseatSize) {
             filteredDstHotseatItems = filteredDstHotseatItems.stream()
-                    .filter(entry -> entry.screenId < srcHotseatSize).toList();
+                    .filter(entry -> entry.screenId < srcHotseatSize)
+                    .collect(Collectors.toList());
         }
         final List<DbEntry> dstWorkspaceItems = destReader.loadAllWorkspaceEntries();
         final List<DbEntry> hotseatToBeAdded = new ArrayList<>(1);
@@ -237,9 +238,12 @@
         Collections.sort(hotseatToBeAdded);
         Collections.sort(workspaceToBeAdded);
 
-        List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
-                Collectors.toList());
-        idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList());
+        List<Integer> idsInUse = dstWorkspaceItems.stream()
+                .map(entry -> entry.id)
+                .collect(Collectors.toList());
+        idsInUse.addAll(dstHotseatItems.stream()
+                .map(entry -> entry.id)
+                .collect(Collectors.toList()));
 
         // Migrate hotseat
         solveHotseatPlacement(helper, destHotseatSize,
@@ -269,7 +273,8 @@
         int screenId = destReader.mLastScreenId + 1;
         while (!workspaceToBeAdded.isEmpty()) {
             solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
-                    workspaceToBeAdded, srcWorkspaceItems.stream().map(entry -> entry.id).toList());
+                    workspaceToBeAdded,
+                    srcWorkspaceItems.stream().map(entry -> entry.id).collect(Collectors.toList()));
             screenId++;
         }
 
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 6a8d86b..bd8c36b 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -307,7 +308,7 @@
      * Make an WorkspaceItemInfo object for a restored application or shortcut item that points
      * to a package that is not yet installed on the system.
      */
-    public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
+    public WorkspaceItemInfo getRestoredItemInfo(Intent intent, boolean isArchived) {
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.user = user;
         info.intent = intent;
@@ -317,7 +318,7 @@
             mIconCache.getTitleAndIcon(info, DEFAULT_LOOKUP_FLAG);
         }
 
-        if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
+        if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON) || isArchived) {
             String title = getTitle();
             if (!TextUtils.isEmpty(title)) {
                 info.title = Utilities.trim(title);
@@ -333,6 +334,7 @@
         info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user);
         info.itemType = itemType;
         info.status = restoreFlag;
+        if (isArchived) info.runtimeStatusFlags |= FLAG_ARCHIVED;
         return info;
     }
 
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 0138390..3a55aa7 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -90,6 +90,7 @@
 import java.io.InputStream;
 import java.io.StringReader;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Utility class which maintains an instance of Launcher database and provides utility methods
@@ -377,7 +378,7 @@
         // to run in grid migration based on if that grid already existed before migration or not.
         List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
-                .toList();
+                .collect(Collectors.toList());
 
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
                 : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
@@ -460,7 +461,7 @@
         // to run in grid migration based on if that grid already existed before migration or not.
         List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
-                .toList();
+                .collect(Collectors.toList());
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
                 : createDatabaseHelper(true /* forMigration */, targetDbName);
         try {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 6bef292..d1eceb9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
@@ -39,7 +40,6 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
@@ -238,19 +238,22 @@
                         if (itemInfo.isPromise() && isNewApkAvailable) {
                             boolean isTargetValid = !cn.getClassName().equals(
                                     IconCache.EMPTY_CLASS_NAME);
-                            if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                            if (itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
                                 List<ShortcutInfo> shortcut =
                                         new ShortcutRequest(context, mUser)
                                                 .forPackage(cn.getPackageName(),
                                                         itemInfo.getDeepShortcutId())
                                                 .query(ShortcutRequest.PINNED);
-                                if (shortcut.isEmpty()) {
+                                if (shortcut.isEmpty()
+                                        && !(Flags.restoreArchivedShortcuts()
+                                            && !itemInfo.isArchived())
+                                ) {
                                     isTargetValid = false;
                                     if (DEBUG) {
                                         Log.d(TAG, "Pinned Shortcut not found for updated"
                                                 + " package=" + itemInfo.getTargetPackage());
                                     }
-                                } else {
+                                } else if (!shortcut.isEmpty()) {
                                     if (DEBUG) {
                                         Log.d(TAG, "Found pinned shortcut for updated"
                                                 + " package=" + itemInfo.getTargetPackage()
@@ -269,7 +272,7 @@
                                     || itemInfo.isArchived())) {
                                 if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                     infoUpdated = true;
-                                } else if (itemInfo.hasPromiseIconUi()) {
+                                } else if (shouldRemoveRestoredShortcut(itemInfo)) {
                                     removedShortcuts.add(itemInfo.id);
                                     if (DEBUG) {
                                         FileLog.w(TAG, "Removing restored shortcut promise icon"
@@ -436,7 +439,7 @@
      */
     private boolean updateWorkspaceItemIntent(Context context,
             WorkspaceItemInfo si, String packageName) {
-        if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+        if (si.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
             // Do not update intent for deep shortcuts as they contain additional information
             // about the shortcut.
             return false;
@@ -452,6 +455,15 @@
         return false;
     }
 
+    private boolean shouldRemoveRestoredShortcut(WorkspaceItemInfo itemInfo) {
+        if (itemInfo.hasPromiseIconUi() && !Flags.restoreArchivedShortcuts()) {
+            return true;
+        }
+        return Flags.restoreArchivedShortcuts()
+                && !itemInfo.isArchived()
+                && itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT;
+    }
+
     private String getOpString() {
         return switch (mOp) {
             case OP_NONE -> "NONE";
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
index 2e4f75f..56e9e43 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.kt
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
@@ -17,6 +17,7 @@
 
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherModel.ModelUpdateTask
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.icons.CacheableShortcutInfo
@@ -59,8 +60,11 @@
             val infoWrapper = ApplicationInfoWrapper(context, packageName, user)
             if (shortcuts.isEmpty()) {
                 // Verify that the app is indeed installed.
-                if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
-                    // App is not installed or archived, ignoring package events
+                if (
+                    (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) ||
+                        (Flags.restoreArchivedShortcuts() && infoWrapper.isArchived())
+                ) {
+                    // App is not installed or is archived, ignoring package events
                     return
                 }
             }
@@ -75,7 +79,7 @@
             val nonPinnedIds: MutableSet<String> = HashSet(allLauncherKnownIds)
             val updatedWorkspaceItemInfos = ArrayList<WorkspaceItemInfo>()
             for (fullDetails in shortcuts) {
-                if (!fullDetails.isPinned) {
+                if (!fullDetails.isPinned && !Flags.restoreArchivedShortcuts()) {
                     continue
                 }
                 val shortcutId = fullDetails.id
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 90f11a3..3919eb7 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -194,27 +194,36 @@
         if (intent.`package` == null) {
             intent.`package` = targetPkg
         }
+        val isPreArchived = appInfoWrapper.isArchived() && c.restoreFlag != 0
+
         // else if cn == null => can't infer much, leave it
         // else if !validPkg => could be restored icon or missing sd-card
         when {
-            !TextUtils.isEmpty(targetPkg) && !validTarget -> {
+            !TextUtils.isEmpty(targetPkg) && (!validTarget || isPreArchived) -> {
                 // Points to a valid app (superset of cn != null) but the apk
                 // is not available.
                 when {
-                    c.restoreFlag != 0 -> {
+                    c.restoreFlag != 0 || isPreArchived -> {
                         // Package is not yet available but might be
                         // installed later.
-                        FileLog.d(TAG, "package not yet restored: $targetPkg")
+                        FileLog.d(
+                            TAG,
+                            "package not yet restored: $targetPkg, itemType=${c.itemType}" +
+                                "isPreArchived=$isPreArchived, restoreFlag=${c.restoreFlag}",
+                        )
                         tempPackageKey.update(targetPkg, c.user)
                         when {
                             c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
                                 // Restore has started once.
                             }
-                            installingPkgs.containsKey(tempPackageKey) -> {
+                            installingPkgs.containsKey(tempPackageKey) || isPreArchived -> {
                                 // App restore has started. Update the flag
                                 c.restoreFlag =
                                     c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
-                                FileLog.d(TAG, "restore started for installing app: $targetPkg")
+                                FileLog.d(
+                                    TAG,
+                                    "restore started for installing app: $targetPkg, itemType=${c.itemType}",
+                                )
                                 c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
                             }
                             else -> {
@@ -253,9 +262,18 @@
             }
         }
         if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
+            FileLog.d(
+                TAG,
+                "restore flag set AND WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0, setting valid target to false: $targetPkg, itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+            )
             validTarget = false
         }
-        if (validTarget) {
+        if (validTarget && !isPreArchived) {
+            FileLog.d(
+                TAG,
+                "valid target true, marking restored: $targetPkg," +
+                    " itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+            )
             // The shortcut points to a valid target (either no target
             // or something which is ready to be used)
             c.markRestored()
@@ -265,7 +283,7 @@
         when {
             c.restoreFlag != 0 -> {
                 // Already verified above that user is same as default user
-                info = c.getRestoredItemInfo(intent)
+                info = c.getRestoredItemInfo(intent, isPreArchived)
             }
             c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
                 info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt
index e620ac9..c0fe4fd 100644
--- a/src/com/android/launcher3/model/data/AppPairInfo.kt
+++ b/src/com/android/launcher3/model/data/AppPairInfo.kt
@@ -23,6 +23,7 @@
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.logger.LauncherAtom
 import com.android.launcher3.views.ActivityContext
+import java.util.stream.Collectors
 
 /** A type of app collection that launches multiple apps into split screen. */
 class AppPairInfo() : CollectionInfo() {
@@ -54,7 +55,7 @@
 
     /** Returns the app pair's member apps as an ArrayList of [ItemInfo]. */
     override fun getContents(): ArrayList<ItemInfo> =
-        ArrayList(contents.stream().map { it as ItemInfo }.toList())
+        ArrayList(contents.stream().map { it as ItemInfo }.collect(Collectors.toList()))
 
     /** Returns the app pair's member apps as an ArrayList of [WorkspaceItemInfo]. */
     override fun getAppContents(): ArrayList<WorkspaceItemInfo> = contents
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 7fb0152..ff40f30 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -323,7 +323,7 @@
      * Returns a FastBitmapDrawable with the icon and context theme applied
      */
     public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
-        if (!ThemeManager.INSTANCE.get(context).isMonoThemeEnabled()) {
+        if (!ThemeManager.INSTANCE.get(context).isIconThemeEnabled()) {
             creationFlags &= ~FLAG_THEMED;
         }
         FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index f56888b..dc42920 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -176,7 +176,7 @@
         // At this point idp.dbFile contains the name of the dbFile from the previous phone
         return LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> context.getDatabasePath(dbName).exists())
-                .toList();
+                .collect(Collectors.toList());
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index d042b1d..4ccf16b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -222,7 +222,8 @@
         if (shouldShowFullPageView(recommendations)) {
             // Show all widgets in single page with unlimited available height.
             return setRecommendations(
-                    recommendations.values().stream().flatMap(Collection::stream).toList(),
+                    recommendations.values().stream().flatMap(Collection::stream)
+                            .collect(Collectors.toList()),
                     deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
                     cellPadding);
 
@@ -369,7 +370,7 @@
         // Show only those widgets that were displayed when user first opened the picker.
         if (!mDisplayedWidgets.isEmpty()) {
             filteredRecommendedWidgets = recommendedWidgets.stream().filter(
-                    w -> mDisplayedWidgets.contains(w.componentName)).toList();
+                    w -> mDisplayedWidgets.contains(w.componentName)).collect(Collectors.toList());
         }
         Context context = getContext();
         LayoutInflater inflater = LayoutInflater.from(context);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index ab0f9a7..7a218ae 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -87,6 +87,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
 /**
@@ -650,7 +651,7 @@
                 mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
                         .getRecommendations()
                         .values().stream()
-                        .flatMap(Collection::stream).toList();
+                        .flatMap(Collection::stream).collect(Collectors.toList());
                 mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
                         mRecommendedWidgets,
                         mDeviceProfile,
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 0bcab60..216f4d4 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -41,6 +41,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /** A {@link TableLayout} for showing recommended widgets. */
 public final class WidgetsRecommendationTableLayout extends TableLayout {
@@ -163,6 +164,7 @@
         }
 
         // Perform re-ordering once we have filtered out recommendations that fit.
-        return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR).toList();
+        return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR)
+                .collect(Collectors.toList());
     }
 }
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
index df72f07..1134781 100644
--- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -95,7 +95,7 @@
         List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
                 sortedWidgetItems, context, dp, rowPx,
                 cellPadding);
-        return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
+        return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).collect(Collectors.toList());
     }
 
     /**
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
index 43b7b68..85c1156 100644
--- a/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
@@ -21,11 +21,13 @@
 import com.android.launcher3.FakeLauncherPrefs
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.icons.mono.MonoIconThemeController
 import com.android.launcher3.util.AllModulesForTest
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.FakePrefsModule
 import com.android.launcher3.util.SandboxApplication
 import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
 import dagger.Component
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -55,12 +57,13 @@
         themeManager.isMonoThemeEnabled = true
         TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
         assertTrue(themeManager.isMonoThemeEnabled)
-        assertTrue(themeManager.iconState.isMonoTheme)
+        assertThat(themeManager.iconState.themeController)
+            .isInstanceOf(MonoIconThemeController::class.java)
 
         themeManager.isMonoThemeEnabled = false
         TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
         assertFalse(themeManager.isMonoThemeEnabled)
-        assertFalse(themeManager.iconState.isMonoTheme)
+        assertThat(themeManager.iconState.themeController).isNull()
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
index fb6d038..c6863f4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
@@ -24,8 +24,12 @@
 import android.content.pm.ShortcutInfo
 import android.os.Process.myUserHandle
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.icons.BitmapInfo
@@ -42,6 +46,7 @@
 import java.util.function.Predicate
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -55,6 +60,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ShortcutsChangedTaskTest {
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     private lateinit var shortcutsChangedTask: ShortcutsChangedTask
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var context: SandboxModelContext
@@ -131,7 +138,8 @@
     }
 
     @Test
-    fun `When installed unpinned shortcut is found then remove from workspace`() {
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When installed unpinned shortcut is found with Flag off then remove from workspace`() {
         // Given
         shortcuts =
             listOf(
@@ -163,6 +171,37 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When installed unpinned shortcut is found with Flag on then keep in workspace`() {
+        // Given
+        shortcuts =
+            listOf(
+                mock<ShortcutInfo>().apply {
+                    whenever(isPinned).thenReturn(false)
+                    whenever(id).thenReturn(expectedShortcutId)
+                }
+            )
+        val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+        items.put(expectedWai.id, expectedWai)
+        doReturn(
+                ApplicationInfo().apply {
+                    enabled = true
+                    flags = flags or FLAG_INSTALLED
+                    isArchived = false
+                }
+            )
+            .whenever(launcherApps)
+            .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+        doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+        // When
+        shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+        // Then
+        verify(mockAppState.iconCache)
+            .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+        verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
+    }
+
+    @Test
     fun `When shortcut app is uninstalled then skip handling`() {
         // Given
         shortcuts =
@@ -192,7 +231,8 @@
     }
 
     @Test
-    fun `When archived pinned shortcut is found then keep in workspace`() {
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When archived pinned shortcut is found with flag off then keep in workspace`() {
         // Given
         shortcuts =
             listOf(
@@ -222,7 +262,8 @@
     }
 
     @Test
-    fun `When archived unpinned shortcut is found then keep in workspace`() {
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When archived unpinned shortcut is found with flag off then keep in workspace`() {
         // Given
         shortcuts =
             listOf(
@@ -310,4 +351,34 @@
         assertThat(modelHelper.bgDataModel.deepShortcutMap).doesNotContainKey(expectedKey)
         verify(mockTaskController, times(0)).bindDeepShortcuts(eq(modelHelper.bgDataModel))
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When restoring archived shortcut with flag on then skip handling`() {
+        // Given
+        shortcuts =
+            listOf(
+                mock<ShortcutInfo>().apply {
+                    whenever(isPinned).thenReturn(true)
+                    whenever(id).thenReturn(expectedShortcutId)
+                }
+            )
+        val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+        items.put(expectedWai.id, expectedWai)
+        doReturn(
+                ApplicationInfo().apply {
+                    enabled = true
+                    flags = flags or FLAG_INSTALLED
+                    isArchived = true
+                }
+            )
+            .whenever(launcherApps)
+            .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+        doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+        // When
+        shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+        // Then
+        verify(mockTaskController, times(0)).deleteAndBindComponentsRemoved(any(), any())
+        verify(mockTaskController, times(0)).bindUpdatedWorkspaceItems(any())
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index da87dfc..7a403e1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -18,16 +18,19 @@
 
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
-import android.content.Context
 import android.content.Intent
+import android.content.pm.ApplicationInfo
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
 import android.os.Process
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.util.LongSparseArray
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
@@ -44,9 +47,14 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
 import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORE_STARTED
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutKey
 import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.ContentWriter
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.UserIconInfo
@@ -55,6 +63,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -66,6 +75,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -73,20 +83,23 @@
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorTest {
 
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
     @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
     @Mock private lateinit var mockBgDataModel: BgDataModel
-    @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockAppState: LauncherAppState
     @Mock private lateinit var mockPmHelper: PackageManagerHelper
-    @Mock private lateinit var mockLauncherApps: LauncherApps
     @Mock private lateinit var mockCursor: LoaderCursor
     @Mock private lateinit var mockUserCache: UserCache
     @Mock private lateinit var mockUserManagerState: UserManagerState
     @Mock private lateinit var mockWidgetInflater: WidgetInflater
 
-    private var intent: Intent = Intent()
-    private var mUserHandle: UserHandle = UserHandle(0)
+    lateinit var mModelHelper: LauncherModelHelper
+    lateinit var mContext: SandboxModelContext
+    lateinit var mLauncherApps: LauncherApps
+    private var mIntent: Intent = Intent()
+    private var mUserHandle: UserHandle = Process.myUserHandle()
     private var mIconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf()
     private var mComponentName: ComponentName = ComponentName("package", "class")
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
@@ -101,40 +114,35 @@
 
     @Before
     fun setup() {
-        mUserHandle = UserHandle(0)
+        mModelHelper = LauncherModelHelper()
+        mContext = mModelHelper.sandboxContext
+        mLauncherApps =
+            mContext.spyService(LauncherApps::class.java).apply {
+                doReturn(true).whenever(this).isPackageEnabled("package", mUserHandle)
+                doReturn(true).whenever(this).isActivityEnabled(mComponentName, mUserHandle)
+            }
+        mUserHandle = Process.myUserHandle()
         mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
         mockWorkspaceInfo = mock<WorkspaceItemInfo>()
         mockBgDataModel = mock<BgDataModel>()
         mComponentName = ComponentName("package", "class")
         mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
-        intent =
+        mIntent =
             Intent().apply {
                 component = mComponentName
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
-        mockContext =
-            mock<Context>().apply {
-                whenever(packageManager).thenReturn(mock())
-                whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
-                whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
-                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
-            }
         mockAppState =
             mock<LauncherAppState>().apply {
-                whenever(context).thenReturn(mockContext)
+                whenever(context).thenReturn(mContext)
                 whenever(iconCache).thenReturn(mock())
                 whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
             }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(intent)
+                    .thenReturn(mIntent)
             }
         mockCursor =
             mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
@@ -143,9 +151,9 @@
                 id = 1
                 restoreFlag = 1
                 serialNumber = 101
-                whenever(parseIntent()).thenReturn(intent)
+                whenever(parseIntent()).thenReturn(mIntent)
                 whenever(markRestored()).doAnswer { restoreFlag = 0 }
-                whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
+                whenever(updater().put(Favorites.INTENT, mIntent.toUri(0)).commit()).thenReturn(1)
                 whenever(getAppShortcutInfo(any(), any(), any(), any()))
                     .thenReturn(mockWorkspaceInfo)
                 whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
@@ -177,7 +185,7 @@
         memoryLogger: LoaderMemoryLogger? = null,
         userCache: UserCache = mockUserCache,
         userManagerState: UserManagerState = mockUserManagerState,
-        launcherApps: LauncherApps = mockLauncherApps,
+        launcherApps: LauncherApps = mLauncherApps,
         shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
         app: LauncherAppState = mockAppState,
         bgDataModel: BgDataModel = mockBgDataModel,
@@ -244,7 +252,7 @@
     fun `When app has null target package then mark deleted`() {
 
         // Given
-        intent.apply {
+        mIntent.apply {
             component = null
             `package` = null
         }
@@ -264,8 +272,8 @@
 
         // Given
         mComponentName = ComponentName("", "")
-        intent.component = mComponentName
-        intent.`package` = ""
+        mIntent.component = mComponentName
+        mIntent.`package` = ""
 
         // When
         itemProcessorUnderTest = createWorkspaceItemProcessorUnderTest()
@@ -298,15 +306,14 @@
     fun `When fallback Activity found for app then mark restored`() {
 
         // Given
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
-            }
+        mLauncherApps.apply {
+            whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+            whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+        }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(intent)
+                    .thenReturn(mIntent)
             }
 
         // When
@@ -317,7 +324,7 @@
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
-        verify(mockCursor.updater().put(Favorites.INTENT, intent.toUri(0))).commit()
+        verify(mockCursor.updater().put(Favorites.INTENT, mIntent.toUri(0))).commit()
         assertThat(mIconRequestInfos).containsExactly(mockIconRequestInfo)
         verify(mockCursor).checkAndAddItem(mockWorkspaceInfo, mockBgDataModel, null)
     }
@@ -326,11 +333,10 @@
     fun `When app with disabled activity and no fallback found then mark deleted`() {
 
         // Given
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
-            }
+        mLauncherApps.apply {
+            whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+            whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+        }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
@@ -358,11 +364,11 @@
 
     @Test
     fun `When valid Pinned Deep Shortcut then mark restored`() {
-
         // Given
         mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
         val expectedShortcutInfo =
             mock<ShortcutInfo>().apply {
+                whenever(userHandle).thenReturn(mUserHandle)
                 whenever(id).thenReturn("")
                 whenever(`package`).thenReturn("")
                 whenever(activity).thenReturn(mock())
@@ -372,7 +378,7 @@
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
             }
-        val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+        val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
         mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
         mIconRequestInfos = mutableListOf()
 
@@ -393,6 +399,67 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When Archived Deep Shortcut with flag on then mark restored`() {
+        // Given
+        val mockContentWriter: ContentWriter = mock()
+        val mockAppInfo: ApplicationInfo =
+            mock<ApplicationInfo>().apply {
+                isArchived = true
+                enabled = true
+            }
+        val expectedRestoreFlag = FLAG_RESTORED_ICON or FLAG_RESTORE_STARTED
+        doReturn(mockAppInfo).whenever(mLauncherApps).getApplicationInfo(any(), any(), any())
+        whenever(mockContentWriter.put(Favorites.RESTORED, expectedRestoreFlag))
+            .thenReturn(mockContentWriter)
+        whenever(mockContentWriter.commit()).thenReturn(1)
+        mockCursor.apply {
+            itemType = ITEM_TYPE_DEEP_SHORTCUT
+            restoreFlag = restoreFlag or FLAG_RESTORED_ICON
+            whenever(updater()).thenReturn(mockContentWriter)
+        }
+        mIconRequestInfos = mutableListOf()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        assertThat(mockCursor.restoreFlag and FLAG_RESTORED_ICON).isEqualTo(FLAG_RESTORED_ICON)
+        assertThat(mockCursor.restoreFlag and FLAG_RESTORE_STARTED).isEqualTo(FLAG_RESTORE_STARTED)
+        assertThat(mIconRequestInfos).isNotEmpty()
+        assertThat(mAllDeepShortcuts).isEmpty()
+        verify(mockContentWriter).put(Favorites.RESTORED, expectedRestoreFlag)
+        verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When Archived Deep Shortcut with flag off then remove`() {
+        // Given
+        mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        mIconRequestInfos = mutableListOf()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        assertWithMessage("item restoreFlag should be set to 0")
+            .that(mockCursor.restoreFlag)
+            .isEqualTo(0)
+        assertThat(mIconRequestInfos).isEmpty()
+        assertThat(mAllDeepShortcuts).isEmpty()
+        verify(mockCursor)
+            .markDeleted(
+                "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+                "shortcut_not_found",
+            )
+    }
+
+    @Test
     fun `When Pinned Deep Shortcut is not stored in ShortcutManager re-query by Shortcut ID`() {
         // Given
         mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
@@ -406,8 +473,9 @@
                 whenever(disabledMessage).thenReturn("")
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
+                whenever(userHandle).thenReturn(mUserHandle)
             }
-        whenever(mockLauncherApps.getShortcuts(any(), any())).thenReturn(listOf(si))
+        doReturn(listOf(si)).whenever(mLauncherApps).getShortcuts(any(), any())
         mKeyToPinnedShortcutsMap.clear()
         mIconRequestInfos = mutableListOf()
 
@@ -417,12 +485,12 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockLauncherApps).getShortcuts(any(), any())
+        verify(mLauncherApps).getShortcuts(any(), any())
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         verify(mockCursor).markRestored()
-        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+        verify(mockCursor).checkAndAddItem(any(), any(), eq(null))
     }
 
     @Test
@@ -469,11 +537,11 @@
             }
         mIconRequestInfos = mutableListOf()
         // Make sure shortcuts map has expected key from expected package
-        intent.`package` = mComponentName.packageName
-        val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+        mIntent.`package` = mComponentName.packageName
+        val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
         mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
         // set intent package back to null to test scenario
-        intent.`package` = null
+        mIntent.`package` = null
 
         // When
         itemProcessorUnderTest =
@@ -656,7 +724,7 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+        verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
     }
 
     @Test
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 2431ef5..1158521 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -211,6 +211,36 @@
     }
 
     /**
+     * Starts dismissing the task by swiping up, then cancels, and task springs back to start.
+     */
+    public void dismissCancel() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to start dismissing an overview task then cancel")) {
+            verifyActiveContainer();
+            int taskCountBeforeDismiss = mOverview.getTaskCount();
+            mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss);
+
+            final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
+            final int centerX = taskBounds.centerX();
+            final int centerY = taskBounds.bottom - 1;
+            final int endCenterY = centerY - (taskBounds.height() / 4);
+            mLauncher.executeAndWaitForLauncherEvent(
+                    // Set slowDown to true so we do not fling the task at the end of the drag, as
+                    // we want it to cancel and return back to the origin. We use 30 steps to
+                    // perform the gesture slowly as well, to avoid flinging.
+                    () -> mLauncher.linearGesture(centerX, centerY, centerX, endCenterY,
+                            /* steps= */ 30, /* slowDown= */ true,
+                            LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
+                    event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(
+                            event.getClassName()),
+                    () -> "Canceling swipe to dismiss did not end with task at origin.",
+                    "cancel swiping to dismiss");
+
+        }
+    }
+
+    /**
      * Clicks the task.
      */
     public LaunchedAppState open() {