Merge "Migrate TopTaskTracker instance to DaggerSingletonObject" into main
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 5a8302c..0703a61 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -74,9 +74,9 @@
             if (visibleTasksCount != field) {
                 val wasVisible = field > 0
                 val isVisible = visibleTasksCount > 0
-                val wereDesktopTasksVisibleBefore = areDesktopTasksVisible()
+                val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview()
                 field = visibleTasksCount
-                val areDesktopTasksVisibleNow = areDesktopTasksVisible()
+                val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview()
                 if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
                     notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
                 }
@@ -192,9 +192,9 @@
             )
         }
         if (overviewStateEnabled != inOverviewState) {
-            val wereDesktopTasksVisibleBefore = areDesktopTasksVisible()
+            val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview()
             inOverviewState = overviewStateEnabled
-            val areDesktopTasksVisibleNow = areDesktopTasksVisible()
+            val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview()
             if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
                 notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
             }
@@ -261,7 +261,7 @@
             this.backgroundStateEnabled = backgroundStateEnabled
             if (this.backgroundStateEnabled) {
                 markLauncherResumed()
-            } else if (areDesktopTasksVisible() && !gestureInProgress) {
+            } else if (areDesktopTasksVisibleAndNotInOverview() && !gestureInProgress) {
                 // Switching out of background state. If desktop tasks are visible, pause launcher.
                 markLauncherPaused()
             }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 70c53fa..d3ac411 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -170,7 +170,6 @@
 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.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
@@ -282,7 +281,6 @@
                         getDepthController(), getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         () -> onStateBack());
-        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext());
         if (DesktopModeStatus.canEnterDesktopMode(this)) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
@@ -290,8 +288,8 @@
         }
         overviewPanel.init(mActionsView, mSplitSelectStateController,
                 mDesktopRecentsTransitionController);
-        mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
-                mSplitSelectStateController, deviceState);
+        mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(
+                this, mSplitSelectStateController);
         mSplitToWorkspaceController = new SplitToWorkspaceController(this,
                 mSplitSelectStateController);
         mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
@@ -564,9 +562,13 @@
             mSplitSelectStateController.onDestroy();
         }
 
+        RecentsView recentsView = getOverviewPanel();
+        if (recentsView != null) {
+            recentsView.destroy();
+        }
+
         super.onDestroy();
         mHotseatPredictionController.destroy();
-        mSplitWithKeyboardShortcutController.onDestroy();
         if (mViewCapture != null) mViewCapture.close();
         removeBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
     }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 03394ef..124be41 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -43,6 +43,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_EXIT_DESKTOP_MODE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
+import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
@@ -114,6 +116,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.logging.StatsLogManager;
@@ -170,8 +173,6 @@
 
 import com.google.android.msdl.data.model.MSDLToken;
 
-import kotlin.Unit;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -181,6 +182,8 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
@@ -196,6 +199,7 @@
     // Fraction of the scroll and transform animation in which the current task fades out
     private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
 
+    protected final RecentsAnimationDeviceState mDeviceState;
     protected final BaseContainerInterface<STATE, RECENTS_CONTAINER> mContainerInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ContextInitListener mContextInitListener;
@@ -371,12 +375,13 @@
 
     private final MSDLPlayerWrapper mMSDLPlayerWrapper;
 
-    public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public AbsSwipeUpHandler(Context context,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer,
             MSDLPlayerWrapper msdlPlayerWrapper) {
-        super(context, deviceState, gestureState);
+        super(context, gestureState);
+        mDeviceState = RecentsAnimationDeviceState.INSTANCE.get(mContext);
         mContainerInterface = gestureState.getContainerInterface();
         mContextInitListener =
                 mContainerInterface.createActivityInitListener(this::onActivityInit);
@@ -594,7 +599,7 @@
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
         if (mGestureState.getEndTarget() != HOME) {
             Runnable initAnimFactory = () -> {
-                mAnimationFactory = mContainerInterface.prepareRecentsUI(mDeviceState,
+                mAnimationFactory = mContainerInterface.prepareRecentsUI(
                         mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
                 maybeUpdateRecentsAttachedState(false /* animate */);
                 if (mGestureState.getEndTarget() != null) {
@@ -660,12 +665,9 @@
         mGestureState.getContainerInterface().setOnDeferredActivityLaunchCallback(
                 mOnDeferredActivityLaunch);
 
-        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
-                () -> {
-                    mDeviceState.getRotationTouchHelper()
-                            .onEndTargetCalculated(mGestureState.getEndTarget(),
-                                    mContainerInterface);
-                });
+        mGestureState.runOnceAtState(STATE_END_TARGET_SET, () ->
+                RotationTouchHelper.INSTANCE.get(mContext)
+                        .onEndTargetCalculated(mGestureState.getEndTarget(), mContainerInterface));
 
         notifyGestureStarted();
     }
@@ -705,7 +707,7 @@
         if (mRecentsView == null) {
             return;
         }
-        mRecentsView.onGestureAnimationStart(runningTasks, mDeviceState.getRotationTouchHelper());
+        mRecentsView.onGestureAnimationStart(runningTasks);
         TaskView currentPageTaskView = mRecentsView.getCurrentPageTaskView();
         if (currentPageTaskView != null) {
             mPreviousTaskViewType = currentPageTaskView.getType();
@@ -1185,11 +1187,13 @@
         if (endTarget != HOME) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
         } else {
+            AccessibilityManagerCompat.sendStateEventToTest(mContext, NORMAL_STATE_ORDINAL);
             InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
         }
         if (endTarget != RECENTS) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         } else {
+            AccessibilityManagerCompat.sendStateEventToTest(mContext, OVERVIEW_STATE_ORDINAL);
             InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         }
 
@@ -1980,7 +1984,7 @@
                 }
                 // Make sure recents is in its final state
                 maybeUpdateRecentsAttachedState(false);
-                mContainerInterface.onSwipeUpToHomeComplete(mDeviceState);
+                mContainerInterface.onSwipeUpToHomeComplete();
             }
         });
         if (mRecentsAnimationTargets != null) {
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index e2ebaa5..b20518c 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -131,7 +131,7 @@
     }
 
     public abstract BaseContainerInterface.AnimationFactory prepareRecentsUI(
-            RecentsAnimationDeviceState deviceState, boolean activityVisible,
+            boolean activityVisible,
             Consumer<AnimatorControllerWithResistance> callback);
 
     public abstract ContextInitListener createActivityInitListener(
@@ -151,11 +151,10 @@
 
     public abstract void onLaunchTaskFailed();
 
-    public abstract void onExitOverview(RotationTouchHelper deviceState,
-            Runnable exitRunnable);
+    public abstract void onExitOverview(Runnable exitRunnable);
 
     /** Called when the animation to home has fully settled. */
-    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {}
+    public void onSwipeUpToHomeComplete() {}
 
     /**
      * Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 94f4920..d60dab6 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep
 
 import android.view.View
+import com.android.internal.jank.Cuj
 import com.android.launcher3.AbstractFloatingViewHelper
 import com.android.launcher3.R
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
@@ -24,6 +25,7 @@
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskContainer
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 
@@ -31,7 +33,7 @@
 class DesktopSystemShortcut(
     container: RecentsViewContainer,
     private val taskContainer: TaskContainer,
-    abstractFloatingViewHelper: AbstractFloatingViewHelper
+    abstractFloatingViewHelper: AbstractFloatingViewHelper,
 ) :
     SystemShortcut<RecentsViewContainer>(
         R.drawable.ic_desktop,
@@ -39,15 +41,17 @@
         container,
         taskContainer.itemInfo,
         taskContainer.taskView,
-        abstractFloatingViewHelper
+        abstractFloatingViewHelper,
     ) {
     override fun onClick(view: View) {
+        InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU)
         dismissTaskMenuView()
         val recentsView = mTarget.getOverviewPanel<RecentsView<*, *>>()
         recentsView.moveTaskToDesktop(
             taskContainer,
-            DesktopModeTransitionSource.APP_FROM_OVERVIEW
+            DesktopModeTransitionSource.APP_FROM_OVERVIEW,
         ) {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU)
             mTarget.statsLogManager
                 .logger()
                 .withItemInfo(taskContainer.itemInfo)
@@ -64,7 +68,7 @@
             return object : TaskShortcutFactory {
                 override fun getShortcuts(
                     container: RecentsViewContainer,
-                    taskContainer: TaskContainer
+                    taskContainer: TaskContainer,
                 ): List<DesktopSystemShortcut>? {
                     return if (!DesktopModeStatus.canEnterDesktopMode(container.asContext())) null
                     else if (!taskContainer.task.isDockable) null
@@ -73,7 +77,7 @@
                             DesktopSystemShortcut(
                                 container,
                                 taskContainer,
-                                abstractFloatingViewHelper
+                                abstractFloatingViewHelper,
                             )
                         )
                 }
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index b787399..d8e0296 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -79,9 +79,9 @@
 
     /** 6 */
     @Override
-    public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+    public AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
-        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        notifyRecentsOfOrientation();
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
         factory.initBackgroundStateUI();
         return factory;
@@ -142,12 +142,12 @@
     }
 
     @Override
-    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+    public void onExitOverview(Runnable exitRunnable) {
         final StateManager<RecentsState, RecentsActivity> stateManager =
                 getCreatedContainer().getStateManager();
         if (stateManager.getState() == HOME) {
             exitRunnable.run();
-            notifyRecentsOfOrientation(deviceState);
+            notifyRecentsOfOrientation();
             return;
         }
 
@@ -158,7 +158,7 @@
                         // Are we going from Recents to Workspace?
                         if (toState == HOME) {
                             exitRunnable.run();
-                            notifyRecentsOfOrientation(deviceState);
+                            notifyRecentsOfOrientation();
                             stateManager.removeStateListener(this);
                         }
                     }
@@ -197,11 +197,9 @@
         }
     }
 
-    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+    private void notifyRecentsOfOrientation() {
         // reset layout on swipe to home
-        RecentsView recentsView = getCreatedContainer().getOverviewPanel();
-        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
-                rotationTouchHelper.getDisplayRotation());
+        getCreatedContainer().getOverviewPanel().reapplyActiveRotation();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 1e857ca..331580c 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -101,11 +101,11 @@
 
     private boolean mAppCanEnterPip;
 
-    public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public FallbackSwipeHandler(Context context,
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer,
             MSDLPlayerWrapper msdlPlayerWrapper) {
-        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+        super(context, taskAnimationManager, gestureState, touchTimeMs,
                 continuingLastGesture, inputConsumer, msdlPlayerWrapper);
 
         mRunningOverHome = mGestureState.getRunningTask() != null
@@ -216,8 +216,7 @@
         if (mRunningOverHome) {
             if (DisplayController.getNavigationMode(mContext).hasGestures) {
                 mRecentsView.onGestureAnimationStartOnHome(
-                        mGestureState.getRunningTask().getPlaceholderTasks(),
-                        mDeviceState.getRotationTouchHelper());
+                        mGestureState.getRunningTask().getPlaceholderTasks());
             }
         } else {
             super.notifyGestureAnimationStartToRecents();
diff --git a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
index f7836b0..35630ef 100644
--- a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
@@ -80,10 +80,9 @@
 
     /** 6 */
     @Override
-    public BaseWindowInterface.AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState
-            deviceState, boolean activityVisible,
+    public BaseWindowInterface.AnimationFactory prepareRecentsUI(boolean activityVisible,
             Consumer<AnimatorControllerWithResistance> callback) {
-        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        notifyRecentsOfOrientation();
         BaseWindowInterface.DefaultAnimationFactory factory =
                 new BaseWindowInterface.DefaultAnimationFactory(callback);
         factory.initBackgroundStateUI();
@@ -153,12 +152,12 @@
     }
 
     @Override
-    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+    public void onExitOverview(Runnable exitRunnable) {
         final StateManager<RecentsState, RecentsWindowManager> stateManager =
                 getCreatedContainer().getStateManager();
         if (stateManager.getState() == HOME) {
             exitRunnable.run();
-            notifyRecentsOfOrientation(deviceState);
+            notifyRecentsOfOrientation();
             return;
         }
 
@@ -169,7 +168,7 @@
                         // Are we going from Recents to Workspace?
                         if (toState == HOME) {
                             exitRunnable.run();
-                            notifyRecentsOfOrientation(deviceState);
+                            notifyRecentsOfOrientation();
                             stateManager.removeStateListener(this);
                         }
                     }
@@ -208,11 +207,9 @@
         }
     }
 
-    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+    private void notifyRecentsOfOrientation() {
         // reset layout on swipe to home
-        RecentsView recentsView = getCreatedContainer().getOverviewPanel();
-        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
-                rotationTouchHelper.getDisplayRotation());
+        ((RecentsView) getCreatedContainer().getOverviewPanel()).reapplyActiveRotation();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/InputConsumerUtils.kt b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
index 558178f..c340c92 100644
--- a/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
+++ b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
@@ -332,14 +332,7 @@
                     reasonPrefix,
                     SUBSTRING_PREFIX,
                 )
-                base =
-                    AccessibilityInputConsumer(
-                        context,
-                        deviceState,
-                        gestureState,
-                        base,
-                        inputMonitorCompat,
-                    )
+                base = AccessibilityInputConsumer(context, deviceState, base, inputMonitorCompat)
             }
         } else {
             val reasonPrefix = "device is not in gesture navigation mode"
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index d193fee..ac0aa76 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -80,7 +80,7 @@
     }
 
     @Override
-    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
+    public void onSwipeUpToHomeComplete() {
         QuickstepLauncher launcher = getCreatedContainer();
         if (launcher == null) {
             return;
@@ -93,7 +93,7 @@
         MAIN_EXECUTOR.getHandler().post(launcher.getStateManager()::reapplyState);
 
         launcher.getRootView().setForceHideBackArrow(false);
-        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        notifyRecentsOfOrientation();
     }
 
     @Override
@@ -106,9 +106,9 @@
     }
 
     @Override
-    public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+    public AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
-        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        notifyRecentsOfOrientation();
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
             protected void createBackgroundToOverviewAnim(QuickstepLauncher activity,
@@ -227,7 +227,7 @@
 
 
     @Override
-    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+    public void onExitOverview(Runnable exitRunnable) {
         final StateManager<LauncherState, Launcher> stateManager =
                 getCreatedContainer().getStateManager();
         stateManager.addStateListener(
@@ -237,18 +237,16 @@
                         // Are we going from Recents to Workspace?
                         if (toState == LauncherState.NORMAL) {
                             exitRunnable.run();
-                            notifyRecentsOfOrientation(deviceState);
+                            notifyRecentsOfOrientation();
                             stateManager.removeStateListener(this);
                         }
                     }
                 });
     }
 
