Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 8c39585..201c5f6 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -81,7 +81,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="behind"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:enableOnBackInvokedCallback="false"
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 645bef6..6e36305 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -45,7 +45,8 @@
         }
         remoteWindowLimitUnminimizeTransition =
             RemoteTransition(
-                DesktopAppLaunchTransition(context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE)
+                DesktopAppLaunchTransition(context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE),
+                "DesktopWindowLimitUnminimize"
             )
         systemUiProxy.registerRemoteTransition(
             remoteWindowLimitUnminimizeTransition,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 3114bc8..cb811d6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType.UNMINIMIZE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -285,8 +286,8 @@
         ) {
             // This app is being unminimized - use our own transition runner.
             remoteTransition = new RemoteTransition(
-                    new DesktopAppLaunchTransition(
-                        context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE));
+                    new DesktopAppLaunchTransition(context, MAIN_EXECUTOR, UNMINIMIZE),
+                    "DesktopKeyboardQuickSwitchUnminimize");
         }
         mControllers.taskbarActivityContext.handleGroupTaskLaunch(
                 task,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 70cb7ce..8e2246b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -907,7 +907,8 @@
         ActivityOptions options = ActivityOptions.makeRemoteTransition(
                 new RemoteTransition(
                         new DesktopAppLaunchTransition(
-                                /* context= */ this, getMainExecutor(), launchType)));
+                                /* context= */ this, getMainExecutor(), launchType),
+                        "TaskbarDesktopLaunch"));
         return new ActivityOptionsWrapper(options, new RunnableList());
     }
 
@@ -1497,8 +1498,8 @@
 
     private RemoteTransition createUnminimizeRemoteTransition() {
         return new RemoteTransition(
-                new DesktopAppLaunchTransition(
-                        this, getMainExecutor(), AppLaunchType.UNMINIMIZE));
+                new DesktopAppLaunchTransition(this, getMainExecutor(), AppLaunchType.UNMINIMIZE),
+                "TaskbarDesktopUnminimize");
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ff8e4a8..9407e73 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -113,7 +113,7 @@
     private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
-    private final Context mContext;
+    private final Context mWindowContext;
     private final @Nullable Context mNavigationBarPanelContext;
     private WindowManager mWindowManager;
     private boolean mAddedWindow;
@@ -231,7 +231,7 @@
             @NonNull DesktopVisibilityController desktopVisibilityController) {
         Display display =
                 context.getSystemService(DisplayManager.class).getDisplay(context.getDisplayId());
-        mContext = context.createWindowContext(display,
+        mWindowContext = context.createWindowContext(display,
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
                 null);
         mAllAppsActionManager = allAppsActionManager;
@@ -240,30 +240,47 @@
                 : null;
         mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
-            mWindowManager = mContext.getSystemService(WindowManager.class);
-            FrameLayout taskbarRootLayout = new FrameLayout(mContext) {
-                @Override
-                public boolean dispatchTouchEvent(MotionEvent ev) {
-                    // The motion events can be outside the view bounds of task bar, and hence
-                    // manually dispatching them to the drag layer here.
-                    TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
-                    if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
-                        return taskbar.getDragLayer().dispatchTouchEvent(ev);
-                    }
-                    return super.dispatchTouchEvent(ev);
-                }
-            };
-            addTaskbarRootLayoutToMap(getDefaultDisplayId(), taskbarRootLayout);
+            mWindowManager = mWindowContext.getSystemService(WindowManager.class);
+            createTaskbarRootLayout(getDefaultDisplayId());
         }
-        mDefaultNavButtonController = new TaskbarNavButtonController(
+        mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
+        mDefaultComponentCallbacks = createDefaultComponentCallbacks();
+        SettingsCache.INSTANCE.get(mWindowContext)
+                .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
+        SettingsCache.INSTANCE.get(mWindowContext)
+                .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
+        mWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.register(mWindowContext, Intent.ACTION_SHUTDOWN);
+        UI_HELPER_EXECUTOR.execute(() -> {
+            mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
+                    mWindowContext,
+                    SYSTEM_ACTION_ID_TASKBAR,
+                    new Intent(ACTION_SHOW_TASKBAR).setPackage(mWindowContext.getPackageName()),
+                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+            mTaskbarBroadcastReceiver.register(
+                    mWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+        });
+
+        debugWhyTaskbarNotDestroyed("TaskbarManager created");
+        recreateTaskbar();
+    }
+
+    @NonNull
+    private TaskbarNavButtonController createDefaultNavButtonController(Context context,
+            TaskbarNavButtonCallbacks navCallbacks) {
+        return new TaskbarNavButtonController(
                 context,
                 navCallbacks,
-                SystemUiProxy.INSTANCE.get(mContext),
-                ContextualEduStatsManager.INSTANCE.get(mContext),
+                SystemUiProxy.INSTANCE.get(mWindowContext),
+                ContextualEduStatsManager.INSTANCE.get(mWindowContext),
                 new Handler(),
-                new ContextualSearchInvoker(mContext));
-        mDefaultComponentCallbacks = new ComponentCallbacks() {
-            private Configuration mOldConfig = mContext.getResources().getConfiguration();
+                new ContextualSearchInvoker(mWindowContext));
+    }
+
+    private ComponentCallbacks createDefaultComponentCallbacks() {
+        return new ComponentCallbacks() {
+            private Configuration mOldConfig = mWindowContext.getResources().getConfiguration();
 
             @Override
             public void onConfigurationChanged(Configuration newConfig) {
@@ -273,7 +290,7 @@
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
                 // TODO: adapt this logic to be specific to different displays.
                 DeviceProfile dp = mUserUnlocked
-                        ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
+                        ? LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext)
                         : null;
                 int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
 
@@ -317,25 +334,6 @@
             @Override
             public void onLowMemory() { }
         };
-        SettingsCache.INSTANCE.get(mContext)
-                .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(mContext)
-                .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
-        Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        mContext.registerComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
-        UI_HELPER_EXECUTOR.execute(() -> {
-            mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
-                    mContext,
-                    SYSTEM_ACTION_ID_TASKBAR,
-                    new Intent(ACTION_SHOW_TASKBAR).setPackage(mContext.getPackageName()),
-                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-            mTaskbarBroadcastReceiver.register(
-                    mContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
-        });
-
-        debugWhyTaskbarNotDestroyed("TaskbarManager created");
-        recreateTaskbar();
     }
 
     private void destroyAllTaskbars() {
@@ -360,7 +358,7 @@
             removeTaskbarFromMap(displayId);
         }
         DeviceProfile dp = mUserUnlocked ?
-                LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
+                LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
         if (dp == null || !isTaskbarEnabled(dp)) {
             removeTaskbarRootViewFromWindow(displayId);
         }
@@ -407,7 +405,7 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
-        DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
+        DisplayController.INSTANCE.get(mWindowContext).addChangeListener(mRecreationListener);
         recreateTaskbar();
         addTaskbarRootViewToWindow(getDefaultDisplayId());
     }
@@ -470,7 +468,7 @@
                 return ql.getUnfoldTransitionProgressProvider();
             }
         } else {
-            return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider();
+            return SystemUiProxy.INSTANCE.get(mWindowContext).getUnfoldTransitionProvider();
         }
         return null;
     }
