From c369d1e4afac48ccdd57ea37df02ebeb0dd60b33 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Tue, 31 Dec 2024 00:00:15 -0800 Subject: [PATCH] Adding ThemeManager as a centralized place for controlling icon theming Bug: 381897614 Flag: EXEMPT refactor Test: atest ThemeManagerTest Change-Id: Ib1dafdcc303f05f78cf586741c3d35243ab06e69 --- .../com/android/quickstep/RecentsModel.java | 29 +++-- .../logging/SettingsChangeLogger.java | 4 +- .../logging/SettingsChangeLoggerTest.kt | 11 +- .../android/quickstep/RecentsModelTest.java | 4 +- src/com/android/launcher3/BubbleTextView.java | 4 +- .../android/launcher3/LauncherAppState.java | 61 ++------- src/com/android/launcher3/LauncherPrefs.kt | 5 - src/com/android/launcher3/Utilities.java | 22 ++-- .../launcher3/apppairs/AppPairIconGraphic.kt | 20 +-- .../dagger/LauncherBaseAppComponent.java | 2 + .../launcher3/folder/PreviewItemManager.java | 4 +- .../graphics/GridCustomizationsProvider.java | 10 +- .../android/launcher3/graphics/IconShape.kt | 14 +- .../launcher3/graphics/ThemeManager.kt | 122 ++++++++++++++++++ .../launcher3/icons/LauncherIconProvider.java | 11 +- .../launcher3/icons/LauncherIcons.java | 7 +- src/com/android/launcher3/util/Themes.java | 12 -- .../folder/PreviewItemManagerTest.kt | 24 ++-- .../launcher3/graphics/ThemeManagerTest.kt | 104 +++++++++++++++ 19 files changed, 325 insertions(+), 145 deletions(-) create mode 100644 src/com/android/launcher3/graphics/ThemeManager.kt create mode 100644 tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index d073580fb5..1977dfa8e2 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -37,8 +37,9 @@ import android.os.UserHandle; 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 @@ import java.util.function.Predicate; * 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 INSTANCE = @@ -85,8 +86,10 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, 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 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, 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 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, 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 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, } } - @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 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, } @Override - public void onSystemIconStateChanged(String iconState) { + public void onThemeChanged() { mIconCache.clearCache(); } @@ -394,6 +401,8 @@ public class RecentsModel implements RecentTasksDataSource, IconChangeListener, } 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 dd721e16f6..946ca2a5a3 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.DeviceGridState.KEY_WORKSPACE_SIZE; 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 7c48ea489c..cf59f44d74 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.core.app.ApplicationProvider 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 @@ class SettingsChangeLoggerTest { 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 @@ class SettingsChangeLoggerTest { @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 ef4591e679..3072d0233d 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java +++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java @@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest; 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 @@ public class RecentsModelTest { 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 da73280b2e..9aa06bf4b0 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -464,8 +464,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } 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 5989e4c6a2..e560a14ab5 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -20,12 +20,6 @@ import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURC 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; 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.RunnableList; 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 @@ public class LauncherAppState implements SafeCloseable { } }); + 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 @@ public class LauncherAppState implements SafeCloseable { 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 class LauncherAppState implements SafeCloseable { 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 ad592d8dea..d8bb84e6c0 100644 --- a/src/com/android/launcher3/LauncherPrefs.kt +++ b/src/com/android/launcher3/LauncherPrefs.kt @@ -34,7 +34,6 @@ import com.android.launcher3.settings.SettingsActivity 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 @@ constructor(@ApplicationContext private val encryptedContext: Context) { 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 9060691d9e..e44caa42c6 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -74,9 +74,11 @@ import androidx.annotation.WorkerThread; 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.testing.shared.ResourceUtils; 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 @@ public final class Utilities { @WorkerThread public static Pair 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 @@ public final class Utilities { // 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 034b686828..81a92f689b 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 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 @@ constructor(context: Context, attrs: AttributeSet? = null) : @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 @@ constructor(context: Context, attrs: AttributeSet? = null) : ((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 @@ constructor(context: Context, attrs: AttributeSet? = null) : 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 72a97a841d..4b43d490e9 100644 --- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -21,6 +21,7 @@ import android.content.Context; 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 @@ public interface LauncherBaseAppComponent { 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 5ee6a252bd..4cf618d73d 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.AppPairInfo; 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 @@ public class PreviewItemManager { 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 eaca6c5e31..5461485936 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; 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 @@ public class GridCustomizationsProvider extends ContentProvider { 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 @@ public class GridCustomizationsProvider extends ContentProvider { } 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 22d3f3d950..c64d4da56d 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.anim.RoundedRectRevealOutlineProvider import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.dagger.LauncherAppComponent import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener import com.android.launcher3.icons.GraphicsUtils import com.android.launcher3.icons.IconNormalizer import com.android.launcher3.util.DaggerSingletonObject +import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.views.ClipPathView import java.io.IOException import javax.inject.Inject @@ -47,7 +49,13 @@ import org.xmlpull.v1.XmlPullParserException /** 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 @@ class IconShape @Inject constructor(@ApplicationContext context: Context) { 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 0000000000..991edf708b --- /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() + + 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 78a31285f9..e40f52638f 100644 --- a/src/com/android/launcher3/icons/LauncherIconProvider.java +++ b/src/com/android/launcher3/icons/LauncherIconProvider.java @@ -27,8 +27,8 @@ import androidx.annotation.NonNull; 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 @@ public class LauncherIconProvider extends IconProvider { private static final Map DISABLED_MAP = Collections.emptyMap(); private Map 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 @@ public class LauncherIconProvider extends IconProvider { } @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 839dfb70ea..04d88b003f 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -23,11 +23,10 @@ import androidx.annotation.NonNull; 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 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { ConcurrentLinkedQueue 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 104040afff..927a2a4e0d 100644 --- a/src/com/android/launcher3/util/Themes.java +++ b/src/com/android/launcher3/util/Themes.java @@ -19,8 +19,6 @@ package com.android.launcher3.util; 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 android.util.TypedValue; 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 @@ import com.android.launcher3.views.ActivityContext; @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 @@ public class Themes { } } - /** - * 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 548cf5bf54..553d08c3bb 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 android.os.Process 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 @@ class PreviewItemManagerTest { private var defaultThemedIcons = false + private val themeManager: ThemeManager + get() = ThemeManager.INSTANCE.get(context) + @Before fun setup() { modelHelper = LauncherModelHelper() @@ -126,19 +128,19 @@ class PreviewItemManagerTest { 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 @@ class PreviewItemManagerTest { @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 @@ class PreviewItemManagerTest { @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 @@ class PreviewItemManagerTest { @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 @@ class PreviewItemManagerTest { @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 @@ class PreviewItemManagerTest { @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 @@ class PreviewItemManagerTest { @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 0000000000..43bbad9e68 --- /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 + } +}