-    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+    private void notifyRecentsOfOrientation() {
         // reset layout on swipe to home
-        RecentsView recentsView = getCreatedContainer().getOverviewPanel();
-        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
-                rotationTouchHelper.getDisplayRotation());
+        ((RecentsView) getCreatedContainer().getOverviewPanel()).reapplyActiveRotation();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 7af0618..c60d3e8 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -67,11 +67,10 @@
 public class LauncherSwipeHandlerV2 extends AbsSwipeUpHandler<
         QuickstepLauncher, RecentsView<QuickstepLauncher, LauncherState>, LauncherState> {
 
-    public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
-            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
-            boolean continuingLastGesture, InputConsumerController inputConsumer,
-            MSDLPlayerWrapper msdlPlayerWrapper) {
-        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+    public LauncherSwipeHandlerV2(Context context, TaskAnimationManager taskAnimationManager,
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture,
+            InputConsumerController inputConsumer, MSDLPlayerWrapper msdlPlayerWrapper) {
+        super(context, taskAnimationManager, gestureState, touchTimeMs,
                 continuingLastGesture, inputConsumer, msdlPlayerWrapper);
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index b34b502..5e8ea37 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -363,14 +363,6 @@
     }
 
     @Override
-    public void onUiChangedWhileSleeping() {
-        super.onUiChangedWhileSleeping();
-        // Dismiss recents and navigate to home if the device goes to sleep
-        // while in recents.
-        startHome();
-    }
-
-    @Override
     protected void onResume() {
         super.onResume();
         AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
@@ -460,6 +452,10 @@
 
     @Override
     protected void onDestroy() {
+        RecentsView recentsView = getOverviewPanel();
+        if (recentsView != null) {
+            recentsView.destroy();
+        }
         super.onDestroy();
         ACTIVITY_TRACKER.onContextDestroyed(this);
         mActivityLaunchAnimationRunner = null;
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 145773d..865cc47 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -36,6 +36,7 @@
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 import com.android.wm.shell.recents.IRecentsAnimationController;
@@ -71,7 +72,7 @@
      * currently being animated.
      */
     public ThumbnailData screenshotTask(int taskId) {
-        return mController.screenshotTask(taskId);
+        return ActivityManagerWrapper.getInstance().takeTaskThumbnail(taskId);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index e296449..d4305a5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -67,7 +67,9 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.NavigationMode;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -88,9 +90,8 @@
 /**
  * Manages the state of the system during a swipe up gesture.
  */
-public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener {
-
-    private static final String TAG = "RecentsAnimationDeviceState";
+public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener,
+        SafeCloseable {
 
     static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
 
@@ -98,6 +99,9 @@
     private static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 3f;
     private static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 1.414f;
 
+    public static MainThreadInitializedObject<RecentsAnimationDeviceState> INSTANCE =
+            new MainThreadInitializedObject<>(RecentsAnimationDeviceState::new);
+
     private final Context mContext;
     private final DisplayController mDisplayController;
 
@@ -130,41 +134,21 @@
     private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
     private boolean mExclusionListenerRegistered;
 
-    public RecentsAnimationDeviceState(Context context) {
-        this(context, false, GestureExclusionManager.INSTANCE);
-    }
-
-    public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
-        this(context, isInstanceForTouches, GestureExclusionManager.INSTANCE);
+    private RecentsAnimationDeviceState(Context context) {
+        this(context, GestureExclusionManager.INSTANCE);
     }
 
     @VisibleForTesting
     RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) {
-        this(context, false, exclusionManager);
-    }
-
-    /**
-     * @param isInstanceForTouches {@code true} if this is the persistent instance being used for
-     *                                   gesture touch handling
-     */
-    RecentsAnimationDeviceState(
-            Context context, boolean isInstanceForTouches,
-            GestureExclusionManager exclusionManager) {
         mContext = context;
         mDisplayController = DisplayController.INSTANCE.get(context);
         mExclusionManager = exclusionManager;
         mContextualSearchStateManager = ContextualSearchStateManager.INSTANCE.get(context);
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
-        if (isInstanceForTouches) {
-            // rotationTouchHelper doesn't get initialized after being destroyed, so only destroy
-            // if primary TouchInteractionService instance needs to be destroyed.
-            mRotationTouchHelper.init();
-            runOnDestroy(mRotationTouchHelper::destroy);
-        }
 
         // Register for exclusion updates
-        runOnDestroy(() -> unregisterExclusionListener());
+        runOnDestroy(this::unregisterExclusionListener);
 
         // Register for display changes changes
         mDisplayController.addChangeListener(this);
@@ -225,10 +209,8 @@
         mOnDestroyActions.add(action);
     }
 
-    /**
-     * Cleans up all the registered listeners and receivers.
-     */
-    public void destroy() {
+    @Override
+    public void close() {
         for (Runnable r : mOnDestroyActions) {
             r.run();
         }
@@ -603,10 +585,6 @@
         return mPipIsActive;
     }
 
-    public RotationTouchHelper getRotationTouchHelper() {
-        return mRotationTouchHelper;
-    }
-
     /** Returns whether IME is rendering nav buttons, and IME is currently showing. */
     public boolean isImeRenderingNavButtons() {
         return mCanImeRenderGesturalNavButtons && mMode == NO_BUTTON
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index d073580..1977dfa 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -37,8 +37,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.graphics.ThemeManager;
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.icons.IconProvider.IconChangeListener;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
@@ -66,9 +67,9 @@
  * Singleton class to load and manage recents model.
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
-        TaskStackChangeListener, TaskVisualsChangeListener, TaskVisualsChangeNotifier,
-        SafeCloseable {
+public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListener,
+        TaskVisualsChangeListener, TaskVisualsChangeNotifier,
+        ThemeChangeListener, SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -85,8 +86,10 @@
     private final TaskIconCache mIconCache;
     private final TaskThumbnailCache mThumbnailCache;
     private final ComponentCallbacks mCallbacks;
+    private final ThemeManager mThemeManager;
 
     private final TaskStackChangeListeners mTaskStackChangeListeners;
+    private final SafeCloseable mIconChangeCloseable;
 
     private RecentsModel(Context context) {
         this(context, new IconProvider(context));
@@ -103,13 +106,15 @@
                 new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider),
                 new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
                 iconProvider,
-                TaskStackChangeListeners.getInstance());
+                TaskStackChangeListeners.getInstance(),
+                ThemeManager.INSTANCE.get(context));
     }
 
     @VisibleForTesting
     RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
             TaskThumbnailCache thumbnailCache, IconProvider iconProvider,
-            TaskStackChangeListeners taskStackChangeListeners) {
+            TaskStackChangeListeners taskStackChangeListeners,
+            ThemeManager themeManager) {
         mContext = context;
         mTaskList = taskList;
         mIconCache = iconCache;
@@ -133,7 +138,10 @@
 
         mTaskStackChangeListeners = taskStackChangeListeners;
         mTaskStackChangeListeners.registerTaskStackListener(this);
-        iconProvider.registerIconChangeListener(this, MAIN_EXECUTOR.getHandler());
+        mIconChangeCloseable = iconProvider.registerIconChangeListener(
+                this::onAppIconChanged, MAIN_EXECUTOR.getHandler());
+        mThemeManager = themeManager;
+        themeManager.addChangeListener(this);
     }
 
     public TaskIconCache getIconCache() {
@@ -268,8 +276,7 @@
         }
     }
 
-    @Override
-    public void onAppIconChanged(String packageName, UserHandle user) {
+    private void onAppIconChanged(String packageName, UserHandle user) {
         mIconCache.invalidateCacheEntries(packageName, user);
         for (TaskVisualsChangeListener listener : mThumbnailChangeListeners) {
             listener.onTaskIconChanged(packageName, user);
@@ -284,7 +291,7 @@
     }
 
     @Override
-    public void onSystemIconStateChanged(String iconState) {
+    public void onThemeChanged() {
         mIconCache.clearCache();
     }
 
@@ -394,6 +401,8 @@
         }
         mIconCache.removeTaskVisualsChangeListener();
         mTaskStackChangeListeners.unregisterTaskStackListener(this);
+        mIconChangeCloseable.close();
+        mThemeManager.removeChangeListener(this);
     }
 
     private boolean isCachePreloadingEnabled() {
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 909cc35..f54b655 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -47,7 +47,6 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
 
 /**
  * Helper class for transforming touch events
@@ -57,16 +56,14 @@
     public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE =
             new MainThreadInitializedObject<>(RotationTouchHelper::new);
 
-    private OrientationTouchTransformer mOrientationTouchTransformer;
-    private DisplayController mDisplayController;
-    private int mDisplayId;
+    private final OrientationTouchTransformer mOrientationTouchTransformer;
+    private final DisplayController mDisplayController;
+    private final int mDisplayId;
     private int mDisplayRotation;
 
-    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
-
     private NavigationMode mMode = THREE_BUTTONS;
 
-    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
+    private final TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
         @Override
         public void onRecentTaskListFrozenChanged(boolean frozen) {
             mTaskListFrozen = frozen;
@@ -93,7 +90,7 @@
         }
     };
 
-    private Runnable mExitOverviewRunnable = new Runnable() {
+    private final Runnable mExitOverviewRunnable = new Runnable() {
         @Override
         public void run() {
             mInOverview = false;
@@ -107,7 +104,7 @@
      * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
      * the navbar.
      */
-    private OrientationEventListener mOrientationListener;
+    private final OrientationEventListener mOrientationListener;
     private int mSensorRotation = ROTATION_0;
     /**
      * This is the configuration of the foreground app or the app that will be in the foreground
@@ -120,7 +117,6 @@
      * would indicate the user's intention to rotate the foreground app.
      */
     private boolean mPrioritizeDeviceRotation = false;
-    private Runnable mOnDestroyFrozenTaskRunnable;
     /**
      * Set to true when user swipes to recents. In recents, we ignore the state of the recents
      * task list being frozen or not to allow the user to keep interacting with nav bar rotation
@@ -131,23 +127,8 @@
     private boolean mTaskListFrozen;
     private final Context mContext;
 
-    /**
-     * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests
-     * where multiple instances of RotationTouchHelper are being created. b/177316094
-     */
-    private boolean mNeedsInit = true;
-
     private RotationTouchHelper(Context context) {
         mContext = context;
-        if (mNeedsInit) {
-            init();
-        }
-    }
-
-    public void init() {
-        if (!mNeedsInit) {
-            return;
-        }
         mDisplayController = DisplayController.INSTANCE.get(mContext);
         Resources resources = mContext.getResources();
         mDisplayId = DEFAULT_DISPLAY;
@@ -158,8 +139,7 @@
         // Register for navigation mode changes
         mDisplayController.addChangeListener(this);
         DisplayController.Info info = mDisplayController.getInfo();
-        onDisplayInfoChangedInternal(info, CHANGE_ALL, hasGestures(info.getNavigationMode()));
-        runOnDestroy(() -> mDisplayController.removeChangeListener(this));
+        onDisplayInfoChanged(context, info, CHANGE_ALL);
 
         mOrientationListener = new OrientationEventListener(mContext) {
             @Override
@@ -180,40 +160,14 @@
                 }
             }
         };
-        runOnDestroy(() -> mOrientationListener.disable());
-        mNeedsInit = false;
-    }
-
-    private void setupOrientationSwipeHandler() {
-        TaskStackChangeListeners.getInstance().registerTaskStackListener(mFrozenTaskListener);
-        mOnDestroyFrozenTaskRunnable = () -> TaskStackChangeListeners.getInstance()
-                .unregisterTaskStackListener(mFrozenTaskListener);
-        runOnDestroy(mOnDestroyFrozenTaskRunnable);
-    }
-
-    private void destroyOrientationSwipeHandlerCallback() {
-        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
-        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
-    }
-
-    private void runOnDestroy(Runnable action) {
-        mOnDestroyActions.add(action);
     }
 
     @Override
     public void close() {
-        destroy();
-    }
-
-    /**
-     * Cleans up all the registered listeners and receivers.
-     */
-    public void destroy() {
-        for (Runnable r : mOnDestroyActions) {
-            r.run();
-        }
-        mNeedsInit = true;
-        mOnDestroyActions.clear();
+        mDisplayController.removeChangeListener(this);
+        mOrientationListener.disable();
+        TaskStackChangeListeners.getInstance()
+                .unregisterTaskStackListener(mFrozenTaskListener);
     }
 
     public boolean isTaskListFrozen() {
@@ -264,10 +218,6 @@
 
     @Override
     public void onDisplayInfoChanged(Context context, Info info, int flags) {
-        onDisplayInfoChangedInternal(info, flags, false);
-    }
-
-    private void onDisplayInfoChangedInternal(Info info, int flags, boolean forceRegister) {
         if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN | CHANGE_NAVIGATION_MODE
                 | CHANGE_SUPPORTED_BOUNDS)) != 0) {
             mDisplayRotation = info.rotation;
@@ -300,12 +250,12 @@
             mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
                     mContext.getResources());
 
-            if (forceRegister || (!hasGestures(mMode) && hasGestures(newMode))) {
-                setupOrientationSwipeHandler();
-            } else if (hasGestures(mMode) && !hasGestures(newMode)) {
-                destroyOrientationSwipeHandlerCallback();
+            TaskStackChangeListeners.getInstance()
+                    .unregisterTaskStackListener(mFrozenTaskListener);
+            if (hasGestures(newMode)) {
+                TaskStackChangeListeners.getInstance()
+                        .registerTaskStackListener(mFrozenTaskListener);
             }
-
             mMode = newMode;
         }
     }
@@ -363,7 +313,7 @@
                 // If we're in landscape w/o ever quickswitching, show the navbar in landscape
                 enableMultipleRegions(true);
             }