@@ -514,7 +512,7 @@
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
-                LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
+                    LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
 
             // All Apps action is unrelated to navbar unification, so we only need to check DP.
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -528,7 +526,7 @@
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
             if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
-                SystemUiProxy.INSTANCE.get(mContext)
+                SystemUiProxy.INSTANCE.get(mWindowContext)
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
                 if (!isTaskbarEnabled) {
                     return;
@@ -537,9 +535,7 @@
 
             TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
             if (enableTaskbarNoRecreate() || taskbar == null) {
-                taskbar = new TaskbarActivityContext(mContext,
-                        mNavigationBarPanelContext, dp, mDefaultNavButtonController,
-                        mUnfoldProgressProvider, mDesktopVisibilityController);
+                taskbar = createTaskbarActivityContext(dp, displayId);
             } else {
                 taskbar.updateDeviceProfile(dp);
             }
@@ -560,7 +556,6 @@
                 taskbarRootLayout.addView(taskbar.getDragLayer());
                 taskbar.notifyUpdateLayoutParams();
             }
-            addTaskbarToMap(displayId, taskbar);
         } finally {
             Trace.endSection();
         }
@@ -716,22 +711,23 @@
         mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
-        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
+        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mWindowContext);
         destroyAllTaskbars();
         if (mUserUnlocked) {
-            DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
+            DisplayController.INSTANCE.get(mWindowContext).removeChangeListener(
+                    mRecreationListener);
         }
-        SettingsCache.INSTANCE.get(mContext)
+        SettingsCache.INSTANCE.get(mWindowContext)
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(mContext)
+        SettingsCache.INSTANCE.get(mWindowContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.unregisterReceiverSafely(mContext);
+        mWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.unregisterReceiverSafely(mWindowContext);
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
-        return getTaskbarForDisplay(mContext.getDisplayId());
+        return getTaskbarForDisplay(mWindowContext.getDisplayId());
     }
 
     public void dumpLogs(String prefix, PrintWriter pw) {
@@ -773,6 +769,19 @@
         return mTaskbars.get(displayId);
     }
 
+
+    /**
+     * Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
+     */
+    private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
+        TaskbarActivityContext newTaskbar = new TaskbarActivityContext(mWindowContext,
+                mNavigationBarPanelContext, dp, mDefaultNavButtonController,
+                mUnfoldProgressProvider, mDesktopVisibilityController);
+
+        addTaskbarToMap(displayId, newTaskbar);
+        return newTaskbar;
+    }
+
     /**
      * Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
      * map if there is not already a taskbar mapped to that displayId.
@@ -796,6 +805,26 @@
     }
 
     /**
+     * Creates {@link FrameLayout} for the taskbar on the specified display and adds it to map.
+     * @param displayId The ID of the display for which to create the taskbar root layout.
+     */
+    private void createTaskbarRootLayout(int displayId) {
+        FrameLayout newTaskbarRootLayout = new FrameLayout(mWindowContext) {
+            @Override
+            public boolean dispatchTouchEvent(MotionEvent ev) {
+                // The motion events can be outside the view bounds of task bar, and hence
+                // manually dispatching them to the drag layer here.
+                TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+                if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
+                    return taskbar.getDragLayer().dispatchTouchEvent(ev);
+                }
+                return super.dispatchTouchEvent(ev);
+            }
+        };
+        addTaskbarRootLayoutToMap(displayId, newTaskbarRootLayout);
+    }
+
+    /**
      * Retrieves the root layout of the taskbar for the specified display.
      *
      * @param displayId The ID of the display for which to retrieve the taskbar root layout.
@@ -812,7 +841,7 @@
      * @param rootLayout The taskbar root layout {@link FrameLayout} to add to the map.
      */
     private void addTaskbarRootLayoutToMap(int displayId, FrameLayout rootLayout) {
-        if (!mRootLayouts.contains(displayId)) {
+        if (!mRootLayouts.contains(displayId) && rootLayout != null) {
             mRootLayouts.put(displayId, rootLayout);
         }
     }
@@ -829,7 +858,7 @@
     }
 
     private int getDefaultDisplayId() {
-        return mContext.getDisplayId();
+        return mWindowContext.getDisplayId();
     }
 
     /** Temp logs for b/254119092. */
