Adding ThemeManager as a centralized place for controlling icon theming

Bug: 381897614
Flag: EXEMPT refactor
Test: atest ThemeManagerTest

Change-Id: Ib1dafdcc303f05f78cf586741c3d35243ab06e69
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/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/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/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/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/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/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/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/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.kt b/src/com/android/launcher3/graphics/IconShape.kt
index 22d3f3d..c64d4da 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/IconShape.kt
@@ -36,9 +36,11 @@
 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
@@ -47,7 +49,13 @@
 
 /** Abstract representation of the shape of an icon shape */
 @LauncherAppSingleton
-class IconShape @Inject constructor(@ApplicationContext context: Context) {
+class IconShape
+@Inject
+constructor(
+    @ApplicationContext context: Context,
+    themeManager: ThemeManager,
+    lifeCycle: DaggerSingletonTracker,
+) {
     var shape: ShapeDelegate = Circle()
         private set
 
@@ -56,6 +64,10 @@
 
     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] */
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/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
+    }
+}