-            containerInterface.onExitOverview(this, mExitOverviewRunnable);
+            containerInterface.onExitOverview(mExitOverviewRunnable);
         } else if (endTarget == GestureState.GestureEndTarget.HOME
                 || endTarget == GestureState.GestureEndTarget.ALL_APPS) {
             enableMultipleRegions(false);
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index f813d9a..f5593b0 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -58,7 +58,7 @@
 import java.util.function.Consumer;
 
 public abstract class SwipeUpAnimationLogic implements
-        RecentsAnimationCallbacks.RecentsAnimationListener{
+        RecentsAnimationCallbacks.RecentsAnimationListener {
 
     protected static final Rect TEMP_RECT = new Rect();
     protected final RemoteTargetGluer mTargetGluer;
@@ -66,7 +66,6 @@
     protected DeviceProfile mDp;
 
     protected final Context mContext;
-    protected final RecentsAnimationDeviceState mDeviceState;
     protected final GestureState mGestureState;
 
     protected RemoteTargetHandle[] mRemoteTargetHandles;
@@ -85,20 +84,19 @@
 
     protected boolean mIsSwipeForSplit;
 
-    public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState) {
+    public SwipeUpAnimationLogic(Context context, GestureState gestureState) {
         mContext = context;
-        mDeviceState = deviceState;
         mGestureState = gestureState;
         updateIsGestureForSplit(TopTaskTracker.INSTANCE.get(context)
                 .getRunningSplitTaskIds().length);
 
         mTargetGluer = new RemoteTargetGluer(mContext, mGestureState.getContainerInterface());
         mRemoteTargetHandles = mTargetGluer.getRemoteTargetHandles();
+        RotationTouchHelper rotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
         runActionOnRemoteHandles(remoteTargetHandle ->
                 remoteTargetHandle.getTaskViewSimulator().getOrientationState().update(
-                        mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
-                        mDeviceState.getRotationTouchHelper().getDisplayRotation()
+                        rotationTouchHelper.getCurrentActiveRotation(),
+                        rotationTouchHelper.getDisplayRotation()
                 ));
     }
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index aea02af..a06029b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -552,8 +552,8 @@
         // Initialize anything here that is needed in direct boot mode.
         // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
-        mDeviceState = new RecentsAnimationDeviceState(this, true);
-        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+        mDeviceState = RecentsAnimationDeviceState.INSTANCE.get(this);
+        mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(this);
         mAllAppsActionManager = new AllAppsActionManager(
                 this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
         mTrackpadsConnected = new ActiveTrackpadList(this, () -> {
@@ -715,7 +715,6 @@
             mOverviewComponentObserver.removeOverviewChangeListener(mOverviewChangeListener);
         }
         disposeEventHandlers("TouchInteractionService onDestroy()");
-        mDeviceState.destroy();
         SystemUiProxy.INSTANCE.get(this).clearProxy();
 
         mAllAppsActionManager.onDestroy();
@@ -1158,21 +1157,21 @@
 
     private AbsSwipeUpHandler createLauncherSwipeHandler(
             GestureState gestureState, long touchTimeMs) {
-        return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
+        return new LauncherSwipeHandlerV2(this, mTaskAnimationManager,
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this));
     }
 
     private AbsSwipeUpHandler createFallbackSwipeHandler(
             GestureState gestureState, long touchTimeMs) {
-        return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+        return new FallbackSwipeHandler(this, mTaskAnimationManager,
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this));
     }
 
     private AbsSwipeUpHandler createRecentsWindowSwipeHandler(
             GestureState gestureState, long touchTimeMs) {
-        return new RecentsWindowSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+        return new RecentsWindowSwipeHandler(this, mTaskAnimationManager,
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this));
     }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index b76e39a..76da4af 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -45,7 +45,6 @@
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.GestureState;
-import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -114,12 +113,11 @@
      * to the home task. This allows us to handle quick-switch similarly to a quick-switching
      * from a foreground task.
      */
-    public void onGestureAnimationStartOnHome(Task[] homeTask,
-            RotationTouchHelper rotationTouchHelper) {
+    public void onGestureAnimationStartOnHome(Task[] homeTask) {
         // TODO(b/195607777) General fallback love, but this might be correct
         //  Home task should be defined as the front-most task info I think?
         mHomeTask = homeTask.length > 0 ? homeTask[0] : null;
-        onGestureAnimationStart(homeTask, rotationTouchHelper);
+        onGestureAnimationStart(homeTask);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index a9259d9..505f2cb 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -24,6 +24,8 @@
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.quickstep.DisplayModel
 import com.android.quickstep.FallbackWindowInterface
 import com.android.quickstep.dagger.QuickstepBaseAppComponent
@@ -31,7 +33,9 @@
 import javax.inject.Inject
 
 @LauncherAppSingleton
-class RecentsDisplayModel @Inject constructor(@ApplicationContext context: Context) :
+class RecentsDisplayModel
+@Inject
+constructor(@ApplicationContext context: Context, tracker: DaggerSingletonTracker) :
     DisplayModel<RecentsDisplayResource>(context) {
 
     companion object {
@@ -47,17 +51,38 @@
 
     init {
         if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
-            displayManager.registerDisplayListener(displayListener, Handler.getMain())
-            createDisplayResource(Display.DEFAULT_DISPLAY)
+            MAIN_EXECUTOR.execute {
+                displayManager.registerDisplayListener(displayListener, Handler.getMain())
+                // In the scenario where displays were added before this display listener was
+                // registered, we should store the RecentsDisplayResources for those displays
+                // directly.
+                displayManager.displays
+                    .filter { getDisplayResource(it.displayId) == null }
+                    .forEach { storeRecentsDisplayResource(it.displayId, it) }
+            }
+            tracker.addCloseable { destroy() }
         }
     }
 
     override fun createDisplayResource(displayId: Int) {
-        if (DEBUG) Log.d(TAG, "create: displayId=$displayId")
+        if (DEBUG) Log.d(TAG, "createDisplayResource: displayId=$displayId")
         getDisplayResource(displayId)?.let {
             return
         }
         val display = displayManager.getDisplay(displayId)
+        if (display == null) {
+            if (DEBUG)
+                Log.w(
+                    TAG,
+                    "createDisplayResource: could not create display for displayId=$displayId",
+                    Exception(),
+                )
+            return
+        }
+        storeRecentsDisplayResource(displayId, display)
+    }
+
+    private fun storeRecentsDisplayResource(displayId: Int, display: Display) {
         displayResourceArray[displayId] =
             RecentsDisplayResource(displayId, context.createDisplayContext(display))
     }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 9a38ff6..5d99aec 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -42,6 +42,7 @@
 import com.android.launcher3.statemanager.StatefulContainer
 import com.android.launcher3.taskbar.TaskbarUIController
 import com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL
+import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL
 import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL
 import com.android.launcher3.util.ContextTracker
 import com.android.launcher3.util.DisplayController
@@ -162,6 +163,7 @@
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
         callbacks?.removeListener(recentsAnimationListener)
         recentsWindowTracker.onContextDestroyed(this)
+        recentsView?.destroy()
     }
 
     override fun startHome() {
@@ -350,17 +352,23 @@
             cleanupRecentsWindow()
         }
         when (state) {
-            HOME ->
+            HOME,
+            BG_LAUNCHER ->
                 AccessibilityManagerCompat.sendStateEventToTest(baseContext, NORMAL_STATE_ORDINAL)
             DEFAULT ->
                 AccessibilityManagerCompat.sendStateEventToTest(baseContext, OVERVIEW_STATE_ORDINAL)
+            OVERVIEW_SPLIT_SELECT ->
+                AccessibilityManagerCompat.sendStateEventToTest(
+                    baseContext,
+                    OVERVIEW_SPLIT_SELECT_ORDINAL,
+                )
         }
     }
 
     private fun getStateName(state: RecentsState?): String {
         return when (state) {
             null -> "NULL"
-            DEFAULT -> "default"
+            DEFAULT -> "DEFAULT"
             MODAL_TASK -> "MODAL_TASK"
             BACKGROUND_APP -> "BACKGROUND_APP"
             HOME -> "HOME"
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
index afc8879..12bae53 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -66,7 +66,6 @@
 import com.android.quickstep.AbsSwipeUpHandler;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.fallback.FallbackRecentsView;
@@ -110,11 +109,10 @@
 
     private boolean mAppCanEnterPip;
 
-    public RecentsWindowSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
-            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
-            boolean continuingLastGesture, InputConsumerController inputConsumer,
-            MSDLPlayerWrapper msdlPlayerWrapper) {
-        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+    public RecentsWindowSwipeHandler(Context context, TaskAnimationManager taskAnimationManager,
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture,
+            InputConsumerController inputConsumer, MSDLPlayerWrapper msdlPlayerWrapper) {
+        super(context, taskAnimationManager, gestureState, touchTimeMs,
                 continuingLastGesture, inputConsumer, msdlPlayerWrapper);
 
         mRecentsDisplayModel = RecentsDisplayModel.getINSTANCE().get(context);
@@ -257,8 +255,7 @@
         if (mRunningOverHome) {
             if (DisplayController.getNavigationMode(mContext).hasGestures) {
                 mRecentsView.onGestureAnimationStartOnHome(
-                        mGestureState.getRunningTask().getPlaceholderTasks(),
-                        mDeviceState.getRotationTouchHelper());
+                        mGestureState.getRunningTask().getPlaceholderTasks());
             }
         } else {
             super.notifyGestureAnimationStartToRecents();
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index ec6efcb..4e5d037 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -29,9 +29,9 @@
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.R;
-import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -47,7 +47,7 @@
     private final VelocityTracker mVelocityTracker;
     private final MotionPauseDetector mMotionPauseDetector;
     private final RecentsAnimationDeviceState mDeviceState;
-    private final GestureState mGestureState;
+    private final RotationTouchHelper mRotationHelper;
 
     private final float mMinGestureDistance;
     private final float mMinFlingVelocity;
@@ -57,7 +57,7 @@
     private float mTotalY;
 
     public AccessibilityInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumer delegate, InputMonitorCompat inputMonitor) {
+            InputConsumer delegate, InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
         mContext = context;
         mVelocityTracker = VelocityTracker.obtain();
@@ -65,7 +65,7 @@
                 .getDimension(R.dimen.accessibility_gesture_min_swipe_distance);
         mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
         mDeviceState = deviceState;
-        mGestureState = gestureState;
+        mRotationHelper = RotationTouchHelper.INSTANCE.get(context);
 
         mMotionPauseDetector = new MotionPauseDetector(context);
     }
@@ -102,8 +102,8 @@
             case ACTION_POINTER_DOWN: {
                 if (mState == STATE_INACTIVE) {
                     int pointerIndex = ev.getActionIndex();
-                    if (mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev,
-                            pointerIndex) && mDelegate.allowInterceptByParent()) {
+                    if (mRotationHelper.isInSwipeUpTouchRegion(ev, pointerIndex)
+                            && mDelegate.allowInterceptByParent()) {
                         setActive(ev);
 
                         mActivePointerId = ev.getPointerId(pointerIndex);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index f3f73c0..390d097 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
@@ -38,8 +39,11 @@
 /**
  * Listens for touch events on the bubble bar.
  */
+// TODO(b/385928447): remove debug logs with Log.d
 public class BubbleBarInputConsumer implements InputConsumer {
 
+    private static final String TAG = "BubbleBarInputConsumer";
+
     private final BubbleStashController mBubbleStashController;
     private final BubbleBarViewController mBubbleBarViewController;
     @Nullable
@@ -81,6 +85,9 @@
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
                 mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed();
+                Log.d(TAG,
+                        "ACTION_DOWN stashedOrCollapsed=" + mStashedOrCollapsedOnDown + " downPos="
+                                + mDownPos);
                 if (mBubbleBarSwipeController != null) {
                     mBubbleBarSwipeController.start();
                 }
@@ -88,6 +95,7 @@
             case MotionEvent.ACTION_MOVE:
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
                 if (pointerIndex == INVALID_POINTER_ID) {
+                    Log.d(TAG, "ACTION_MOVE skip, invalid pointer id");
                     break;
                 }
                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
@@ -96,10 +104,14 @@
                 float dY = mLastPos.y - mDownPos.y;
                 if (!mPassedTouchSlop) {
                     mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
+                    if (mPassedTouchSlop) {
+                        Log.d(TAG, "ACTION_MOVE passed touch slop pos=" + mLastPos);
+                    }
                 }
                 if (mBubbleBarSwipeController != null) {
                     mBubbleBarSwipeController.swipeTo(dY);
                     if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) {
+                        Log.d(TAG, "ACTION_MOVE swipe gesture, pilfering");
                         mPilfered = true;
                         // Bubbles is handling the swipe so make sure no one else gets it.
                         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
@@ -112,13 +124,22 @@
                         && mBubbleBarSwipeController.isSwipeGesture();
                 // Anything less than a long-press is a tap
                 boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForLongPress;
+                Log.d(TAG, "ACTION_UP swipeUp=" + swipeUpOnBubbleHandle + " isInTapTime="
+                        + isWithinTapTime + " passedTouchSlop=" + mPassedTouchSlop
+                        + " stashedOrCollapsedOnDown=" + mStashedOrCollapsedOnDown);
                 if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
+                    Log.d(TAG, "ACTION_UP showing bubble bar");
                     // Taps on the handle / collapsed state should open the bar
                     mBubbleStashController.showBubbleBar(
                             /* expandBubbles= */ true, /* bubbleBarGesture= */ true);
+                } else {
+                    Log.d(TAG, "ACTION_UP nothing to do");
                 }
                 break;
+            case MotionEvent.ACTION_CANCEL:
+                Log.d(TAG, "ACTION_CANCEL");
+                break;
         }
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
             cleanupAfterMotionEvent();
@@ -126,6 +147,7 @@
     }
 
     private void cleanupAfterMotionEvent() {
+        Log.d(TAG, "cleaning up passedSlop=" + mPassedTouchSlop + " pilfered=" + mPilfered);
         if (mBubbleBarSwipeController != null) {
             mBubbleBarSwipeController.finish();
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index b66d4cb..01f5522 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -52,6 +52,7 @@
 import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
 import com.android.quickstep.util.TransformParams;
@@ -82,7 +83,7 @@
             getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
 
     private final Context mContext;
-    private final RecentsAnimationDeviceState mDeviceState;
+    private final RotationTouchHelper mRotationTouchHelper;
     private final TaskAnimationManager mTaskAnimationManager;
     private final GestureState mGestureState;
     private final float mTouchSlopSquared;
@@ -110,14 +111,14 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             InputMonitorCompat inputMonitorCompat) {
         mContext = context;
-        mDeviceState = deviceState;
         mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
-        mTouchSlopSquared = mDeviceState.getSquaredTouchSlop();
+        mTouchSlopSquared = deviceState.getSquaredTouchSlop();
         mTransformParams = new TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
         mMaxTranslationY = context.getResources().getDimensionPixelSize(
                 R.dimen.device_locked_y_offset);
+        mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(mContext);
 
         // Do not use DeviceProfile as the user data might be locked
         mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
@@ -152,7 +153,7 @@
                 if (!mThresholdCrossed) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
+                    if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         int action = ev.getAction();
                         ev.setAction(ACTION_CANCEL);
                         finishTouchTracking(ev);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index c4198db..870a479 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -156,8 +156,7 @@
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
-        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
-
+        mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(this);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 1c4e7a7..e265e61 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -50,7 +50,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.OverviewComponentObserver;
-import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RemoteTargetGluer;
 import com.android.quickstep.SwipeUpAnimationLogic;
 import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim;
@@ -85,10 +84,8 @@
 
     SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
         super(tutorialFragment, tutorialType);
-        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
-        mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
+        mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext,
                 new GestureState(OverviewComponentObserver.INSTANCE.get(mContext), -1));
-        deviceState.destroy();
 
         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext)
                 .getDeviceProfile(mContext)
@@ -311,9 +308,8 @@
 
     class ViewSwipeUpAnimation extends SwipeUpAnimationLogic {
 
-        ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState,
-                             GestureState gestureState) {
-            super(context, deviceState, gestureState);
+        ViewSwipeUpAnimation(Context context, GestureState gestureState) {
+            super(context, gestureState);
             mRemoteTargetHandles[0] = new RemoteTargetGluer.RemoteTargetHandle(
                     mRemoteTargetHandles[0].getTaskViewSimulator(), new FakeTransformParams());
 
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index dd721e1..946ca2a 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -16,9 +16,10 @@
 
 package com.android.quickstep.logging;
 
-import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 import static com.android.launcher3.LauncherPrefs.getDevicePrefs;
 import static com.android.launcher3.LauncherPrefs.getPrefs;
+import static com.android.launcher3.graphics.ThemeManager.KEY_THEMED_ICONS;
+import static com.android.launcher3.graphics.ThemeManager.THEMED_ICONS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED;
@@ -29,7 +30,6 @@
 import static com.android.launcher3.model.PredictionUpdateTask.LAST_PREDICTION_ENABLED;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
-import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
 
 import android.content.Context;
 import android.content.SharedPreferences;
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 0ba4083..425c4fe 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -40,7 +40,6 @@
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
@@ -55,18 +54,15 @@
 
     private final QuickstepLauncher mLauncher;
     private final SplitSelectStateController mController;
-    private final RecentsAnimationDeviceState mDeviceState;
     private final OverviewComponentObserver mOverviewComponentObserver;
 
     private final int mSplitPlaceholderSize;
     private final int mSplitPlaceholderInset;
 
-    public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
-            SplitSelectStateController controller,
-            RecentsAnimationDeviceState deviceState) {
+    public SplitWithKeyboardShortcutController(
+            QuickstepLauncher launcher, SplitSelectStateController controller) {
         mLauncher = launcher;
         mController = controller;
-        mDeviceState = deviceState;
         mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher);
 
         mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
@@ -104,10 +100,6 @@
         });
     }
 
-    public void onDestroy() {
-        mDeviceState.destroy();
-    }
-
     private class SplitWithKeyboardShortcutRecentsAnimationListener implements
             RecentsAnimationCallbacks.RecentsAnimationListener {
 
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 7a7a7f9..d6fe049 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -50,7 +50,6 @@
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -264,9 +263,8 @@
     }
 
     @Override
-    public void onGestureAnimationStart(Task[] runningTasks,
-            RotationTouchHelper rotationTouchHelper) {
-        super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
+    public void onGestureAnimationStart(Task[] runningTasks) {
+        super.onGestureAnimationStart(runningTasks);
         if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) {
             // TODO: b/333533253 - Remove after flag rollout
             DesktopVisibilityController.INSTANCE.get(mContainer).setRecentsGestureStart();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ab96474..892b89d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -248,7 +248,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -850,8 +849,6 @@
 
     private final Matrix mTmpMatrix = new Matrix();
 
-    private int mTaskViewCount = 0;
-
     @Nullable
     public TaskView getFirstTaskView() {
         return mUtils.getFirstTaskView();
@@ -1241,8 +1238,15 @@
             mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
         }
         reset();
+    }
+
+    /**
+     * Execute clean-up logic needed when the view is destroyed.
+     */
+    public void destroy() {
+        Log.d(TAG, "destroy");
         if (enableRefactorTaskThumbnail()) {
-            mHelper.onDetachedFromWindow();
+            mHelper.onDestroy();
             RecentsDependencies.destroy();
         }
     }
@@ -1254,11 +1258,9 @@
         // - It's the initial taskview for entering split screen, we only pretend to dismiss the
         // task
         // - It's the focused task to be moved to the front, we immediately re-add the task
-        if (child instanceof TaskView) {
-            mTaskViewCount = Math.max(0, --mTaskViewCount);
-            if (child != mSplitHiddenTaskView && child != mMovingTaskView) {
-                clearAndRecycleTaskView((TaskView) child);
-            }
+        if (child instanceof TaskView && child != mSplitHiddenTaskView
+                && child != mMovingTaskView) {
+            clearAndRecycleTaskView((TaskView) child);
         }
     }
 
@@ -1279,9 +1281,6 @@
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
-        if (child instanceof TaskView) {
-            mTaskViewCount++;
-        }
         child.setAlpha(mContentAlpha);
         // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
         // child direction back to match system settings.
@@ -2096,7 +2095,11 @@
     }
 
     public int getTaskViewCount() {
-        return mTaskViewCount;
+        int taskViewCount = getChildCount();
+        if (indexOfChild(mClearAllButton) != -1) {
+            taskViewCount--;
+        }
+        return taskViewCount;
     }
 
     /**
@@ -2781,14 +2784,12 @@
     /**
      * Called when a gesture from an app is starting.
      */
-    public void onGestureAnimationStart(
-            Task[] runningTasks, RotationTouchHelper rotationTouchHelper) {
+    public void onGestureAnimationStart(Task[] runningTasks) {
         Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
         mActiveGestureRunningTasks = runningTasks;
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
-            setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
-                    rotationTouchHelper.getDisplayRotation());
+            reapplyActiveRotation();
             // Force update to ensure the initial task size is computed even if the orientation has
             // not changed.
             updateSizeAndPadding();
@@ -4158,16 +4159,15 @@
 
                         if (showAsGrid) {
                             // Rebalance tasks in the grid
-                            int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
-                            if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
-                                final TaskView taskView = requireTaskViewAt(
-                                        highestVisibleTaskIndex);
-
+                            TaskView highestVisibleTaskView = getHighestVisibleTaskView();
+                            if (highestVisibleTaskView != null) {
                                 boolean shouldRebalance;
                                 int screenStart = getPagedOrientationHandler().getPrimaryScroll(
                                         RecentsView.this);
-                                int taskStart = getPagedOrientationHandler().getChildStart(taskView)
-                                        + (int) taskView.getOffsetAdjustment(/*gridEnabled=*/ true);
+                                int taskStart = getPagedOrientationHandler().getChildStart(
+                                        highestVisibleTaskView)
+                                        + (int) highestVisibleTaskView.getOffsetAdjustment(
+                                                /*gridEnabled=*/true);
 
                                 // Rebalance only if there is a maximum gap between the task and the
                                 // screen's edge; this ensures that rebalanced tasks are outside the
@@ -4180,7 +4180,7 @@
                                             RecentsView.this);
                                     int taskSize = (int) (
                                             getPagedOrientationHandler().getMeasuredSize(
-                                                    taskView) * taskView
+                                                    highestVisibleTaskView) * highestVisibleTaskView
                                                     .getSizeAdjustment(/*fullscreenEnabled=*/
                                                             false));
                                     int taskEnd = taskStart + taskSize;
@@ -4189,7 +4189,7 @@
                                 }
 
                                 if (shouldRebalance) {
-                                    updateGridProperties(taskView);
+                                    updateGridProperties(highestVisibleTaskView);
                                     updateScrollSynchronously();
                                 }
                             }
@@ -4397,12 +4397,12 @@
      * Iterate the grid by columns instead of by TaskView index, starting after the focused task and
      * up to the last balanced column.
      *
-     * @return the highest visible TaskView index between both rows
+     * @return the highest visible TaskView between both rows
      */
-    private int getHighestVisibleTaskIndex() {
-        if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier
+    private TaskView getHighestVisibleTaskView() {
+        if (mTopRowIdSet.isEmpty()) return null; // return earlier
 
-        int lastVisibleIndex = Integer.MAX_VALUE;
+        TaskView lastVisibleTaskView = null;
         IntArray topRowIdArray = getTopRowIdArray();
         IntArray bottomRowIdArray = getBottomRowIdArray();
         int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size());
@@ -4412,13 +4412,14 @@
 
             if (isTaskViewVisible(topTask)) {
                 TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i));
-                lastVisibleIndex = Math.max(indexOfChild(topTask), indexOfChild(bottomTask));
-            } else if (lastVisibleIndex < Integer.MAX_VALUE) {
+                lastVisibleTaskView =
+                        indexOfChild(topTask) > indexOfChild(bottomTask) ? topTask : bottomTask;
+            } else if (lastVisibleTaskView != null) {
                 break;
             }
         }
 
-        return lastVisibleIndex;
+        return lastVisibleTaskView;
     }
 
   private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
@@ -4676,6 +4677,12 @@
         }
     }
 