@@ -839,15 +868,15 @@
 
         boolean activityTaskbarPresent = mActivity != null
                 && mActivity.getDeviceProfile().isTaskbarPresent;
-        boolean contextTaskbarPresent = mUserUnlocked
-                && LauncherAppState.getIDP(mContext).getDeviceProfile(mContext).isTaskbarPresent;
+        boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(mWindowContext)
+                .getDeviceProfile(mWindowContext).isTaskbarPresent;
         if (activityTaskbarPresent == contextTaskbarPresent) {
-            log.add("mActivity and mContext agree taskbarIsPresent=" + contextTaskbarPresent);
+            log.add("mActivity and mWindowContext agree taskbarIsPresent=" + contextTaskbarPresent);
             Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
             return;
         }
 
-        log.add("mActivity and mContext device profiles have different values, add more logs.");
+        log.add("mActivity & mWindowContext device profiles have different values, add more logs.");
 
         log.add("\tmActivity logs:");
         log.add("\t\tmActivity=" + mActivity);
@@ -857,13 +886,13 @@
             log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent="
                     + activityTaskbarPresent);
         }
-        log.add("\tmContext logs:");
-        log.add("\t\tmContext=" + mContext);
-        log.add("\t\tmContext.getResources().getConfiguration()="
-                + mContext.getResources().getConfiguration());
+        log.add("\tmWindowContext logs:");
+        log.add("\t\tmWindowContext=" + mWindowContext);
+        log.add("\t\tmWindowContext.getResources().getConfiguration()="
+                + mWindowContext.getResources().getConfiguration());
         if (mUserUnlocked) {
-            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mContext).isTaskbarPresent="
-                    + contextTaskbarPresent);
+            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mWindowContext)"
+                    + ".isTaskbarPresent=" + contextTaskbarPresent);
         } else {
             log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
         }
@@ -876,6 +905,6 @@
 
     @VisibleForTesting
     public Context getWindowContext() {
-        return mContext;
+        return mWindowContext;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 374db6a..2e2d7cc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -63,7 +63,7 @@
 
     override fun createFadeOutAnimOptions(): ActivityOptions =
         ActivityOptions.makeBasic().apply {
-            remoteTransition = RemoteTransition(FadeOutRemoteTransition())
+            remoteTransition = RemoteTransition(FadeOutRemoteTransition(), "FadeOut")
         }
 
     override fun queryAllUsers(): Map<UserHandle, UserIconInfo> {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 9164405..9dec332 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -256,7 +256,7 @@
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
         mLauncher.getActionsView().getVisibilityAlpha().updateValue(
                 (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
-        mRecentsView.setTaskIconScaledDown(true);
+        mRecentsView.setTaskIconVisible(false);
 
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
@@ -340,7 +340,7 @@
                 public void onAnimationEnd(Animator animation) {
                     onAnimationToStateCompleted(OVERVIEW);
                     // Animate the icon after onAnimationToStateCompleted() so it doesn't clobber.
-                    mRecentsView.animateUpTaskIconScale();
+                    mRecentsView.startIconFadeInOnGestureComplete();
                 }
             });
             overviewAnim.start();
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 6075294..61a150b 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -19,6 +19,8 @@
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
+import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
+import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_RECREATE_TO_UPDATE_THEME;
 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_PRE_DELAY;
@@ -362,6 +364,14 @@
     }
 
     @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);
@@ -382,6 +392,32 @@
 
         // Set screen title for Talkback
         setTitle(R.string.accessibility_recent_apps);
+
+        restoreState(savedInstanceState);
+    }
+
+    @Override
+    protected void onSaveInstanceState(@NonNull Bundle outState) {
+        outState.putInt(RUNTIME_STATE, mStateManager.getState().ordinal);
+        super.onSaveInstanceState(outState);
+    }
+
+    /**
+     * Restores the previous state, if it exists.
+     *
+     * @param savedState The previous state.
+     */
+    private void restoreState(Bundle savedState) {
+        if (savedState == null) {
+            return;
+        }
+
+        if (savedState.getBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME)) {
+            // RecentsState is only restored after theme changes.
+            int stateOrdinal = savedState.getInt(RUNTIME_STATE, RecentsState.DEFAULT.ordinal);
+            RecentsState recentsState = RecentsState.stateFromOrdinal(stateOrdinal);
+            mStateManager.goToState(recentsState, /*animated=*/false);
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 34783c7..c2e7536 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -45,6 +45,8 @@
     private static final int FLAG_TASK_THUMBNAIL_SPLASH = BaseState.getFlag(8);
     private static final int FLAG_DETACH_DESKTOP_CAROUSEL = BaseState.getFlag(9);
 
+    private static final RecentsState[] sAllStates = new RecentsState[6];
+
     public static final RecentsState DEFAULT = new RecentsState(0,
             FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID
                     | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE);
@@ -61,6 +63,11 @@
             FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_RECENTS_VIEW_VISIBLE | FLAG_CLOSE_POPUPS
                     | FLAG_DISABLE_RESTORE);
 
+    /** Returns the corresponding RecentsState from ordinal provided */
+    public static RecentsState stateFromOrdinal(int ordinal) {
+        return sAllStates[ordinal];
+    }
+
     public final int ordinal;
     private final int mFlags;
 
