Adding ThemeManager as a centralized place for controlling icon theming

Bug: 381897614
Flag: EXEMPT refactor
Test: atest ThemeManagerTest

Change-Id: Ib1dafdcc303f05f78cf586741c3d35243ab06e69
This commit is contained in:
Sunny Goyal
2024-12-31 00:00:15 -08:00
parent 239745aae9
commit c369d1e4af
19 changed files with 325 additions and 145 deletions
@@ -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<RecentsModel> 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() {
@@ -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;
@@ -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)
}
@@ -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);
@@ -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;
}
/**
@@ -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));
}
}
}
}
@@ -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
+11 -11
View File
@@ -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 <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 @@ 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;
}
}
}
@@ -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
}
@@ -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 {
@@ -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) {
@@ -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;
}
@@ -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] */
@@ -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")
}
}
@@ -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<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 @@ 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
@@ -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<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;
}
@@ -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});
@@ -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])
@@ -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
}
}