+    public void reapplyActiveRotation() {
+        RotationTouchHelper rotationTouchHelper = RotationTouchHelper.INSTANCE.get(getContext());
+        setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+                rotationTouchHelper.getDisplayRotation());
+    }
+
     public void setLayoutRotation(int touchRotation, int displayRotation) {
         if (mOrientationState.update(touchRotation, displayRotation)) {
             updateOrientationHandler();
@@ -4734,14 +4741,6 @@
     }
 
     /**
-     * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
-     */
-    @NonNull
-    private TaskView requireTaskViewAt(int index) {
-        return Objects.requireNonNull(getTaskViewAt(index));
-    }
-
-    /**
      * Returns iterable [TaskView] children.
      */
     public RecentsViewUtils.TaskViewsIterable getTaskViews() {
@@ -6814,6 +6813,8 @@
         }
 
         mDesktopRecentsTransitionController.moveToDesktop(taskContainer, transitionSource);
+        // TODO(b/387471509): Invoke successCallback after actual transition completion of
+        //  overview menu to desktop
         successCallback.run();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index d92c4d0..ff711da 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -32,8 +32,8 @@
     private val recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) {
-    fun onDetachedFromWindow() {
-        recentsCoroutineScope.cancel("RecentsView detaching from window")
+    fun onDestroy() {
+        recentsCoroutineScope.cancel("RecentsView is being destroyed")
     }
 
     fun switchToScreenshot(
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index c334552..f16e193 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -118,7 +118,6 @@
 
     protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected TaskAnimationManager mTaskAnimationManager;
-    protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
 
     @Mock protected CONTAINER_INTERFACE mActivityInterface;
     @Mock protected ContextInitListener<?> mContextInitListener;
@@ -180,7 +179,8 @@
 
     @Before
     public void setUpRecentsContainer() {
-        mTaskAnimationManager = new TaskAnimationManager(mContext, mRecentsAnimationDeviceState);
+        mTaskAnimationManager = new TaskAnimationManager(mContext,
+                RecentsAnimationDeviceState.INSTANCE.get(mContext));
         RecentsViewContainer recentsContainer = getRecentsContainer();
         RECENTS_VIEW recentsView = getRecentsView();
 
@@ -198,12 +198,6 @@
         }).when(recentsContainer).runOnBindToTouchInteractionService(any());
     }
 
-    @Before
-    public void setUpRecentsAnimationDeviceState() {
-        runOnMainSync(() ->
-                mRecentsAnimationDeviceState = new RecentsAnimationDeviceState(mContext, true));
-    }
-
     @Test
     public void testInitWhenReady_registersActivityInitListener() {
         String reasonString = "because i said so";
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
index d4eb8e2..3489519 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
@@ -44,7 +44,6 @@
             long touchTimeMs, boolean continuingLastGesture) {
         return new FallbackSwipeHandler(
                 mContext,
-                mRecentsAnimationDeviceState,
                 mTaskAnimationManager,
                 mGestureState,
                 touchTimeMs,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 41877c9..0738336 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -25,7 +25,6 @@
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.MSDLPlayerWrapper
-import com.android.quickstep.fallback.window.RecentsDisplayModel
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.shared.system.InputConsumerController
 import dagger.BindsInstance
@@ -40,7 +39,6 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -53,8 +51,6 @@
 
     @Mock private lateinit var systemUiProxy: SystemUiProxy
 
-    @Mock private lateinit var recentsDisplayModel: RecentsDisplayModel
-
     @Mock private lateinit var msdlPlayerWrapper: MSDLPlayerWrapper
 
     private lateinit var underTest: LauncherSwipeHandlerV2
@@ -70,19 +66,20 @@
     @Before
     fun setup() {
         sandboxContext.initDaggerComponent(
-            DaggerTestComponent.builder()
-                .bindSystemUiProxy(systemUiProxy)
-                .bindRecentsDisplayModel(recentsDisplayModel)
+            DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
         )
-
+        sandboxContext.putObject(
+            RotationTouchHelper.INSTANCE,
+            mock(RotationTouchHelper::class.java),
+        )
         val deviceState = mock(RecentsAnimationDeviceState::class.java)
-        whenever(deviceState.rotationTouchHelper).thenReturn(mock(RotationTouchHelper::class.java))
+        sandboxContext.putObject(RecentsAnimationDeviceState.INSTANCE, deviceState)
+
         gestureState = spy(GestureState(OverviewComponentObserver.INSTANCE.get(sandboxContext), 0))
 
         underTest =
             LauncherSwipeHandlerV2(
                 sandboxContext,
-                deviceState,
                 taskAnimationManager,
                 gestureState,
                 0,
@@ -122,8 +119,6 @@
     interface Builder : LauncherAppComponent.Builder {
         @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
 
-        @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
         override fun build(): TestComponent
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
index fc6acfd..e6c5a6c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
@@ -73,7 +73,6 @@
             long touchTimeMs, boolean continuingLastGesture) {
         return new LauncherSwipeHandlerV2(
                 mContext,
-                mRecentsAnimationDeviceState,
                 mTaskAnimationManager,
                 mGestureState,
                 touchTimeMs,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
index 40fefae..dcb45e5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
@@ -62,7 +62,6 @@
             boolean continuingLastGesture) {
         return new RecentsWindowSwipeHandler(
                 mContext,
-                mRecentsAnimationDeviceState,
                 mTaskAnimationManager,
                 mGestureState,
                 touchTimeMs,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index 7c48ea4..cf59f44 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -21,8 +21,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.ALLOW_ROTATION
-import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
 import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY
+import com.android.launcher3.graphics.ThemeManager
 import com.android.launcher3.logging.InstanceId
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED
@@ -66,16 +66,19 @@
     private var mDefaultThemedIcons = false
     private var mDefaultAllowRotation = false
 
+    private val themeManager: ThemeManager
+        get() = ThemeManager.INSTANCE.get(mContext)
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
         whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
         whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
-        mDefaultThemedIcons = LauncherPrefs.get(mContext).get(THEMED_ICONS)
+        mDefaultThemedIcons = themeManager.isMonoThemeEnabled
         mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION)
         // To match the default value of THEMED_ICONS
-        LauncherPrefs.get(mContext).put(THEMED_ICONS, false)
+        themeManager.isMonoThemeEnabled = false
         // To match the default value of ALLOW_ROTATION
         LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false)
 
@@ -84,7 +87,7 @@
 
     @After
     fun tearDown() {
-        LauncherPrefs.get(mContext).put(THEMED_ICONS, mDefaultThemedIcons)
+        themeManager.isMonoThemeEnabled = mDefaultThemedIcons
         LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
index d2aa6ac..44ea73e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
@@ -28,14 +28,9 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW
 import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppModule
-import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.quickstep.fallback.window.RecentsDisplayModel
-import dagger.BindsInstance
-import dagger.Component
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
@@ -75,10 +70,6 @@
         whenever(displayManager.getDisplay(anyInt())).thenReturn(display)
 
         runOnMainSync { recentsDisplayModel = RecentsDisplayModel.INSTANCE.get(context) }
-        context.initDaggerComponent(
-            DaggerRecentsDisplayModelComponent.builder()
-                .bindRecentsDisplayModel(recentsDisplayModel)
-        )
     }
 
     @Test
@@ -125,14 +116,3 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
     }
 }
-
-@LauncherAppSingleton
-@Component(modules = [LauncherAppModule::class])
-interface RecentsDisplayModelComponent : LauncherAppComponent {
-    @Component.Builder
-    interface Builder : LauncherAppComponent.Builder {
-        @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
-        override fun build(): RecentsDisplayModelComponent
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 94e7c2e..c152ee1 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName
 import android.content.Intent
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.launcher3.AbstractFloatingView
@@ -113,6 +114,8 @@
         whenever(launcher.statsLogManager).thenReturn(statsLogManager)
         whenever(statsLogManager.logger()).thenReturn(statsLogger)
         whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
+        whenever(taskView.context)
+            .thenReturn(InstrumentationRegistry.getInstrumentation().targetContext)
         whenever(recentsView.moveTaskToDesktop(any(), any(), any())).thenAnswer {
             val successCallback = it.getArgument<Runnable>(2)
             successCallback.run()
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 5bb2fad..a4c9ef2 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -333,13 +333,11 @@
 
     private class OverviewUpdateHandler implements OverviewChangeListener {
 
-        final RecentsAnimationDeviceState mRads;
         final OverviewComponentObserver mObserver;
         final CountDownLatch mChangeCounter;
 
         OverviewUpdateHandler() {
             Context ctx = getInstrumentation().getTargetContext();
-            mRads = new RecentsAnimationDeviceState(ctx);
             mObserver = OverviewComponentObserver.INSTANCE.get(ctx);
             mChangeCounter = new CountDownLatch(1);
             if (mObserver.getHomeIntent().getComponent()
@@ -358,7 +356,6 @@
 
         void destroy() {
             mObserver.removeOverviewChangeListener(this);
-            mRads.destroy();
         }
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
index 160c578..3c5e1e8 100644
--- a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
@@ -127,6 +127,8 @@
     @Before
     public void setupMainThreadInitializedObjects() {
         mContext.putObject(LockedUserState.INSTANCE, mLockedUserState);
+        mContext.putObject(RotationTouchHelper.INSTANCE, mock(RotationTouchHelper.class));
+        mContext.putObject(RecentsAnimationDeviceState.INSTANCE, mDeviceState);
     }
 
     @Before
@@ -193,7 +195,6 @@
         when(mDeviceState.canStartSystemGesture()).thenReturn(true);
         when(mDeviceState.isFullyGesturalNavMode()).thenReturn(true);
         when(mDeviceState.getNavBarPosition()).thenReturn(mock(NavBarPosition.class));
-        when(mDeviceState.getRotationTouchHelper()).thenReturn(mock(RotationTouchHelper.class));
     }
 
     @After
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
index ef4591e..3072d02 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -39,6 +39,7 @@
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.IconProvider;
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
@@ -93,7 +94,8 @@
         when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
 
         mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
-                mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class));
+                mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class),
+                mock(ThemeManager.class));
 
         mResource = mock(Resources.class);
         when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 098b417..a87c328 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -46,18 +46,12 @@
     private SystemUiProxy mSystemUiProxy;
 
     private TaskAnimationManager mTaskAnimationManager;
-    protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
-
-    @Before
-    public void setUpRecentsAnimationDeviceState() {
-        runOnMainSync(() ->
-                mRecentsAnimationDeviceState = new RecentsAnimationDeviceState(mContext, true));
-    }
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mTaskAnimationManager = new TaskAnimationManager(mContext, mRecentsAnimationDeviceState) {
+        mTaskAnimationManager = new TaskAnimationManager(mContext,
+                RecentsAnimationDeviceState.INSTANCE.get(mContext)) {
             @Override
             SystemUiProxy getSystemUiProxy() {
                 return mSystemUiProxy;
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 698877a..e06895c 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -163,8 +163,8 @@
     </declare-styleable>
 
     <declare-styleable name="GridSize">
-        <attr name="minDeviceWidthDp" format="float"/>
-        <attr name="minDeviceHeightDp" format="float"/>
+        <attr name="minDeviceWidthPx" format="float"/>
+        <attr name="minDeviceHeightPx" format="float"/>
         <attr name="numGridRows" format="integer"/>
         <attr name="numGridColumns" format="integer"/>
         <attr name="dbFile" />
diff --git a/shared/src/com/android/launcher3/testing/shared/TestProtocol.java b/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
index 4a7471a..5fcbbf1 100644
--- a/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
@@ -170,6 +170,7 @@
     public static final String ICON_MISSING = "b/282963545";
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
     public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
+    public static final String REQUEST_IS_RECENTS_WINDOW_ENABLED = "recents-window-enabled";
 
     public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED =
             "unstash-bubble-bar-if-stashed";
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index da73280..9aa06bf 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -464,8 +464,8 @@
     }
 
     protected boolean shouldUseTheme() {
-        return (mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER
-                || mDisplay == DISPLAY_TASKBAR) && Themes.isThemedIconEnabled(getContext());
+        return mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER
+                || mDisplay == DISPLAY_TASKBAR;
     }
 
     /**
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 5cca990..753e017 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -685,7 +685,7 @@
         }
 
         // Finds the min width and height in dp for all displays.
-        int[] dimens = findMinWidthAndHeightDpForDevice(displayInfo);
+        int[] dimens = findMinWidthAndHeightPxForDevice(displayInfo);
 
         return findBestGridSize(gridSizes, dimens[0], dimens[1]);
     }
@@ -694,11 +694,11 @@
      * @return the biggest grid size that fits the display dimensions.
      * If no best grid size is found, return null.
      */
-    private static GridSize findBestGridSize(List<GridSize> list, int minWidthDp,
-            int minHeightDp) {
+    private static GridSize findBestGridSize(List<GridSize> list, int minWidthPx,
+            int minHeightPx) {
         GridSize selectedGridSize = null;
         for (GridSize item: list) {
-            if (minWidthDp >= item.mMinDeviceWidthDp && minHeightDp >= item.mMinDeviceHeightDp) {
+            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
                 if (selectedGridSize == null
                         || (selectedGridSize.mNumColumns <= item.mNumColumns
                         && selectedGridSize.mNumRows <= item.mNumRows)) {
@@ -709,16 +709,14 @@
         return selectedGridSize;
     }
 
-    private static int[] findMinWidthAndHeightDpForDevice(Info displayInfo) {
-        int minDisplayWidthDp = Integer.MAX_VALUE;
-        int minDisplayHeightDp = Integer.MAX_VALUE;
+    private static int[] findMinWidthAndHeightPxForDevice(Info displayInfo) {
+        int minDisplayWidthPx = Integer.MAX_VALUE;
+        int minDisplayHeightPx = Integer.MAX_VALUE;
         for (CachedDisplayInfo display: displayInfo.getAllDisplays()) {
-            minDisplayWidthDp = Math.min(minDisplayWidthDp,
-                    (int) dpiFromPx(display.size.x, DisplayMetrics.DENSITY_DEVICE_STABLE));
-            minDisplayHeightDp = Math.min(minDisplayHeightDp,
-                    (int) dpiFromPx(display.size.y, DisplayMetrics.DENSITY_DEVICE_STABLE));
+            minDisplayWidthPx = Math.min(minDisplayWidthPx, display.size.x);
+            minDisplayHeightPx = Math.min(minDisplayHeightPx, display.size.y);
         }
-        return new int[]{minDisplayWidthDp, minDisplayHeightDp};
+        return new int[]{minDisplayWidthPx, minDisplayHeightPx};
     }
 
     /**
@@ -1246,8 +1244,8 @@
     public static final class GridSize {
         final int mNumRows;
         final int mNumColumns;
-        final float mMinDeviceWidthDp;
-        final float mMinDeviceHeightDp;
+        final float mMinDeviceWidthPx;
+        final float mMinDeviceHeightPx;
         final String mDbFile;
         final int mDefaultLayoutId;
         final int mDemoModeLayoutId;
@@ -1258,8 +1256,8 @@
 
             mNumRows = (int) a.getFloat(R.styleable.GridSize_numGridRows, 0);
             mNumColumns = (int) a.getFloat(R.styleable.GridSize_numGridColumns, 0);
-            mMinDeviceWidthDp = a.getFloat(R.styleable.GridSize_minDeviceWidthDp, 0);
-            mMinDeviceHeightDp = a.getFloat(R.styleable.GridSize_minDeviceHeightDp, 0);
+            mMinDeviceWidthPx = a.getFloat(R.styleable.GridSize_minDeviceWidthPx, 0);
+            mMinDeviceHeightPx = a.getFloat(R.styleable.GridSize_minDeviceHeightPx, 0);
             mDbFile = a.getString(R.styleable.GridSize_dbFile);
             mDefaultLayoutId = a.getResourceId(
                     R.styleable.GridSize_defaultLayoutId, 0);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 5989e4c..e560a14 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -20,12 +20,6 @@
 import static android.content.Context.RECEIVER_EXPORTED;
 
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.InvariantDeviceProfile.GRID_NAME_PREFS_KEY;
-import static com.android.launcher3.LauncherPrefs.DB_FILE;
-import static com.android.launcher3.LauncherPrefs.GRID_NAME;
-import static com.android.launcher3.LauncherPrefs.ICON_STATE;
-import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
-import static com.android.launcher3.model.DeviceGridState.KEY_DB_FILE;
 import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
@@ -38,18 +32,17 @@
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.pm.LauncherApps;
 import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
-import android.os.UserHandle;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.core.os.BuildCompat;
 
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIconProvider;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ModelLauncherCallbacks;
 import com.android.launcher3.model.WidgetsFilterDataProvider;
 import com.android.launcher3.notification.NotificationListener;
@@ -64,7 +57,6 @@
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
@@ -108,6 +100,11 @@
             }
         });
 
+        ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
+        ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
+        mOnTerminateCallback.add(() ->
+                ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
+
         ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
         LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
         launcherApps.registerCallback(callbacks);
@@ -156,14 +153,9 @@
             CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
             mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
 
-            IconObserver observer = new IconObserver();
             SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
-                    observer, MODEL_EXECUTOR.getHandler());
+                    mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
             mOnTerminateCallback.add(iconChangeTracker::close);
-            MODEL_EXECUTOR.execute(observer::verifyIconChanged);
-            LauncherPrefs.get(context).addListener(observer, THEMED_ICONS);
-            mOnTerminateCallback.add(
-                    () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS));
 
             InstallSessionTracker installSessionTracker =
                     InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
@@ -255,41 +247,4 @@
     public static InvariantDeviceProfile getIDP(Context context) {
         return InvariantDeviceProfile.INSTANCE.get(context);
     }
-
-    private class IconObserver
-            implements IconProvider.IconChangeListener, LauncherPrefChangeListener {
-
-        @Override
-        public void onAppIconChanged(String packageName, UserHandle user) {
-            mModel.onAppIconChanged(packageName, user);
-        }
-
-        @Override
-        public void onSystemIconStateChanged(String iconState) {
-            IconShape.INSTANCE.get(mContext).pickBestShape(mContext);
-            refreshAndReloadLauncher();
-            LauncherPrefs.get(mContext).put(ICON_STATE, iconState);
-        }
-
-        void verifyIconChanged() {
-            String iconState = mIconProvider.getSystemIconState();
-            if (!iconState.equals(LauncherPrefs.get(mContext).get(ICON_STATE))) {
-                onSystemIconStateChanged(iconState);
-            }
-        }
-
-        @Override
-        public void onPrefChanged(String key) {
-            if (Themes.KEY_THEMED_ICONS.equals(key)) {
-                mIconProvider.setIconThemeSupported(Themes.isThemedIconEnabled(mContext));
-                verifyIconChanged();
-            } else if (GRID_NAME_PREFS_KEY.equals(key)) {
-                FileLog.d(TAG, "onPrefChanged GRID_NAME changed: "
-                        + LauncherPrefs.get(mContext).get(GRID_NAME));
-            } else if (KEY_DB_FILE.equals(key)) {
-                FileLog.d(TAG, "onPrefChanged DB_FILE changed: "
-                        + LauncherPrefs.get(mContext).get(DB_FILE));
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index ad592d8..d8bb84e 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -34,7 +34,6 @@
 import com.android.launcher3.states.RotationHelper
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DisplayController
-import com.android.launcher3.util.Themes
 import javax.inject.Inject
 
 /**
@@ -235,13 +234,9 @@
         const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
         const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
         const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
-        @JvmField
-        val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
 
         @JvmField
         val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
-        @JvmField
-        val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
         @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
         @JvmField val WORK_EDU_STEP = backedUpItem("showed_work_profile_edu", 0)
         @JvmField
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index b05a46d..0ec3b79 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -903,14 +903,12 @@
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
-        mPageScrolls = null;
         dispatchPageCountChanged();
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
-        mPageScrolls = null;
         runOnPageScrollsInitialized(() -> {
             mCurrentPage = validateNewPage(mCurrentPage);
             mCurrentScrollOverPage = mCurrentPage;
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 9060691..e44caa4 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -74,9 +74,11 @@
 import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.CacheableShortcutInfo;
+import com.android.launcher3.icons.IconThemeController;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -88,7 +90,6 @@
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -626,7 +627,6 @@
     @WorkerThread
     public static <T extends Context & ActivityContext> Pair<AdaptiveIconDrawable, Drawable>
             getFullDrawable(T context, ItemInfo info, int width, int height, boolean useTheme) {
-        useTheme &= Themes.isThemedIconEnabled(context);
         LauncherAppState appState = LauncherAppState.getInstance(context);
         Drawable mainIcon = null;
 
@@ -690,15 +690,15 @@
 
         // Inject theme icon drawable
         if (ATLEAST_T && useTheme) {
-            try (LauncherIcons li = LauncherIcons.obtain(context)) {
-                if (li.getThemeController() != null) {
-                    AdaptiveIconDrawable themed = li.getThemeController().createThemedAdaptiveIcon(
-                            context,
-                            result,
-                            info instanceof ItemInfoWithIcon iiwi ? iiwi.bitmap : null);
-                    if (themed != null) {
-                        result = themed;
-                    }
+            IconThemeController themeController =
+                    ThemeManager.INSTANCE.get(context).getThemeController();
+            if (themeController != null) {
+                AdaptiveIconDrawable themed = themeController.createThemedAdaptiveIcon(
+                        context,
+                        result,
+                        info instanceof ItemInfoWithIcon iiwi ? iiwi.bitmap : null);
+                if (themed != null) {
+                    result = themed;
                 }
             }
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index bc751d9..0d050b2 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -109,6 +109,7 @@
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.util.EdgeEffectCompat;
@@ -2241,6 +2242,23 @@
             }
             mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
                     .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
+
+            if (mAccessibilityDragListener != null) {
+                // This code needs to be called after StateManager.cancelAnimation. Before changing
+                // the order of operations in this method related to the StateListener below, please
+                // test that accessibility moves retain focus after accessibility dropping an item.
+                // Accessibility focus must be requested after launcher is back to a normal state
+                mLauncher.getStateManager().addStateListener(new StateListener<LauncherState>() {
+                    @Override
+                    public void onStateTransitionComplete(LauncherState finalState) {
+                        if (finalState == NORMAL) {
+                            cell.performAccessibilityAction(
+                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+                            mLauncher.getStateManager().removeStateListener(this);
+                        }
+                    }
+                });
+            }
         }
 
         if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index 034b686..81a92f6 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -27,7 +27,6 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
 import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.model.data.AppPairInfo
-import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
 
 /**
@@ -46,12 +45,11 @@
         @JvmStatic
         fun composeDrawable(
             appPairInfo: AppPairInfo,
-            p: AppPairIconDrawingParams
+            p: AppPairIconDrawingParams,
         ): AppPairIconDrawable {
-            // Generate new icons, using themed flag if needed.
-            val flags = if (Themes.isThemedIconEnabled(p.context)) BitmapInfo.FLAG_THEMED else 0
-            val appIcon1 = appPairInfo.getFirstApp().newIcon(p.context, flags)
-            val appIcon2 = appPairInfo.getSecondApp().newIcon(p.context, flags)
+            // Generate new icons, using themed flag since the icon is drawn on homescreen
+            val appIcon1 = appPairInfo.getFirstApp().newIcon(p.context, BitmapInfo.FLAG_THEMED)
+            val appIcon2 = appPairInfo.getSecondApp().newIcon(p.context, BitmapInfo.FLAG_THEMED)
             appIcon1.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt())
             appIcon2.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt())
 
@@ -125,7 +123,7 @@
             ((parentIcon.width - drawParams.backgroundSize) / 2).toInt(),
             // y-coordinate in parent's coordinate system
             (parentIcon.paddingTop + drawParams.standardIconPadding + drawParams.outerPadding)
-                .toInt()
+                .toInt(),
         )
     }
 
@@ -140,17 +138,13 @@
         drawable.draw(canvas)
     }
 
-    /**
-     * Sets the scale of the icon background while hovered.
-     */
+    /** Sets the scale of the icon background while hovered. */
     fun setHoverScale(scale: Float) {
         drawParams.hoverScale = scale
         redraw()
     }
 
-    /**
-     * Gets the scale of the icon background while hovered.
-     */
+    /** Gets the scale of the icon background while hovered. */
     fun getHoverScale(): Float {
         return drawParams.hoverScale
     }
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 72a97a8..4b43d49 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -21,6 +21,7 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
 import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.ApiWrapper;
@@ -64,6 +65,7 @@
     MSDLPlayerWrapper getMSDLPlayerWrapper();
     WindowManagerProxy getWmProxy();
     LauncherPrefs getLauncherPrefs();
+    ThemeManager getThemeManager();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index e68e3c9..42556ca 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -110,7 +110,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -517,8 +516,6 @@
         mInfo = info;
         mFromTitle = info.title;
         mFromLabelState = info.getFromLabelState();
-        ArrayList<ItemInfo> children = info.getContents();
-        Collections.sort(children, ITEM_POS_COMPARATOR);
         updateItemLocationsInDatabaseBatch(true);
 
         BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 5ee6a25..4cf618d 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -53,7 +53,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
@@ -448,8 +447,7 @@
             if (isActivePendingIcon(wii)) {
                 p.drawable = newPendingIcon(mContext, wii);
             } else {
-                p.drawable = wii.newIcon(mContext,
-                        Themes.isThemedIconEnabled(mContext) ? FLAG_THEMED : 0);
+                p.drawable = wii.newIcon(mContext, FLAG_THEMED);
             }
             p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         } else if (item instanceof AppPairInfo api) {
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index eaca6c5..5461485 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -15,10 +15,8 @@
  */
 package com.android.launcher3.graphics;
 
-import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.Themes.isThemedIconEnabled;
 
 import android.content.ContentProvider;
 import android.content.ContentValues;
@@ -42,7 +40,6 @@
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.shapes.AppShape;
 import com.android.launcher3.shapes.AppShapesProvider;
@@ -178,7 +175,8 @@
             case GET_ICON_THEMED:
             case ICON_THEMED: {
                 MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE});
-                cursor.newRow().add(BOOLEAN_VALUE, isThemedIconEnabled(getContext()) ? 1 : 0);
+                cursor.newRow().add(BOOLEAN_VALUE,
+                        ThemeManager.INSTANCE.get(getContext()).isMonoThemeEnabled() ? 1 : 0);
                 return cursor;
             }
             default:
@@ -247,8 +245,8 @@
             }
             case ICON_THEMED:
             case SET_ICON_THEMED: {
-                LauncherPrefs.get(context)
-                        .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE));
+                ThemeManager.INSTANCE.get(context)
+                        .setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
                 context.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
deleted file mode 100644
index cb14587..0000000
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Copyright (C) 2018 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.graphics;
-
-import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.FloatArrayEvaluator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.Region.Op;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.util.AttributeSet;
-import android.util.Xml;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.dagger.ApplicationContext;
-import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.dagger.LauncherBaseAppComponent;
-import com.android.launcher3.icons.GraphicsUtils;
-import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.DaggerSingletonObject;
-import com.android.launcher3.views.ClipPathView;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * Abstract representation of the shape of an icon shape
- */
-@LauncherAppSingleton
-public final class IconShape {
-
-    public static DaggerSingletonObject<IconShape> INSTANCE =
-            new DaggerSingletonObject<>(LauncherBaseAppComponent::getIconShape);
-
-    private ShapeDelegate mDelegate = new Circle();
-    private float mNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
-
-    @Inject
-    public IconShape(@ApplicationContext Context context) {
-        pickBestShape(context);
-    }
-
-    public ShapeDelegate getShape() {
-        return mDelegate;
-    }
-
-    public float getNormalizationScale() {
-        return mNormalizationScale;
-    }
-
-    /**
-     * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
-     */
-    public void pickBestShape(Context context) {
-        // Pick any large size
-        final int size = 200;
-
-        Region full = new Region(0, 0, size, size);
-        Region iconR = new Region();
-        AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
-                new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
-        drawable.setBounds(0, 0, size, size);
-        iconR.setPath(drawable.getIconMask(), full);
-
-        Path shapePath = new Path();
-        Region shapeR = new Region();
-
-        // Find the shape with minimum area of divergent region.
-        int minArea = Integer.MAX_VALUE;
-        ShapeDelegate closestShape = null;
-        for (ShapeDelegate shape : getAllShapes(context)) {
-            shapePath.reset();
-            shape.addToPath(shapePath, 0, 0, size / 2f);
-            shapeR.setPath(shapePath, full);
-            shapeR.op(iconR, Op.XOR);
-
-            int area = GraphicsUtils.getArea(shapeR);
-            if (area < minArea) {
-                minArea = area;
-                closestShape = shape;
-            }
-        }
-
-        if (closestShape != null) {
-            mDelegate = closestShape;
-        }
-
-        // Initialize shape properties
-        mNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
-    }
-
-
-
-    public interface ShapeDelegate {
-
-        default boolean enableShapeDetection() {
-            return false;
-        }
-
-        void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint paint);
-
-        void addToPath(Path path, float offsetX, float offsetY, float radius);
-
-        <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
-                Rect startRect, Rect endRect, float endRadius, boolean isReversed);
-    }
-
-    /**
-     * Abstract shape where the reveal animation is a derivative of a round rect animation
-     */
-    private static abstract class SimpleRectShape implements ShapeDelegate {
-
-        @Override
-        public final <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
-                Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
-            return new RoundedRectRevealOutlineProvider(
-                    getStartRadius(startRect), endRadius, startRect, endRect) {
-                @Override
-                public boolean shouldRemoveElevationDuringAnimation() {
-                    return true;
-                }
-            }.createRevealAnimator(target, isReversed);
-        }
-
-        protected abstract float getStartRadius(Rect startRect);
-    }
-
-    /**
-     * Abstract shape which draws using {@link Path}
-     */
-    private static abstract class PathShape implements ShapeDelegate {
-
-        private final Path mTmpPath = new Path();
-
-        @Override
-        public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
-                Paint paint) {
-            mTmpPath.reset();
-            addToPath(mTmpPath, offsetX, offsetY, radius);
-            canvas.drawPath(mTmpPath, paint);
-        }
-
-        protected abstract AnimatorUpdateListener newUpdateListener(
-                Rect startRect, Rect endRect, float endRadius, Path outPath);
-
-        @Override
-        public final <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
-                Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
-            Path path = new Path();
-            AnimatorUpdateListener listener =
-                    newUpdateListener(startRect, endRect, endRadius, path);
-
-            ValueAnimator va =
-                    isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
-            va.addListener(new AnimatorListenerAdapter() {
-                private ViewOutlineProvider mOldOutlineProvider;
-
-                public void onAnimationStart(Animator animation) {
-                    mOldOutlineProvider = target.getOutlineProvider();
-                    target.setOutlineProvider(null);
-
-                    target.setTranslationZ(-target.getElevation());
-                }
-
-                public void onAnimationEnd(Animator animation) {
-                    target.setTranslationZ(0);
-                    target.setClipPath(null);
-                    target.setOutlineProvider(mOldOutlineProvider);
-                }
-            });
-
-            va.addUpdateListener((anim) -> {
-                path.reset();
-                listener.onAnimationUpdate(anim);
-                target.setClipPath(path);
-            });
-
-            return va;
-        }
-    }
-
-    public static final class Circle extends PathShape {
-
-        private final float[] mTempRadii = new float[8];
-
-        protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
-                float endRadius, Path outPath) {
-            float r1 = getStartRadius(startRect);
-
-            float[] startValues = new float[] {
-                    startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r1};
-            float[] endValues = new float[] {
-                    endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
-
-            FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
-
-            return (anim) -> {
-                float progress = (Float) anim.getAnimatedValue();
-                float[] values = evaluator.evaluate(progress, startValues, endValues);
-                outPath.addRoundRect(
-                        values[0], values[1], values[2], values[3],
-                        getRadiiArray(values[4], values[5]), Path.Direction.CW);
-            };
-        }
-
-        private float[] getRadiiArray(float r1, float r2) {
-            mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
-                    mTempRadii[6] = mTempRadii[7] = r1;
-            mTempRadii[4] = mTempRadii[5] = r2;
-            return mTempRadii;
-        }
-
-
-        @Override
-        public void addToPath(Path path, float offsetX, float offsetY, float radius) {
-            path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW);
-        }
-
-        protected float getStartRadius(Rect startRect) {
-            return startRect.width() / 2f;
-        }
-
-        @Override
-        public boolean enableShapeDetection() {
-            return true;
-        }
-    }
-
-    private static class RoundedSquare extends SimpleRectShape {
-
-        /**
-         * Ratio of corner radius to half size.
-         */
-        private final float mRadiusRatio;
-
-        public RoundedSquare(float radiusRatio) {
-            mRadiusRatio = radiusRatio;
-        }
-
-        @Override
-        public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
-            float cx = radius + offsetX;
-            float cy = radius + offsetY;
-            float cr = radius * mRadiusRatio;
-            canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
-        }
-
-        @Override
-        public void addToPath(Path path, float offsetX, float offsetY, float radius) {
-            float cx = radius + offsetX;
-            float cy = radius + offsetY;
-            float cr = radius * mRadiusRatio;
-            path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
-                    Path.Direction.CW);
-        }
-
-        @Override
-        protected float getStartRadius(Rect startRect) {
-            return (startRect.width() / 2f) * mRadiusRatio;
-        }
-    }
-
-    private static class TearDrop extends PathShape {
-
-        /**
-         * Radio of short radius to large radius, based on the shape options defined in the config.
-         */
-        private final float mRadiusRatio;
-        private final float[] mTempRadii = new float[8];
-
-        public TearDrop(float radiusRatio) {
-            mRadiusRatio = radiusRatio;
-        }
-
-        @Override
-        public void addToPath(Path p, float offsetX, float offsetY, float r1) {
-            float r2 = r1 * mRadiusRatio;
-            float cx = r1 + offsetX;
-            float cy = r1 + offsetY;
-
-            p.addRoundRect(cx - r1, cy - r1, cx + r1, cy + r1, getRadiiArray(r1, r2),
-                    Path.Direction.CW);
-        }
-
-        private float[] getRadiiArray(float r1, float r2) {
-            mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
-                    mTempRadii[6] = mTempRadii[7] = r1;
-            mTempRadii[4] = mTempRadii[5] = r2;
-            return mTempRadii;
-        }
-
-        @Override
-        protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
-                float endRadius, Path outPath) {
-            float r1 = startRect.width() / 2f;
-            float r2 = r1 * mRadiusRatio;
-
-            float[] startValues = new float[] {
-                    startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2};
-            float[] endValues = new float[] {
-                    endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
-
-            FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
-
-            return (anim) -> {
-                float progress = (Float) anim.getAnimatedValue();
-                float[] values = evaluator.evaluate(progress, startValues, endValues);
-                outPath.addRoundRect(
-                        values[0], values[1], values[2], values[3],
-                        getRadiiArray(values[4], values[5]), Path.Direction.CW);
-            };
-        }
-    }
-
-    private static class Squircle extends PathShape {
-
-        /**
-         * Radio of radius to circle radius, based on the shape options defined in the config.
-         */
-        private final float mRadiusRatio;
-
-        public Squircle(float radiusRatio) {
-            mRadiusRatio = radiusRatio;
-        }
-
-        @Override
-        public void addToPath(Path p, float offsetX, float offsetY, float r) {
-            float cx = r + offsetX;
-            float cy = r + offsetY;
-            float control = r - r * mRadiusRatio;
-
-            p.moveTo(cx, cy - r);
-            addLeftCurve(cx, cy, r, control, p);
-            addRightCurve(cx, cy, r, control, p);
-            addLeftCurve(cx, cy, -r, -control, p);
-            addRightCurve(cx, cy, -r, -control, p);
-            p.close();
-        }
-
-        private void addLeftCurve(float cx, float cy, float r, float control, Path path) {
-            path.cubicTo(
-                    cx - control, cy - r,
-                    cx - r, cy - control,
-                    cx - r, cy);
-        }
-
-        private void addRightCurve(float cx, float cy, float r, float control, Path path) {
-            path.cubicTo(
-                    cx - r, cy + control,
-                    cx - control, cy + r,
-                    cx, cy + r);
-        }
-
-        @Override
-        protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
-                float endR, Path outPath) {
-
-            float startCX = startRect.exactCenterX();
-            float startCY = startRect.exactCenterY();
-            float startR = startRect.width() / 2f;
-            float startControl = startR - startR * mRadiusRatio;
-            float startHShift = 0;
-            float startVShift = 0;
-
-            float endCX = endRect.exactCenterX();
-            float endCY = endRect.exactCenterY();
-            // Approximate corner circle using bezier curves
-            // http://spencermortensen.com/articles/bezier-circle/
-            float endControl = endR * 0.551915024494f;
-            float endHShift = endRect.width() / 2f - endR;
-            float endVShift = endRect.height() / 2f - endR;
-
-            return (anim) -> {
-                float progress = (Float) anim.getAnimatedValue();
-
-                float cx = (1 - progress) * startCX + progress * endCX;
-                float cy = (1 - progress) * startCY + progress * endCY;
-                float r = (1 - progress) * startR + progress * endR;
-                float control = (1 - progress) * startControl + progress * endControl;
-                float hShift = (1 - progress) * startHShift + progress * endHShift;
-                float vShift = (1 - progress) * startVShift + progress * endVShift;
-
-                outPath.moveTo(cx, cy - vShift - r);
-                outPath.rLineTo(-hShift, 0);
-
-                addLeftCurve(cx - hShift, cy - vShift, r, control, outPath);
-                outPath.rLineTo(0, vShift + vShift);
-
-                addRightCurve(cx - hShift, cy + vShift, r, control, outPath);
-                outPath.rLineTo(hShift + hShift, 0);
-
-                addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath);
-                outPath.rLineTo(0, -vShift - vShift);
-
-                addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath);
-                outPath.close();
-            };
-        }
-    }
-
-    private static ShapeDelegate getShapeDefinition(String type, float radius) {
-        switch (type) {
-            case "Circle":
-                return new Circle();
-            case "RoundedSquare":
-                return new RoundedSquare(radius);
-            case "TearDrop":
-                return new TearDrop(radius);
-            case "Squircle":
-                return new Squircle(radius);
-            default:
-                throw new IllegalArgumentException("Invalid shape type: " + type);
-        }
-    }
-
-    private static List<ShapeDelegate> getAllShapes(Context context) {
-        ArrayList<ShapeDelegate> result = new ArrayList<>();
-        try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {
-
-            // Find the root tag
-            int type;
-            while ((type = parser.next()) != XmlPullParser.END_TAG
-                    && type != XmlPullParser.END_DOCUMENT
-                    && !"shapes".equals(parser.getName()));
-
-            final int depth = parser.getDepth();
-            int[] radiusAttr = new int[] {R.attr.folderIconRadius};
-
-            while (((type = parser.next()) != XmlPullParser.END_TAG ||
-                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
-
-                if (type == XmlPullParser.START_TAG) {
-                    AttributeSet attrs = Xml.asAttributeSet(parser);
-                    TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
-                    ShapeDelegate shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
-                    a.recycle();
-
-                    result.add(shape);
-                }
-            }
-        } catch (IOException | XmlPullParserException e) {
-            throw new RuntimeException(e);
-        }
-        return result;
-    }
-
-}
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/IconShape.kt
new file mode 100644
index 0000000..c64d4da
--- /dev/null
+++ b/src/com/android/launcher3/graphics/IconShape.kt
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2018 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.graphics
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.FloatArrayEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.Rect
+import android.graphics.Region
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.ColorDrawable
+import android.util.Xml
+import android.view.View
+import android.view.ViewOutlineProvider
+import com.android.launcher3.R
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.GraphicsUtils
+import com.android.launcher3.icons.IconNormalizer
+import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.views.ClipPathView
+import java.io.IOException
+import javax.inject.Inject
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+/** Abstract representation of the shape of an icon shape */
+@LauncherAppSingleton
+class IconShape
+@Inject
+constructor(
+    @ApplicationContext context: Context,
+    themeManager: ThemeManager,
+    lifeCycle: DaggerSingletonTracker,
+) {
+    var shape: ShapeDelegate = Circle()
+        private set
+
+    var normalizationScale: Float = IconNormalizer.ICON_VISIBLE_AREA_FACTOR
+        private set
+
+    init {
+        pickBestShape(context)
+
+        val changeListener = ThemeChangeListener { pickBestShape(context) }
+        themeManager.addChangeListener(changeListener)
+        lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
+    }
+
+    /** Initializes the shape which is closest to the [AdaptiveIconDrawable] */
+    fun pickBestShape(context: Context) {
+        // Pick any large size
+        val size = 200
+        val full = Region(0, 0, size, size)
+        val shapePath = Path()
+        val shapeR = Region()
+        val iconR = Region()
+        val drawable = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), ColorDrawable(Color.BLACK))
+        drawable.setBounds(0, 0, size, size)
+        iconR.setPath(drawable.iconMask, full)
+
+        // Find the shape with minimum area of divergent region.
+        var minArea = Int.MAX_VALUE
+        var closestShape: ShapeDelegate? = null
+        for (shape in getAllShapes(context)) {
+            shapePath.reset()
+            shape.addToPath(shapePath, 0f, 0f, size / 2f)
+            shapeR.setPath(shapePath, full)
+            shapeR.op(iconR, Region.Op.XOR)
+
+            val area = GraphicsUtils.getArea(shapeR)
+            if (area < minArea) {
+                minArea = area
+                closestShape = shape
+            }
+        }
+
+        if (closestShape != null) {
+            shape = closestShape
+        }
+
+        // Initialize shape properties
+        normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null)
+    }
+
+    interface ShapeDelegate {
+        fun enableShapeDetection(): Boolean {
+            return false
+        }
+
+        fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
+
+        fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
+
+        fun <T> createRevealAnimator(
+            target: T,
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            isReversed: Boolean,
+        ): ValueAnimator where T : View?, T : ClipPathView?
+    }
+
+    /** Abstract shape where the reveal animation is a derivative of a round rect animation */
+    private abstract class SimpleRectShape : ShapeDelegate {
+        override fun <T> createRevealAnimator(
+            target: T,
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            isReversed: Boolean,
+        ): ValueAnimator where T : View?, T : ClipPathView? {
+            return object :
+                    RoundedRectRevealOutlineProvider(
+                        getStartRadius(startRect),
+                        endRadius,
+                        startRect,
+                        endRect,
+                    ) {
+                    override fun shouldRemoveElevationDuringAnimation(): Boolean {
+                        return true
+                    }
+                }
+                .createRevealAnimator(target, isReversed)
+        }
+
+        protected abstract fun getStartRadius(startRect: Rect): Float
+    }
+
+    /** Abstract shape which draws using [Path] */
+    abstract class PathShape : ShapeDelegate {
+        private val mTmpPath = Path()
+
+        override fun drawShape(
+            canvas: Canvas,
+            offsetX: Float,
+            offsetY: Float,
+            radius: Float,
+            paint: Paint,
+        ) {
+            mTmpPath.reset()
+            addToPath(mTmpPath, offsetX, offsetY, radius)
+            canvas.drawPath(mTmpPath, paint)
+        }
+
+        protected abstract fun newUpdateListener(
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            outPath: Path,
+        ): ValueAnimator.AnimatorUpdateListener
+
+        override fun <T> createRevealAnimator(
+            target: T,
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            isReversed: Boolean,
+        ): ValueAnimator where T : View?, T : ClipPathView? {
+            val path = Path()
+            val listener = newUpdateListener(startRect, endRect, endRadius, path)
+
+            val va =
+                if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)
+            va.addListener(
+                object : AnimatorListenerAdapter() {
+                    private var mOldOutlineProvider: ViewOutlineProvider? = null
+
+                    override fun onAnimationStart(animation: Animator) {
+                        target?.apply {
+                            mOldOutlineProvider = outlineProvider
+                            outlineProvider = null
+                            translationZ = -target.elevation
+                        }
+                    }
+
+                    override fun onAnimationEnd(animation: Animator) {
+                        target?.apply {
+                            translationZ = 0f
+                            setClipPath(null)
+                            outlineProvider = mOldOutlineProvider
+                        }
+                    }
+                }
+            )
+
+            va.addUpdateListener { anim: ValueAnimator ->
+                path.reset()
+                listener.onAnimationUpdate(anim)
+                target?.setClipPath(path)
+            }
+
+            return va
+        }
+    }
+
+    open class Circle : PathShape() {
+        private val mTempRadii = FloatArray(8)
+
+        override fun newUpdateListener(
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            outPath: Path,
+        ): ValueAnimator.AnimatorUpdateListener {
+            val r1 = getStartRadius(startRect)
+
+            val startValues =
+                floatArrayOf(
+                    startRect.left.toFloat(),
+                    startRect.top.toFloat(),
+                    startRect.right.toFloat(),
+                    startRect.bottom.toFloat(),
+                    r1,
+                    r1,
+                )
+            val endValues =
+                floatArrayOf(
+                    endRect.left.toFloat(),
+                    endRect.top.toFloat(),
+                    endRect.right.toFloat(),
+                    endRect.bottom.toFloat(),
+                    endRadius,
+                    endRadius,
+                )
+
+            val evaluator = FloatArrayEvaluator(FloatArray(6))
+
+            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
+                val progress = anim.animatedValue as Float
+                val values = evaluator.evaluate(progress, startValues, endValues)
+                outPath.addRoundRect(
+                    values[0],
+                    values[1],
+                    values[2],
+                    values[3],
+                    getRadiiArray(values[4], values[5]),
+                    Path.Direction.CW,
+                )
+            }
+        }
+
+        private fun getRadiiArray(r1: Float, r2: Float): FloatArray {
+            mTempRadii[7] = r1
+            mTempRadii[6] = mTempRadii[7]
+            mTempRadii[3] = mTempRadii[6]
+            mTempRadii[2] = mTempRadii[3]
+            mTempRadii[1] = mTempRadii[2]
+            mTempRadii[0] = mTempRadii[1]
+            mTempRadii[5] = r2
+            mTempRadii[4] = mTempRadii[5]
+            return mTempRadii
+        }
+
+        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+            path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW)
+        }
+
+        private fun getStartRadius(startRect: Rect): Float {
+            return startRect.width() / 2f
+        }
+
+        override fun enableShapeDetection(): Boolean {
+            return true
+        }
+    }
+
+    private class RoundedSquare(
+        /** Ratio of corner radius to half size. */
+        private val mRadiusRatio: Float
+    ) : SimpleRectShape() {
+        override fun drawShape(
+            canvas: Canvas,
+            offsetX: Float,
+            offsetY: Float,
+            radius: Float,
+            paint: Paint,
+        ) {
+            val cx = radius + offsetX
+            val cy = radius + offsetY
+            val cr = radius * mRadiusRatio
+            canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, paint)
+        }
+
+        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+            val cx = radius + offsetX
+            val cy = radius + offsetY
+            val cr = radius * mRadiusRatio
+            path.addRoundRect(
+                cx - radius,
+                cy - radius,
+                cx + radius,
+                cy + radius,
+                cr,
+                cr,
+                Path.Direction.CW,
+            )
+        }
+
+        override fun getStartRadius(startRect: Rect): Float {
+            return (startRect.width() / 2f) * mRadiusRatio
+        }
+    }
+
+    private class TearDrop(
+        /**
+         * Radio of short radius to large radius, based on the shape options defined in the config.
+         */
+        private val mRadiusRatio: Float
+    ) : PathShape() {
+        private val mTempRadii = FloatArray(8)
+
+        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+            val r2 = radius * mRadiusRatio
+            val cx = radius + offsetX
+            val cy = radius + offsetY
+
+            path.addRoundRect(
+                cx - radius,
+                cy - radius,
+                cx + radius,
+                cy + radius,
+                getRadiiArray(radius, r2),
+                Path.Direction.CW,
+            )
+        }
+
+        fun getRadiiArray(r1: Float, r2: Float): FloatArray {
+            mTempRadii[7] = r1
+            mTempRadii[6] = mTempRadii[7]
+            mTempRadii[3] = mTempRadii[6]
+            mTempRadii[2] = mTempRadii[3]
+            mTempRadii[1] = mTempRadii[2]
+            mTempRadii[0] = mTempRadii[1]
+            mTempRadii[5] = r2
+            mTempRadii[4] = mTempRadii[5]
+            return mTempRadii
+        }
+
+        override fun newUpdateListener(
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            outPath: Path,
+        ): ValueAnimator.AnimatorUpdateListener {
+            val r1 = startRect.width() / 2f
+            val r2 = r1 * mRadiusRatio
+
+            val startValues =
+                floatArrayOf(
+                    startRect.left.toFloat(),
+                    startRect.top.toFloat(),
+                    startRect.right.toFloat(),
+                    startRect.bottom.toFloat(),
+                    r1,
+                    r2,
+                )
+            val endValues =
+                floatArrayOf(
+                    endRect.left.toFloat(),
+                    endRect.top.toFloat(),
+                    endRect.right.toFloat(),
+                    endRect.bottom.toFloat(),
+                    endRadius,
+                    endRadius,
+                )
+
+            val evaluator = FloatArrayEvaluator(FloatArray(6))
+
+            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
+                val progress = anim.animatedValue as Float
+                val values = evaluator.evaluate(progress, startValues, endValues)
+                outPath.addRoundRect(
+                    values[0],
+                    values[1],
+                    values[2],
+                    values[3],
+                    getRadiiArray(values[4], values[5]),
+                    Path.Direction.CW,
+                )
+            }
+        }
+    }
+
+    private class Squircle(
+        /** Radio of radius to circle radius, based on the shape options defined in the config. */
+        private val mRadiusRatio: Float
+    ) : PathShape() {
+        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+            val cx = radius + offsetX
+            val cy = radius + offsetY
+            val control = radius - radius * mRadiusRatio
+
+            path.moveTo(cx, cy - radius)
+            addLeftCurve(cx, cy, radius, control, path)
+            addRightCurve(cx, cy, radius, control, path)
+            addLeftCurve(cx, cy, -radius, -control, path)
+            addRightCurve(cx, cy, -radius, -control, path)
+            path.close()
+        }
+
+        fun addLeftCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
+            path.cubicTo(cx - control, cy - r, cx - r, cy - control, cx - r, cy)
+        }
+
+        fun addRightCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
+            path.cubicTo(cx - r, cy + control, cx - control, cy + r, cx, cy + r)
+        }
+
+        override fun newUpdateListener(
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            outPath: Path,
+        ): ValueAnimator.AnimatorUpdateListener {
+            val startCX = startRect.exactCenterX()
+            val startCY = startRect.exactCenterY()
+            val startR = startRect.width() / 2f
+            val startControl = startR - startR * mRadiusRatio
+            val startHShift = 0f
+            val startVShift = 0f
+
+            val endCX = endRect.exactCenterX()
+            val endCY = endRect.exactCenterY()
+            // Approximate corner circle using bezier curves
+            // http://spencermortensen.com/articles/bezier-circle/
+            val endControl = endRadius * 0.551915024494f
+            val endHShift = endRect.width() / 2f - endRadius
+            val endVShift = endRect.height() / 2f - endRadius
+
+            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
+                val progress = anim.animatedValue as Float
+                val cx = (1 - progress) * startCX + progress * endCX
+                val cy = (1 - progress) * startCY + progress * endCY
+                val r = (1 - progress) * startR + progress * endRadius
+                val control = (1 - progress) * startControl + progress * endControl
+                val hShift = (1 - progress) * startHShift + progress * endHShift
+                val vShift = (1 - progress) * startVShift + progress * endVShift
+
+                outPath.moveTo(cx, cy - vShift - r)
+                outPath.rLineTo(-hShift, 0f)
+
+                addLeftCurve(cx - hShift, cy - vShift, r, control, outPath)
+                outPath.rLineTo(0f, vShift + vShift)
+
+                addRightCurve(cx - hShift, cy + vShift, r, control, outPath)
+                outPath.rLineTo(hShift + hShift, 0f)
+
+                addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath)
+                outPath.rLineTo(0f, -vShift - vShift)
+
+                addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath)
+                outPath.close()
+            }
+        }
+    }
+
+    companion object {
+        @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
+
+        private fun getShapeDefinition(type: String, radius: Float): ShapeDelegate {
+            return when (type) {
+                "Circle" -> Circle()
+                "RoundedSquare" -> RoundedSquare(radius)
+                "TearDrop" -> TearDrop(radius)
+                "Squircle" -> Squircle(radius)
+                else -> throw IllegalArgumentException("Invalid shape type: $type")
+            }
+        }
+
+        private fun getAllShapes(context: Context): List<ShapeDelegate> {
+            val result = ArrayList<ShapeDelegate>()
+            try {
+                context.resources.getXml(R.xml.folder_shapes).use { parser ->
+                    // Find the root tag
+                    var type: Int = parser.next()
+                    while (
+                        type != XmlPullParser.END_TAG &&
+                            type != XmlPullParser.END_DOCUMENT &&
+                            "shapes" != parser.name
+                    ) {
+                        type = parser.next()
+                    }
+                    val depth = parser.depth
+                    val radiusAttr = intArrayOf(R.attr.folderIconRadius)
+                    type = parser.next()
+                    while (
+                        (type != XmlPullParser.END_TAG || parser.depth > depth) &&
+                            type != XmlPullParser.END_DOCUMENT
+                    ) {
+                        if (type == XmlPullParser.START_TAG) {
+                            val attrs = Xml.asAttributeSet(parser)
+                            val arr = context.obtainStyledAttributes(attrs, radiusAttr)
+                            val shape = getShapeDefinition(parser.name, arr.getFloat(0, 1f))
+                            arr.recycle()
+                            result.add(shape)
+                        }
+                        type = parser.next()
+                    }
+                }
+            } catch (e: IOException) {
+                throw RuntimeException(e)
+            } catch (e: XmlPullParserException) {
+                throw RuntimeException(e)
+            }
+            return result
+        }
+    }
+}
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
new file mode 100644
index 0000000..991edf7
--- /dev/null
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.graphics
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.launcher3.EncryptionType
+import com.android.launcher3.LauncherPrefChangeListener
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.icons.IconThemeController
+import com.android.launcher3.icons.mono.MonoIconThemeController
+import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+
+/** Centralized class for managing Launcher icon theming */
+@LauncherAppSingleton
+open class ThemeManager
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    private val prefs: LauncherPrefs,
+    lifecycle: DaggerSingletonTracker,
+) {
+
+    /** Representation of the current icon state */
+    var iconState = parseIconState()
+        private set
+
+    var isMonoThemeEnabled
+        set(value) = prefs.put(THEMED_ICONS, value)
+        get() = prefs.get(THEMED_ICONS)
+
+    var themeController: IconThemeController? =
+        if (isMonoThemeEnabled) MonoIconThemeController() else null
+        private set
+
+    private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
+
+    init {
+        val receiver = SimpleBroadcastReceiver(MAIN_EXECUTOR) { verifyIconState() }
+        receiver.registerPkgActions(context, "android", ACTION_OVERLAY_CHANGED)
+
+        val prefListener = LauncherPrefChangeListener { key ->
+            if (key == THEMED_ICONS.sharedPrefKey) verifyIconState()
+        }
+        prefs.addListener(prefListener, THEMED_ICONS)
+
+        lifecycle.addCloseable {
+            receiver.unregisterReceiverSafely(context)
+            prefs.removeListener(prefListener)
+        }
+    }
+
+    private fun verifyIconState() {
+        val newState = parseIconState()
+        if (newState == iconState) return
+
+        iconState = newState
+        themeController = if (isMonoThemeEnabled) MonoIconThemeController() else null
+
+        listeners.forEach { it.onThemeChanged() }
+    }
+
+    fun addChangeListener(listener: ThemeChangeListener) = listeners.add(listener)
+
+    fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
+
+    private fun parseIconState() =
+        IconState(
+            iconMask =
+                if (CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL) ""
+                else context.resources.getString(CONFIG_ICON_MASK_RES_ID),
+            isMonoTheme = isMonoThemeEnabled,
+        )
+
+    data class IconState(
+        val iconMask: String,
+        val isMonoTheme: Boolean,
+        val themeCode: String = if (isMonoTheme) "with-theme" else "no-theme",
+    ) {
+        fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
+    }
+
+    /** Interface for receiving theme change events */
+    fun interface ThemeChangeListener {
+        fun onThemeChanged()
+    }
+
+    companion object {
+
+        @JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager)
+
+        const val KEY_THEMED_ICONS = "themed_icons"
+        @JvmField val THEMED_ICONS = backedUpItem(KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
+
+        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")
+    }
+}
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index 78a3128..e40f526 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -27,8 +27,8 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.util.ApiWrapper;
-import com.android.launcher3.util.Themes;
 
 import org.xmlpull.v1.XmlPullParser;
 