@@ -70,6 +77,7 @@
     public RecentsState(int id, int flags) {
         this.ordinal = id;
         this.mFlags = flags;
+        sAllStates[id] = this;
     }
 
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index e7cb05e..da5411b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -691,7 +691,7 @@
     protected boolean mRunningTaskTileHidden;
     protected int mFocusedTaskViewId = INVALID_TASK_ID;
 
-    private boolean mTaskIconScaledDown = false;
+    private boolean mTaskIconVisible = true;
     private boolean mRunningTaskShowScreenshot = false;
     private float mRunningTaskAttachAlpha;
 
@@ -1244,24 +1244,21 @@
         // - It's the focused task to be moved to the front, we immediately re-add the task
         if (child instanceof TaskView && child != mSplitHiddenTaskView
                 && child != mMovingTaskView) {
-            clearAndRecycleTaskView((TaskView) child);
+            TaskView taskView = (TaskView) child;
+            for (int i : taskView.getTaskIds()) {
+                mHasVisibleTaskData.delete(i);
+            }
+            if (child instanceof GroupedTaskView) {
+                mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+            } else if (child instanceof DesktopTaskView) {
+                mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+            } else {
+                mTaskViewPool.recycle(taskView);
+            }
+            mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         }
     }
 
-    private void clearAndRecycleTaskView(TaskView taskView) {
-        for (int taskId : taskView.getTaskIds()) {
-            mHasVisibleTaskData.delete(taskId);
-        }
-        if (taskView instanceof GroupedTaskView) {
-            mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
-        } else if (taskView instanceof DesktopTaskView) {
-            mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
-        } else {
-            mTaskViewPool.recycle(taskView);
-        }
-        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
-    }
-
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
@@ -2120,7 +2117,7 @@
             if (Arrays.stream(taskView.getTaskIds()).noneMatch(
                     taskId -> taskId == mIgnoreResetTaskId)) {
                 taskView.resetViewTransforms();
-                taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+                taskView.setIconVisibleForGesture(mTaskIconVisible);
                 taskView.setStableAlpha(mContentAlpha);
                 taskView.setFullscreenProgress(mFullscreenProgress);
                 taskView.setModalness(mTaskModalness);
@@ -2788,7 +2785,7 @@
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
-        setTaskIconScaledDown(true);
+        setTaskIconVisible(false);
     }
 
     /**
@@ -2807,7 +2804,7 @@
      * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
      */
     public void onSwipeUpAnimationSuccess() {
-        animateUpTaskIconScale();
+        startIconFadeInOnGestureComplete();
         setSwipeDownShouldLaunchApp(true);
     }
 
@@ -2934,7 +2931,7 @@
         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
         Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
         setRunningTaskHidden(false);
-        animateUpTaskIconScale();
+        startIconFadeInOnGestureComplete();
         animateActionsViewIn();
 
         mCurrentGestureEndTarget = null;
@@ -3058,7 +3055,7 @@
 
         if (mRunningTaskViewId != -1) {
             // Reset the state on the old running task view
-            setTaskIconScaledDown(false);
+            setTaskIconVisible(true);
             setRunningTaskViewShowScreenshot(true);
             setRunningTaskHidden(false);
         }
@@ -3127,25 +3124,31 @@
         }
     }
 
-    public void setTaskIconScaledDown(boolean isScaledDown) {
-        if (mTaskIconScaledDown != isScaledDown) {
-            mTaskIconScaledDown = isScaledDown;
+    /**
+     * Updates icon visibility when going in or out of overview.
+     */
+    public void setTaskIconVisible(boolean isVisible) {
+        if (mTaskIconVisible != isVisible) {
+            mTaskIconVisible = isVisible;
             for (TaskView taskView : getTaskViews()) {
-                taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+                taskView.setIconVisibleForGesture(mTaskIconVisible);
             }
         }
     }
 
     private void animateActionsViewIn() {
         if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) {
-            animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION);
+            animateActionsViewAlpha(1, TaskView.FADE_IN_ICON_DURATION);
         }
     }
 
-    public void animateUpTaskIconScale() {
-        mTaskIconScaledDown = false;
+    /**
+     * Updates icon visibility when gesture is settled.
+     */
+    public void startIconFadeInOnGestureComplete() {
+        mTaskIconVisible = true;
         for (TaskView taskView : getTaskViews()) {
-            taskView.animateIconScaleAndDimIntoView();
+            taskView.startIconFadeInOnGestureComplete();
         }
     }
 
@@ -3935,7 +3938,7 @@
                     anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
                             secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
                                     dismissTranslationInterpolationEnd));
-                    anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
+                    anim.add(taskView.getDismissIconFadeOutAnimator(),
                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
                 } else if ((isFocusedTaskDismissed && nextFocusedTaskView != null && isSameGridRow(
                         taskView, nextFocusedTaskView))
@@ -4119,7 +4122,7 @@
                                     ? INVALID_TASK_ID
                                     : finalNextFocusedTaskView.getTaskViewId());
                             mTopRowIdSet.remove(mFocusedTaskViewId);
-                            finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
+                            finalNextFocusedTaskView.getDismissIconFadeInAnimator().start();
                         }
                         updateTaskSize();
                         updateChildTaskOrientations();
@@ -5407,13 +5410,6 @@
         mSplitHiddenTaskViewIndex = -1;
         if (mSplitHiddenTaskView != null) {
             mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
-            // mSplitHiddenTaskView is set when split select animation starts. The TaskView is only
-            // removed when when the animation finishes. So in the case of overview being dismissed
-            // during the animation, we should not call clearAndRecycleTaskView() because it has
-            // not been removed yet.
-            if (mSplitHiddenTaskView.getParent() == null) {
-                clearAndRecycleTaskView(mSplitHiddenTaskView);
-            }
             mSplitHiddenTaskView = null;
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 082971c..d207edf 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -434,37 +434,54 @@
     // Used to cache thumbnail bounds to avoid recalculating on every hover move.
     private var thumbnailBounds = Rect()
 
-    private var focusTransitionProgress = 1f
+    // Progress variable indicating if the TaskView is in a settled state:
+    // 0 = The TaskView is in a transitioning state e.g. during gesture, in quickswitch carousel,
+    // becoming focus task etc.
+    // 1 = The TaskView is settled and no longer transitioning
+    private var settledProgress = 1f
         set(value) {
             field = value
-            onFocusTransitionProgressUpdated(field)
+            onSettledProgressUpdated(field)
         }
 
-    private val focusTransitionPropertyFactory =
+    private val settledProgressPropertyFactory =
         MultiPropertyFactory(
             this,
-            FOCUS_TRANSITION,
-            FOCUS_TRANSITION_INDEX_COUNT,
+            SETTLED_PROGRESS,
+            SETTLED_PROGRESS_INDEX_COUNT,
             { x: Float, y: Float -> x * y },
             1f,
         )
-    private val focusTransitionFullscreen =
-        focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
-    private val focusTransitionScaleAndDim =
-        focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM)
+    private val settledProgressFullscreen =
+        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_FULLSCREEN)
+    private val settledProgressGesture =
+        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_GESTURE)
+    private val settledProgressDismiss =
+        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_DISMISS)
 
     /**
-     * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in
+     * Returns an animator of [settledProgressDismiss] that transition in with a built-in
      * interpolator.
      */
-    fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator =
+    fun getDismissIconFadeInAnimator(): ObjectAnimator =
+        ObjectAnimator.ofFloat(settledProgressDismiss, MULTI_PROPERTY_VALUE, 1f).apply {
+            duration = FADE_IN_ICON_DURATION
+            interpolator = FADE_IN_ICON_INTERPOLATOR
+        }
+
+    /**
+     * Returns an animator of [settledProgressDismiss] that transition out with a built-in
+     * interpolator. [AnimatedFloat] is used to apply another level of interpolation, on top of
+     * interpolator set to the [Animator] by the caller.
+     */
+    fun getDismissIconFadeOutAnimator(): ObjectAnimator =
         AnimatedFloat { v ->
-                focusTransitionScaleAndDim.value =
-                    FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v)
+                settledProgressDismiss.value =
+                    SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v)
             }
             .animateToValue(1f, 0f)
 
-    private var iconAndDimAnimator: ObjectAnimator? = null
+    private var iconFadeInOnGestureCompleteAnimator: ObjectAnimator? = null
     // The current background requests to load the task thumbnail and icon
     private val pendingThumbnailLoadRequests = mutableListOf<CancellableTask<*>>()
     private val pendingIconLoadRequests = mutableListOf<CancellableTask<*>>()
@@ -1423,23 +1440,23 @@
      * Called to animate a smooth transition when going directly from an app into Overview (and vice
      * versa). Icons fade in, and DWB banners slide in with a "shift up" animation.
      */
-    private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) {
+    private fun onSettledProgressUpdated(settledProgress: Float) {
         taskContainers.forEach {
-            it.iconView.setContentAlpha(focusTransitionProgress)
-            it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - focusTransitionProgress
+            it.iconView.setContentAlpha(settledProgress)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - settledProgress
         }
     }
 
-    fun animateIconScaleAndDimIntoView() {
-        iconAndDimAnimator?.cancel()
-        iconAndDimAnimator =
-            ObjectAnimator.ofFloat(focusTransitionScaleAndDim, MULTI_PROPERTY_VALUE, 0f, 1f).apply {
-                duration = SCALE_ICON_DURATION
+    fun startIconFadeInOnGestureComplete() {
+        iconFadeInOnGestureCompleteAnimator?.cancel()
+        iconFadeInOnGestureCompleteAnimator =
+            ObjectAnimator.ofFloat(settledProgressGesture, MULTI_PROPERTY_VALUE, 1f).apply {
+                duration = FADE_IN_ICON_DURATION
                 interpolator = Interpolators.LINEAR
                 addListener(
                     object : AnimatorListenerAdapter() {
                         override fun onAnimationEnd(animation: Animator) {
-                            iconAndDimAnimator = null
+                            iconFadeInOnGestureCompleteAnimator = null
                         }
                     }
                 )
@@ -1447,9 +1464,9 @@
             }
     }
 
-    fun setIconScaleAndDim(iconScale: Float) {
-        iconAndDimAnimator?.cancel()
-        focusTransitionScaleAndDim.value = iconScale
+    fun setIconVisibleForGesture(isVisible: Boolean) {
+        iconFadeInOnGestureCompleteAnimator?.cancel()
+        settledProgressGesture.value = if (isVisible) 1f else 0f
     }
 
     /** Set a color tint on the snapshot and supporting views. */
@@ -1544,8 +1561,8 @@
             it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE)
             it.overlay.setFullscreenProgress(fullscreenProgress)
         }
-        focusTransitionFullscreen.value =
-            FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
+        settledProgressFullscreen.value =
+            SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
         updateFullscreenParams()
     }
 
@@ -1602,7 +1619,8 @@
         }
         dismissScale = 1f
         translationZ = 0f
-        setIconScaleAndDim(1f)
+        setIconVisibleForGesture(true)
+        settledProgressDismiss.value = 1f
         setColorTint(0f, 0)
     }
 
@@ -1630,9 +1648,10 @@
         const val FLAG_UPDATE_ALL =
             (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS)
 