@@ -48,18 +48,16 @@
     private static final Map<String, ThemeData> DISABLED_MAP = Collections.emptyMap();
 
     private Map<String, ThemeData> mThemedIconMap;
-    private boolean mSupportsIconTheme;
 
     public LauncherIconProvider(Context context) {
         super(context);
-        setIconThemeSupported(Themes.isThemedIconEnabled(context));
+        setIconThemeSupported(ThemeManager.INSTANCE.get(context).isMonoThemeEnabled());
     }
 
     /**
      * Enables or disables icon theme support
      */
     public void setIconThemeSupported(boolean isSupported) {
-        mSupportsIconTheme = isSupported;
         mThemedIconMap = isSupported && FeatureFlags.USE_LOCAL_ICON_OVERRIDES.get()
                 ? null : DISABLED_MAP;
     }
@@ -70,8 +68,9 @@
     }
 
     @Override
-    public String getSystemIconState() {
-        return super.getSystemIconState() + (mSupportsIconTheme ? ",with-theme" : ",no-theme");
+    public void updateSystemState() {
+        super.updateSystemState();
+        mSystemState += "," + ThemeManager.INSTANCE.get(mContext).getIconState().toUniqueId();
     }
 
     @Override
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 839dfb7..04d88b0 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -23,11 +23,10 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.icons.mono.MonoIconThemeController;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.UserIconInfo;
 
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -59,9 +58,7 @@
             ConcurrentLinkedQueue<LauncherIcons> pool) {
         super(context, fillResIconDpi, iconBitmapSize,
                 IconShape.INSTANCE.get(context).getShape().enableShapeDetection());
-        if (Themes.isThemedIconEnabled(context)) {
-            mThemeController = new MonoIconThemeController();
-        }
+        mThemeController = ThemeManager.INSTANCE.get(context).getThemeController();
         mPool = pool;
     }
 
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index f0f2892..9656ac1 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -164,7 +164,7 @@
     }
 
     /**
-     * Returns the folder's contents as an ArrayList of {@link ItemInfo}. Includes
+     * Returns the folder's contents as an unsorted ArrayList of {@link ItemInfo}. Includes
      * {@link WorkspaceItemInfo} and {@link AppPairInfo}s.
      */
     @NonNull
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index fb5c8c7..cde72c1 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -15,7 +15,9 @@
  */
 package com.android.launcher3.testing;
 