-        const val FOCUS_TRANSITION_INDEX_FULLSCREEN = 0
-        const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1
-        const val FOCUS_TRANSITION_INDEX_COUNT = 2
+        const val SETTLED_PROGRESS_INDEX_FULLSCREEN = 0
+        const val SETTLED_PROGRESS_INDEX_GESTURE = 1
+        const val SETTLED_PROGRESS_INDEX_DISMISS = 2
+        const val SETTLED_PROGRESS_INDEX_COUNT = 3
 
         private const val ALPHA_INDEX_STABLE = 0
         private const val ALPHA_INDEX_ATTACH = 1
@@ -1642,25 +1661,26 @@
 
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
-        const val SCALE_ICON_DURATION: Long = 120
+        const val FADE_IN_ICON_DURATION: Long = 120
         private const val DIM_ANIM_DURATION: Long = 700
-        private const val FOCUS_TRANSITION_THRESHOLD =
-            SCALE_ICON_DURATION.toFloat() / DIM_ANIM_DURATION
-        val FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR =
+        private const val SETTLE_TRANSITION_THRESHOLD =
+            FADE_IN_ICON_DURATION.toFloat() / DIM_ANIM_DURATION
+        val SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR =
             Interpolators.clampToProgress(
                 Interpolators.FAST_OUT_SLOW_IN,
-                1f - FOCUS_TRANSITION_THRESHOLD,
+                1f - SETTLE_TRANSITION_THRESHOLD,
                 1f,
             )!!
+        private val FADE_IN_ICON_INTERPOLATOR = Interpolators.LINEAR
         private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect())
 
-        private val FOCUS_TRANSITION: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("focusTransition") {
+        private val SETTLED_PROGRESS: FloatProperty<TaskView> =
+            object : FloatProperty<TaskView>("settleTransition") {
                 override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.focusTransitionProgress = v
+                    taskView.settledProgress = v
                 }
 
-                override fun get(taskView: TaskView) = taskView.focusTransitionProgress
+                override fun get(taskView: TaskView) = taskView.settledProgress
             }
 
         private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index d9f9769..4a6340a 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -47,7 +47,7 @@
     <color name="widget_picker_unselected_tab_text_color_dark">
         @android:color/system_neutral2_200</color>
     <color name="widget_picker_collapse_handle_color_dark">
-        @android:color/system_neutral2_700</color>
+        @android:color/system_neutral2_400</color>
     <color name="widget_picker_add_button_background_color_dark">
         @android:color/system_accent1_200</color>
     <color name="widget_picker_add_button_text_color_dark">
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index d74e308..8b43f20 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -98,7 +98,7 @@
     <color name="widget_picker_unselected_tab_text_color_light">
         @android:color/system_neutral2_700</color>
     <color name="widget_picker_collapse_handle_color_light">
-        @android:color/system_neutral2_200</color>
+        @android:color/system_neutral2_500</color>
     <color name="widget_picker_add_button_background_color_light">
         @android:color/system_accent1_600</color>
     <color name="widget_picker_add_button_text_color_light">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 967d97d..914ffd6 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -114,7 +114,7 @@
     <color name="widget_picker_tab_background_unselected_light">#E3E3E3</color>
     <color name="widget_picker_selected_tab_text_color_light">#FFFFFF</color>
     <color name="widget_picker_unselected_tab_text_color_light">#444746</color>
-    <color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
+    <color name="widget_picker_collapse_handle_color_light">#747775</color>
     <color name="widget_picker_add_button_background_color_light">#0B57D0</color>
     <color name="widget_picker_add_button_text_color_light">#0B57D0</color>
     <color name="widget_picker_expand_button_background_color_light">
@@ -141,7 +141,7 @@
     <color name="widget_picker_tab_background_unselected_dark">#343535</color>
     <color name="widget_picker_selected_tab_text_color_dark">#2D312F</color>
     <color name="widget_picker_unselected_tab_text_color_dark">#C4C7C5</color>
-    <color name="widget_picker_collapse_handle_color_dark">#444746</color>
+    <color name="widget_picker_collapse_handle_color_dark">#8e918f</color>
     <color name="widget_picker_add_button_background_color_dark">#062E6F</color>
     <color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
     <color name="widget_picker_expand_button_background_color_dark">
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5b8d2fc..cb021c7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -423,8 +423,6 @@
     private final SettingsCache.OnChangeListener mNaturalScrollingChangedListener =
             enabled -> mIsNaturalScrollingEnabled = enabled;
 
-    private boolean mRecreateToUpdateTheme = false;
-
     public static Launcher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -1751,12 +1749,6 @@
     }
 
     @Override
-    protected void recreateToUpdateTheme() {
-        mRecreateToUpdateTheme = true;
-        super.recreateToUpdateTheme();
-    }
-
-    @Override
     public void onRestoreInstanceState(Bundle state) {
         super.onRestoreInstanceState(state);
         IntSet synchronouslyBoundPages = mModelCallbacks.getSynchronouslyBoundPages();
@@ -1801,8 +1793,6 @@
             outState.putParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT, mPendingActivityResult);
         }
 
-        outState.putBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME, mRecreateToUpdateTheme);
-
         super.onSaveInstanceState(outState);
     }
 
diff --git a/src/com/android/launcher3/LauncherConstants.java b/src/com/android/launcher3/LauncherConstants.java
index 0ed239d..5cba82f 100644
--- a/src/com/android/launcher3/LauncherConstants.java
+++ b/src/com/android/launcher3/LauncherConstants.java
@@ -68,7 +68,7 @@
         // Type int[]
         static final String RUNTIME_STATE_CURRENT_SCREEN_IDS = "launcher.current_screen_ids";
         // Type: boolean
-        static final String RUNTIME_STATE_RECREATE_TO_UPDATE_THEME =
+        public static final String RUNTIME_STATE_RECREATE_TO_UPDATE_THEME =
                 "launcher.recreate_to_update_theme";
     }
 }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 71a2589..ebcb5da 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3;
 
-import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
-
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
@@ -36,9 +34,6 @@
 import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BlendMode;
-import android.graphics.BlendModeColorFilter;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.LightingColorFilter;
@@ -49,10 +44,8 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
 import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.os.DeadObjectException;
@@ -85,7 +78,6 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.icons.ThemedIconDrawable;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
@@ -696,29 +688,19 @@
             return null;
         }
 
-        // Inject monochrome icon drawable
+        // Inject theme icon drawable
         if (ATLEAST_T && useTheme) {
-            result.mutate();
-            int[] colors = ThemedIconDrawable.getColors(context);
-            Drawable mono = result.getMonochrome();
-
-            if (mono != null) {
-                mono.setTint(colors[1]);
-            } else  if (info instanceof ItemInfoWithIcon iiwi) {
-                // Inject a previously generated monochrome icon
-                Bitmap monoBitmap = iiwi.bitmap.getMono();
-                if (monoBitmap != null) {
-                    // Use BitmapDrawable instead of FastBitmapDrawable so that the colorState is
-                    // preserved in constantState
-                    mono = new BitmapDrawable(monoBitmap);
-                    mono.setColorFilter(new BlendModeColorFilter(colors[1], BlendMode.SRC_IN));
-                    // Inset the drawable according to the AdaptiveIconDrawable layers
-                    mono = new InsetDrawable(mono, getExtraInsetFraction() / 2);
+            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;
+                    }
                 }
             }
-            if (mono != null) {
-                result = new AdaptiveIconDrawable(new ColorDrawable(colors[0]), mono);
-            }
         }
 
         if (badge == null) {
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 884d448..839dfb7 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -17,15 +17,13 @@
 package com.android.launcher3.icons;
 
 import android.content.Context;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.icons.mono.MonoIconThemeController;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
@@ -57,13 +55,13 @@
 
     private final ConcurrentLinkedQueue<LauncherIcons> mPool;
 
-    private MonochromeIconFactory mMonochromeIconFactory;
-
     protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize,
             ConcurrentLinkedQueue<LauncherIcons> pool) {
         super(context, fillResIconDpi, iconBitmapSize,
                 IconShape.INSTANCE.get(context).getShape().enableShapeDetection());
-        mMonoIconEnabled = Themes.isThemedIconEnabled(context);
+        if (Themes.isThemedIconEnabled(context)) {
+            mThemeController = new MonoIconThemeController();
+        }
         mPool = pool;
     }
 
@@ -75,18 +73,6 @@
         mPool.add(this);
     }
 
-    @Override
-    protected Drawable getMonochromeDrawable(AdaptiveIconDrawable base) {
-        Drawable mono = super.getMonochromeDrawable(base);
-        if (mono != null || !Flags.forceMonochromeAppIcons()) {
-            return mono;
-        }
-        if (mMonochromeIconFactory == null) {
-            mMonochromeIconFactory = new MonochromeIconFactory(mIconBitmapSize);
-        }
-        return mMonochromeIconFactory.wrap(base);
-    }
-
     @NonNull
     @Override
     protected UserIconInfo getUserInfo(@NonNull UserHandle user) {
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 079191f..f21e5da 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -18,6 +18,7 @@
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
+import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_RECREATE_TO_UPDATE_THEME;
 import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 
 import android.content.Context;
@@ -29,6 +30,7 @@
 import android.view.View;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.LauncherRootView;
@@ -54,6 +56,7 @@
 
     protected Configuration mOldConfig;
     private int mOldRotation;
+    private boolean mRecreateToUpdateTheme = false;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -63,6 +66,18 @@
         mOldRotation = WindowManagerProxy.INSTANCE.get(this).getRotation(this);
     }
 
+    @Override
+    protected void onSaveInstanceState(@NonNull Bundle outState) {
+        outState.putBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME, mRecreateToUpdateTheme);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    protected void recreateToUpdateTheme() {
+        mRecreateToUpdateTheme = true;
+        super.recreateToUpdateTheme();
+    }
+
     /**
      * Create handlers to control the property changes for this activity
      */
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 111ffaa..7f481b7 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -18,6 +18,7 @@
 
 import android.R
 import android.content.Context
+import android.graphics.Bitmap
 import android.os.Process
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -26,9 +27,9 @@
 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.icons.BaseIconFactory
 import com.android.launcher3.icons.FastBitmapDrawable
 import com.android.launcher3.icons.UserBadgeDrawable
+import com.android.launcher3.icons.mono.MonoThemedBitmap
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
@@ -88,54 +89,26 @@
         // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
         val folderApps = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
         // Set first icon to be themed.