+import static com.android.launcher3.Flags.enableFallbackOverviewInWindow;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableLauncherOverviewInWindow;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
@@ -330,6 +332,12 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_IS_RECENTS_WINDOW_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        enableLauncherOverviewInWindow() || enableFallbackOverviewInWindow());
+                return response;
+            }
+
             case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
                 return getLauncherUIProperty(Bundle::putInt,
                         l -> l.getAppsView().getAppsStore().getDeferUpdatesFlags());
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index 104040a..927a2a4 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -19,8 +19,6 @@
 import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_TEXT;
 import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_THEME;
 
-import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
-
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
@@ -32,7 +30,6 @@
 
 import androidx.annotation.ColorInt;
 
-import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.GraphicsUtils;
@@ -44,8 +41,6 @@
 @SuppressWarnings("NewApi")
 public class Themes {
 
-    public static final String KEY_THEMED_ICONS = "themed_icons";
-
     /** Gets the WallpaperColorHints and then uses those to get the correct activity theme res. */
     public static int getActivityThemeRes(Context context) {
         return getActivityThemeRes(context, WallpaperColorHints.get(context).getHints());
@@ -64,13 +59,6 @@
         }
     }
 
-    /**
-     * Returns true if workspace icon theming is enabled
-     */
-    public static boolean isThemedIconEnabled(Context context) {
-        return LauncherPrefs.get(context).get(THEMED_ICONS);
-    }
-
     public static String getDefaultBodyFont(Context context) {
         TypedArray ta = context.obtainStyledAttributes(android.R.style.TextAppearance_DeviceDefault,
                 new int[]{android.R.attr.fontFamily});
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 548cf5b..553d08c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -22,9 +22,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
-import com.android.launcher3.LauncherPrefs.Companion.THEMED_ICONS
-import com.android.launcher3.LauncherPrefs.Companion.get
 import com.android.launcher3.graphics.PreloadIconDrawable
+import com.android.launcher3.graphics.ThemeManager
 import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.icons.FastBitmapDrawable
 import com.android.launcher3.icons.IconCache
@@ -71,6 +70,9 @@
 
     private var defaultThemedIcons = false
 
+    private val themeManager: ThemeManager
+        get() = ThemeManager.INSTANCE.get(context)
+
     @Before
     fun setup() {
         modelHelper = LauncherModelHelper()
@@ -126,19 +128,19 @@
             folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
         folderItems[3].bitmap.themedBitmap = null
 
-        defaultThemedIcons = get(context).get(THEMED_ICONS)
+        defaultThemedIcons = themeManager.isMonoThemeEnabled
     }
 
     @After
     @Throws(Exception::class)
     fun tearDown() {
-        get(context).put(THEMED_ICONS, defaultThemedIcons)
+        themeManager.isMonoThemeEnabled = defaultThemedIcons
         modelHelper.destroy()
     }
 
     @Test
     fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
-        get(context).put(THEMED_ICONS, true)
+        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -148,7 +150,7 @@
 
     @Test
     fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(THEMED_ICONS, false)
+        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -158,7 +160,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
-        get(context).put(THEMED_ICONS, true)
+        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -168,7 +170,7 @@
 
     @Test
     fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
-        get(context).put(THEMED_ICONS, false)
+        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -178,7 +180,7 @@
 
     @Test
     fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
-        get(context).put(THEMED_ICONS, true)
+        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[2])
@@ -191,7 +193,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
-        get(context).put(THEMED_ICONS, true)
+        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -204,7 +206,7 @@
 
     @Test
     fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