-        folderApps[0]
-            .bitmap
-            .setMonoIcon(
+        folderApps[0].bitmap.themedBitmap =
+            MonoThemedBitmap(
                 folderApps[0].bitmap.icon,
-                BaseIconFactory(
-                    context,
-                    context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize,
-                ),
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
             )
 
         // Set second icon to be non-themed.
-        folderApps[1]
-            .bitmap
-            .setMonoIcon(
-                null,
-                BaseIconFactory(
-                    context,
-                    context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize,
-                ),
-            )
+        folderApps[1].bitmap.themedBitmap = null
 
         // Set third icon to be themed with badge.
-        folderApps[2]
-            .bitmap
-            .setMonoIcon(
+        folderApps[2].bitmap.themedBitmap =
+            MonoThemedBitmap(
                 folderApps[2].bitmap.icon,
-                BaseIconFactory(
-                    context,
-                    context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize,
-                ),
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
             )
         folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
 
         // Set fourth icon to be non-themed with badge.
         folderApps[3].bitmap = folderApps[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
-        folderApps[3]
-            .bitmap
-            .setMonoIcon(
-                null,
-                BaseIconFactory(
-                    context,
-                    context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize,
-                ),
-            )
+        folderApps[3].bitmap.themedBitmap = null
 
         defaultThemedIcons = get(context).get(THEMED_ICONS)
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoIconThemeControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoIconThemeControllerTest.kt
new file mode 100644
index 0000000..4af564e
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoIconThemeControllerTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.icons.mono
+
+import android.graphics.Color
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.ColorDrawable
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.platform.uiautomatorhelpers.DeviceHelpers.context
+import android.util.DisplayMetrics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assume.assumeFalse
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MonoIconThemeControllerTest {
+
+    @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+    private val iconFactory = BaseIconFactory(context, DisplayMetrics.DENSITY_MEDIUM, 30)
+
+    @Test
+    fun `createThemedBitmap when mono drawable is present`() {
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, ColorDrawable(Color.RED))
+        assertNotNull(
+            MonoIconThemeController().createThemedBitmap(icon, BitmapInfo.LOW_RES_INFO, iconFactory)
+        )
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_FORCE_MONOCHROME_APP_ICONS)
+    fun `createThemedBitmap when mono generation is disabled`() {
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, null)
+        assertNull(
+            MonoIconThemeController().createThemedBitmap(icon, BitmapInfo.LOW_RES_INFO, iconFactory)
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FORCE_MONOCHROME_APP_ICONS)
+    fun `createThemedBitmap when mono generation is enabled`() {
+        ensureBitmapSerializationSupported()
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, null)
+        assertNotNull(
+            MonoIconThemeController().createThemedBitmap(icon, BitmapInfo.LOW_RES_INFO, iconFactory)
+        )
+    }
+
+    @Test
+    fun `decode bitmap after serialization valid data`() {
+        ensureBitmapSerializationSupported()
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, ColorDrawable(Color.RED))
+        val iconInfo = iconFactory.createBadgedIconBitmap(icon)
+
+        val themeBitmap =
+            MonoIconThemeController().createThemedBitmap(icon, iconInfo, iconFactory)!!
+        assertNotNull(
+            MonoIconThemeController().decode(themeBitmap.serialize(), iconInfo, iconFactory)
+        )
+    }
+
+    @Test
+    fun `decode bitmap after serialization invalid data`() {
+        ensureBitmapSerializationSupported()
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, ColorDrawable(Color.RED))
+        val iconInfo = iconFactory.createBadgedIconBitmap(icon)
+        assertNull(MonoIconThemeController().decode(byteArrayOf(1, 1, 1, 1), iconInfo, iconFactory))
+    }
+
+    @Test
+    fun `createThemedAdaptiveIcon with monochrome drawable`() {
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, ColorDrawable(Color.RED))
+        assertNotNull(MonoIconThemeController().createThemedAdaptiveIcon(context, icon, null))
+    }
+
+    @Test
+    fun `createThemedAdaptiveIcon with bitmap info`() {
+        val icon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, ColorDrawable(Color.RED))
+        val iconInfo = iconFactory.createBadgedIconBitmap(icon)
+        iconInfo.themedBitmap =
+            MonoIconThemeController().createThemedBitmap(icon, iconInfo, iconFactory)
+
+        val nonMonoIcon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, null)
+        assertNotNull(
+            MonoIconThemeController().createThemedAdaptiveIcon(context, nonMonoIcon, iconInfo)
+        )
+    }
+
+    @Test
+    fun `createThemedAdaptiveIcon invalid bitmap info`() {
+        val nonMonoIcon = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), null, null)
+        assertNull(
+            MonoIconThemeController()
+                .createThemedAdaptiveIcon(context, nonMonoIcon, BitmapInfo.LOW_RES_INFO)
+        )
+    }
+
+    companion object {
+
+        fun ensureBitmapSerializationSupported() {
+            // Robolectric doesn't support serializing 8-bit bitmaps
+            assumeFalse(isRunningInRobolectric)
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoThemedBitmapTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoThemedBitmapTest.kt
new file mode 100644
index 0000000..32de9e9
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/mono/MonoThemedBitmapTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.icons.mono
+
+import android.graphics.Bitmap
+import android.platform.uiautomatorhelpers.DeviceHelpers.context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.mono.MonoIconThemeControllerTest.Companion.ensureBitmapSerializationSupported
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MonoThemedBitmapTest {
+
+    @Test
+    fun `newDrawable returns valid drawable`() {
+        val bitmap =
+            MonoThemedBitmap(
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8),
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
+            )
+        val d = bitmap.newDrawable(BitmapInfo.LOW_RES_INFO, context)
+        assertTrue(d is ThemedIconDrawable)
+    }
+
+    @Test
+    fun `serialize returns valid bytes`() {
+        ensureBitmapSerializationSupported()
+        val bitmap =
+            MonoThemedBitmap(
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8),
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
+            )
+        assertTrue(bitmap.serialize().isNotEmpty())
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index c623513..c852729 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -37,7 +37,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsRecyclerView;
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.icons.ThemedIconDrawable;
+import com.android.launcher3.icons.mono.ThemedIconDrawable;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.launcher3.util.Executors;