-        get(context).put(THEMED_ICONS, false)
+        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
new file mode 100644
index 0000000..43bbad9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.graphics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.FakeLauncherPrefs
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppModule
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.TestUtil
+import dagger.Component
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ThemeManagerTest {
+
+    @get:Rule val context = SandboxApplication()
+
+    lateinit var themeManager: ThemeManager
+
+    @Before
+    fun setUp() {
+        context.initDaggerComponent(DaggerThemeManagerComponent.builder())
+        themeManager = ThemeManager.INSTANCE[context]
+    }
+
+    @Test
+    fun `isMonoThemeEnabled get and set`() {
+        themeManager.isMonoThemeEnabled = true
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertTrue(themeManager.isMonoThemeEnabled)
+        assertTrue(themeManager.iconState.isMonoTheme)
+
+        themeManager.isMonoThemeEnabled = false
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertFalse(themeManager.isMonoThemeEnabled)
+        assertFalse(themeManager.iconState.isMonoTheme)
+    }
+
+    @Test
+    fun `callback called on theme change`() {
+        themeManager.isMonoThemeEnabled = false
+
+        var callbackCalled = false
+        themeManager.addChangeListener { callbackCalled = true }
+        themeManager.isMonoThemeEnabled = true
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+
+        assertTrue(callbackCalled)
+    }
+
+    @Test
+    fun `iconState changes with theme`() {
+        themeManager.isMonoThemeEnabled = false
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        val disabledIconState = themeManager.iconState
+
+        themeManager.isMonoThemeEnabled = true
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertNotEquals(disabledIconState, themeManager.iconState)
+
+        themeManager.isMonoThemeEnabled = false
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertEquals(disabledIconState, themeManager.iconState)
+    }
+}
+
+@LauncherAppSingleton
+@Component(modules = [LauncherAppModule::class])
+interface ThemeManagerComponent : LauncherAppComponent {
+
+    override fun getLauncherPrefs(): FakeLauncherPrefs
+
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+
+        override fun build(): ThemeManagerComponent
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 85e28e8..4055100 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -60,7 +60,8 @@
 
     @Override
     protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
-        return true;
+        return !mLauncher.isRecentsWindowEnabled()
+                || super.zeroButtonToOverviewGestureStateTransitionWhileHolding();
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index bdfe2ab..0d9f5ce 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -954,7 +954,7 @@
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    if (isTablet() && !is3PLauncher()) {
+                    if (isTablet() && !is3PLauncher() && !isRecentsWindowEnabled()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
                     } else {
                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
@@ -1008,6 +1008,11 @@
         }
     }
 
+    boolean isRecentsWindowEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_IS_RECENTS_WINDOW_ENABLED)
+                .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     public void waitForModelQueueCleared() {
         getTestInfo(TestProtocol.REQUEST_MODEL_QUEUE_CLEARED);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index d615879..4a7caf8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -843,7 +843,9 @@
 
     @Override
     protected String getSwipeHeightRequestName() {
-        return TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT;
+        return mLauncher.isRecentsWindowEnabled()
+                ? super.getSwipeHeightRequestName()
+                : TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT;
     }
 
     @Override