From 0ad926a18494cb8b99ac0325ba0b15c1869a4581 Mon Sep 17 00:00:00 2001 From: Lucas Dupin Date: Tue, 10 Nov 2020 03:36:32 +0000 Subject: [PATCH] Let wallpaper colors drive system theme Refarctored ThemeOverlayManager and ThemeOverlayController to conform to current SysUI paradigm (dependency injection) and registered WallpaperManager listener so themes can be driven by system colors. Test: manual Test: atest ThemeOverlayControllerTest Test: atest ThemeOverlayApplierTest Bug: 171916625 Change-Id: Icbc1e0f6bcda160b4ba9dd39fe29938bce538689 --- .../systemui/dagger/DependencyProvider.java | 14 + .../dagger/FrameworkServicesModule.java | 9 +- ...yManager.java => ThemeOverlayApplier.java} | 112 +++++++- .../theme/ThemeOverlayController.java | 240 ++++++++++++++++-- ...Test.java => ThemeOverlayApplierTest.java} | 53 ++-- .../theme/ThemeOverlayControllerTest.java | 173 +++++++++++++ 6 files changed, 545 insertions(+), 56 deletions(-) rename packages/SystemUI/src/com/android/systemui/theme/{ThemeOverlayManager.java => ThemeOverlayApplier.java} (64%) rename packages/SystemUI/tests/src/com/android/systemui/theme/{ThemeOverlayManagerTest.java => ThemeOverlayApplierTest.java} (85%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index 3aa462657637b..ea7dc9a860464 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -23,6 +23,7 @@ import android.annotation.SuppressLint; import android.app.INotificationManager; import android.content.Context; import android.content.SharedPreferences; +import android.content.om.OverlayManager; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Handler; import android.os.HandlerThread; @@ -43,6 +44,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.ViewMediatorCallback; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.Prefs; +import com.android.systemui.R; import com.android.systemui.accessibility.ModeSwitchesController; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; @@ -78,6 +80,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.NetworkController; +import com.android.systemui.theme.ThemeOverlayApplier; import com.android.systemui.util.leak.LeakDetector; import com.android.wm.shell.splitscreen.SplitScreen; @@ -192,6 +195,17 @@ public class DependencyProvider { return new PluginManagerImpl(context, new PluginInitializerImpl()); } + /** */ + @SysUISingleton + @Provides + static ThemeOverlayApplier provideThemeOverlayManager(Context context, + @Background Executor bgExecutor, OverlayManager overlayManager, + DumpManager dumpManager) { + return new ThemeOverlayApplier(overlayManager, bgExecutor, + context.getString(R.string.launcher_overlayable_package), + context.getString(R.string.themepicker_overlayable_package), dumpManager); + } + /** */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 79925bad3cc78..16859fe86d6eb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -31,6 +31,7 @@ import android.app.role.RoleManager; import android.app.trust.TrustManager; import android.content.ContentResolver; import android.content.Context; +import android.content.om.OverlayManager; import android.content.pm.IPackageManager; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; @@ -343,7 +344,7 @@ public class FrameworkServicesModule { @Provides static WallpaperManager provideWallpaperManager(Context context) { - return (WallpaperManager) context.getSystemService(Context.WALLPAPER_SERVICE); + return context.getSystemService(WallpaperManager.class); } @Provides @@ -353,6 +354,12 @@ public class FrameworkServicesModule { return context.getSystemService(WifiManager.class); } + @Provides + @Singleton + static OverlayManager provideOverlayManager(Context context) { + return context.getSystemService(OverlayManager.class); + } + @Provides @Singleton static WindowManager provideWindowManager(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java similarity index 64% rename from packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java rename to packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index 665cb6307b6ae..0aa2a739eb82e 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -21,11 +21,18 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.systemui.Dumpable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dump.DumpManager; + import com.google.android.collect.Lists; import com.google.android.collect.Sets; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -34,9 +41,19 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.stream.Collectors; -class ThemeOverlayManager { +/** + * Responsible for orchestrating overlays, based on user preferences and other inputs from + * {@link ThemeOverlayController}. + */ +@SysUISingleton +public class ThemeOverlayApplier implements Dumpable { private static final String TAG = "ThemeOverlayManager"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + @VisibleForTesting + static final String MONET_ACCENT_COLOR_PACKAGE = "com.android.theme.accentcolor.color"; + @VisibleForTesting + static final String MONET_SYSTEM_PALETTE_PACKAGE = "com.android.theme.systemcolors.color"; @VisibleForTesting static final String ANDROID_PACKAGE = "android"; @@ -46,7 +63,11 @@ class ThemeOverlayManager { static final String SYSUI_PACKAGE = "com.android.systemui"; @VisibleForTesting - static final String OVERLAY_CATEGORY_COLOR = "android.theme.customization.accent_color"; + static final String OVERLAY_CATEGORY_ACCENT_COLOR = + "android.theme.customization.accent_color"; + @VisibleForTesting + static final String OVERLAY_CATEGORY_SYSTEM_PALETTE = + "android.theme.customization.system_palette"; @VisibleForTesting static final String OVERLAY_CATEGORY_FONT = "android.theme.customization.font"; @VisibleForTesting @@ -73,24 +94,36 @@ class ThemeOverlayManager { * starts with launcher and grouped by target package. */ static final List THEME_CATEGORIES = Lists.newArrayList( + OVERLAY_CATEGORY_SYSTEM_PALETTE, OVERLAY_CATEGORY_ICON_LAUNCHER, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_FONT, - OVERLAY_CATEGORY_COLOR, + OVERLAY_CATEGORY_ACCENT_COLOR, OVERLAY_CATEGORY_ICON_ANDROID, OVERLAY_CATEGORY_ICON_SYSUI, OVERLAY_CATEGORY_ICON_SETTINGS, OVERLAY_CATEGORY_ICON_THEME_PICKER); - /* Categories that need to applied to the current user as well as the system user. */ + /* Categories that need to be applied to the current user as well as the system user. */ @VisibleForTesting static final Set SYSTEM_USER_CATEGORIES = Sets.newHashSet( - OVERLAY_CATEGORY_COLOR, + OVERLAY_CATEGORY_SYSTEM_PALETTE, + OVERLAY_CATEGORY_ACCENT_COLOR, OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_ICON_ANDROID, OVERLAY_CATEGORY_ICON_SYSUI); + /** + * List of main colors of Monet themes. These are extracted from overlays installed + * on the system. + */ + private final ArrayList mMainSystemColors = new ArrayList<>(); + /** + * Same as above, but providing accent colors instead of a system palette. + */ + private final ArrayList mAccentColors = new ArrayList<>(); + /* Allowed overlay categories for each target package. */ private final Map> mTargetPackageToCategories = new ArrayMap<>(); /* Target package for each overlay category. */ @@ -100,15 +133,15 @@ class ThemeOverlayManager { private final String mLauncherPackage; private final String mThemePickerPackage; - ThemeOverlayManager(OverlayManager overlayManager, Executor executor, - String launcherPackage, String themePickerPackage) { + public ThemeOverlayApplier(OverlayManager overlayManager, Executor executor, + String launcherPackage, String themePickerPackage, DumpManager dumpManager) { mOverlayManager = overlayManager; mExecutor = executor; mLauncherPackage = launcherPackage; mThemePickerPackage = themePickerPackage; mTargetPackageToCategories.put(ANDROID_PACKAGE, Sets.newHashSet( - OVERLAY_CATEGORY_COLOR, OVERLAY_CATEGORY_FONT, - OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_ICON_ANDROID)); + OVERLAY_CATEGORY_SYSTEM_PALETTE, OVERLAY_CATEGORY_ACCENT_COLOR, + OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_ICON_ANDROID)); mTargetPackageToCategories.put(SYSUI_PACKAGE, Sets.newHashSet(OVERLAY_CATEGORY_ICON_SYSUI)); mTargetPackageToCategories.put(SETTINGS_PACKAGE, @@ -117,7 +150,7 @@ class ThemeOverlayManager { Sets.newHashSet(OVERLAY_CATEGORY_ICON_LAUNCHER)); mTargetPackageToCategories.put(mThemePickerPackage, Sets.newHashSet(OVERLAY_CATEGORY_ICON_THEME_PICKER)); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_COLOR, ANDROID_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ACCENT_COLOR, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_FONT, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_SHAPE, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_ANDROID, ANDROID_PACKAGE); @@ -125,6 +158,54 @@ class ThemeOverlayManager { mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SETTINGS, SETTINGS_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_LAUNCHER, mLauncherPackage); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_THEME_PICKER, mThemePickerPackage); + + collectMonetSystemOverlays(); + dumpManager.registerDumpable(TAG, this); + } + + /** + * List of accent colors available as Monet overlays. + */ + List getAvailableAccentColors() { + return mAccentColors; + } + + /** + * List of main system colors available as Monet overlays. + */ + List getAvailableSystemColors() { + return mMainSystemColors; + } + + private void collectMonetSystemOverlays() { + List androidOverlays = mOverlayManager + .getOverlayInfosForTarget(ANDROID_PACKAGE, UserHandle.SYSTEM); + for (OverlayInfo overlayInfo : androidOverlays) { + String packageName = overlayInfo.packageName; + if (DEBUG) { + Log.d(TAG, "Processing overlay " + packageName); + } + if (OVERLAY_CATEGORY_SYSTEM_PALETTE.equals(overlayInfo.category) + && packageName.startsWith(MONET_SYSTEM_PALETTE_PACKAGE)) { + try { + String color = packageName.replace(MONET_SYSTEM_PALETTE_PACKAGE, ""); + mMainSystemColors.add(Integer.parseInt(color, 16)); + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid package name for overlay " + packageName, e); + } + } else if (OVERLAY_CATEGORY_ACCENT_COLOR.equals(overlayInfo.category) + && packageName.startsWith(MONET_ACCENT_COLOR_PACKAGE)) { + try { + String color = packageName.replace(MONET_ACCENT_COLOR_PACKAGE, ""); + mAccentColors.add(Integer.parseInt(color, 16)); + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid package name for overlay " + packageName, e); + } + } else if (DEBUG) { + Log.d(TAG, "Unknown overlay: " + packageName + " category: " + + overlayInfo.category); + } + } } /** @@ -184,4 +265,13 @@ class ThemeOverlayManager { } }); } + + /** + * @inherit + */ + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("mMainSystemColors=" + mMainSystemColors.size()); + pw.println("mAccentColors=" + mAccentColors.size()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 132e092b8adad..c4e2b5d47ba8d 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -15,16 +15,20 @@ */ package com.android.systemui.theme; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ACCENT_COLOR; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SYSTEM_PALETTE; + import android.app.ActivityManager; +import android.app.WallpaperColors; +import android.app.WallpaperManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.om.OverlayManager; import android.content.pm.UserInfo; import android.database.ContentObserver; +import android.graphics.Color; import android.net.Uri; -import android.os.AsyncTask; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; @@ -33,20 +37,32 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; -import com.android.systemui.R; +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; +import com.android.systemui.Dumpable; import com.android.systemui.SystemUI; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.settings.SecureSettings; import com.google.android.collect.Sets; import org.json.JSONException; import org.json.JSONObject; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -60,49 +76,76 @@ import javax.inject.Inject; * associated work profiles */ @SysUISingleton -public class ThemeOverlayController extends SystemUI { +public class ThemeOverlayController extends SystemUI implements Dumpable { private static final String TAG = "ThemeOverlayController"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private ThemeOverlayManager mThemeManager; - private UserManager mUserManager; - private BroadcastDispatcher mBroadcastDispatcher; + // If lock screen wallpaper colors should also be considered when selecting the theme. + // Doing this has performance impact, given that overlays would need to be swapped when + // the device unlocks. + @VisibleForTesting + static final boolean USE_LOCK_SCREEN_WALLPAPER = false; + + private final ThemeOverlayApplier mThemeManager; + private final UserManager mUserManager; + private final BroadcastDispatcher mBroadcastDispatcher; + private final Executor mBgExecutor; + private final SecureSettings mSecureSettings; + private final Executor mMainExecutor; private final Handler mBgHandler; + private final WallpaperManager mWallpaperManager; + private final KeyguardStateController mKeyguardStateController; + private WallpaperColors mLockColors; + private WallpaperColors mSystemColors; + // Color extracted from wallpaper, NOT the color used on the overlay + private int mMainWallpaperColor = Color.TRANSPARENT; + // Color extracted from wallpaper, NOT the color used on the overlay + private int mWallpaperAccentColor = Color.TRANSPARENT; + // Main system color that maps to an overlay color + private int mSystemOverlayColor = Color.TRANSPARENT; + // Accent color that maps to an overlay color + private int mAccentOverlayColor = Color.TRANSPARENT; @Inject public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher, - @Background Handler bgHandler) { + @Background Handler bgHandler, @Main Executor mainExecutor, + @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier, + SecureSettings secureSettings, WallpaperManager wallpaperManager, + UserManager userManager, KeyguardStateController keyguardStateController, + DumpManager dumpManager) { super(context); + mBroadcastDispatcher = broadcastDispatcher; + mUserManager = userManager; + mBgExecutor = bgExecutor; + mMainExecutor = mainExecutor; mBgHandler = bgHandler; + mThemeManager = themeOverlayApplier; + mSecureSettings = secureSettings; + mWallpaperManager = wallpaperManager; + mKeyguardStateController = keyguardStateController; + dumpManager.registerDumpable(TAG, this); } @Override public void start() { if (DEBUG) Log.d(TAG, "Start"); - mUserManager = mContext.getSystemService(UserManager.class); - mThemeManager = new ThemeOverlayManager( - mContext.getSystemService(OverlayManager.class), - AsyncTask.THREAD_POOL_EXECUTOR, - mContext.getString(R.string.launcher_overlayable_package), - mContext.getString(R.string.themepicker_overlayable_package)); final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); - mBroadcastDispatcher.registerReceiverWithHandler(new BroadcastReceiver() { + mBroadcastDispatcher.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "Updating overlays for user switch / profile added."); updateThemeOverlays(); } - }, filter, mBgHandler, UserHandle.ALL); - mContext.getContentResolver().registerContentObserver( + }, filter, mBgExecutor, UserHandle.ALL); + mSecureSettings.registerContentObserverForUser( Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), false, new ContentObserver(mBgHandler) { - @Override - public void onChange(boolean selfChange, Collection uris, int flags, + public void onChange(boolean selfChange, Collection collection, int flags, int userId) { if (DEBUG) Log.d(TAG, "Overlay changed for user: " + userId); if (ActivityManager.getCurrentUser() == userId) { @@ -111,20 +154,147 @@ public class ThemeOverlayController extends SystemUI { } }, UserHandle.USER_ALL); + + // Upon boot, make sure we have the most up to date colors + mBgExecutor.execute(() -> { + WallpaperColors lockColors = mWallpaperManager.getWallpaperColors( + WallpaperManager.FLAG_LOCK); + WallpaperColors systemColor = mWallpaperManager.getWallpaperColors( + WallpaperManager.FLAG_SYSTEM); + mMainExecutor.execute(() -> { + if (USE_LOCK_SCREEN_WALLPAPER) { + mLockColors = lockColors; + } + mSystemColors = systemColor; + reevaluateSystemTheme(); + }); + }); + if (USE_LOCK_SCREEN_WALLPAPER) { + mKeyguardStateController.addCallback(new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + if (mLockColors == null) { + return; + } + // It's possible that the user has a lock screen wallpaper. On this case we'll + // end up with different colors after unlocking. + reevaluateSystemTheme(); + } + }); + } + mWallpaperManager.addOnColorsChangedListener((wallpaperColors, which) -> { + if (USE_LOCK_SCREEN_WALLPAPER && (which & WallpaperManager.FLAG_LOCK) != 0) { + mLockColors = wallpaperColors; + if (DEBUG) { + Log.d(TAG, "got new lock colors: " + wallpaperColors + " where: " + which); + } + } + if ((which & WallpaperManager.FLAG_SYSTEM) != 0) { + mSystemColors = wallpaperColors; + if (DEBUG) { + Log.d(TAG, "got new lock colors: " + wallpaperColors + " where: " + which); + } + } + reevaluateSystemTheme(); + }, null, UserHandle.USER_ALL); + } + + private void reevaluateSystemTheme() { + if (mLockColors == null && mSystemColors == null) { + Log.w(TAG, "Cannot update theme, colors are null"); + return; + } + WallpaperColors currentColor = + mKeyguardStateController.isShowing() && mLockColors != null + ? mLockColors : mSystemColors; + int mainColor = currentColor.getPrimaryColor().toArgb(); + + //TODO(b/172860591) implement more complex logic for picking accent color. + //For now, picking the secondary should be enough. + Color accentCandidate = currentColor.getSecondaryColor(); + if (accentCandidate == null) { + accentCandidate = currentColor.getTertiaryColor(); + } + if (accentCandidate == null) { + accentCandidate = currentColor.getPrimaryColor(); + } + + if (mMainWallpaperColor == mainColor && mWallpaperAccentColor == accentCandidate.toArgb()) { + return; + } + + mMainWallpaperColor = mainColor; + mWallpaperAccentColor = accentCandidate.toArgb(); + + // Let's compare these colors to our finite set of overlays, and then pick an overlay. + List systemColors = mThemeManager.getAvailableSystemColors(); + List accentColors = mThemeManager.getAvailableAccentColors(); + + if (systemColors.size() == 0 || accentColors.size() == 0) { + if (DEBUG) { + Log.d(TAG, "Cannot apply system theme, palettes are unavailable"); + } + return; + } + + mSystemOverlayColor = getClosest(systemColors, mMainWallpaperColor); + mAccentOverlayColor = getClosest(accentColors, mWallpaperAccentColor); + + updateThemeOverlays(); + } + + /** + * Given a color and a list of candidates, return the candidate that's the most similar to the + * given color. + */ + private static int getClosest(List candidates, int color) { + float[] hslMain = new float[3]; + float[] hslCandidate = new float[3]; + + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hslMain); + hslMain[0] /= 360f; + + float minDistance = Float.MAX_VALUE; + int closestColor = Color.TRANSPARENT; + for (int candidate: candidates) { + ColorUtils.RGBToHSL(Color.red(candidate), Color.green(candidate), Color.blue(candidate), + hslCandidate); + hslCandidate[0] /= 360f; + + float sqDistance = squared(hslCandidate[0] - hslMain[0]) + + squared(hslCandidate[1] - hslMain[1]) + + squared(hslCandidate[2] - hslMain[2]); + if (sqDistance < minDistance) { + minDistance = sqDistance; + closestColor = candidate; + } + } + return closestColor; + } + + private static float squared(float f) { + return f * f; } private void updateThemeOverlays() { final int currentUser = ActivityManager.getCurrentUser(); - final String overlayPackageJson = Settings.Secure.getStringForUser( - mContext.getContentResolver(), Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + final String overlayPackageJson = mSecureSettings.getStringForUser( + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, currentUser); if (DEBUG) Log.d(TAG, "updateThemeOverlays: " + overlayPackageJson); + boolean hasSystemPalette = false; + boolean hasAccentColor = false; final Map categoryToPackage = new ArrayMap<>(); if (!TextUtils.isEmpty(overlayPackageJson)) { try { JSONObject object = new JSONObject(overlayPackageJson); - for (String category : ThemeOverlayManager.THEME_CATEGORIES) { + for (String category : ThemeOverlayApplier.THEME_CATEGORIES) { if (object.has(category)) { + if (category.equals(OVERLAY_CATEGORY_ACCENT_COLOR)) { + hasAccentColor = true; + } else if (category.equals(OVERLAY_CATEGORY_SYSTEM_PALETTE)) { + hasSystemPalette = true; + } categoryToPackage.put(category, object.getString(category)); } } @@ -132,6 +302,20 @@ public class ThemeOverlayController extends SystemUI { Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e); } } + + // Let's apply the system palette, but only if it was not overridden by the style picker. + if (!hasSystemPalette && mSystemOverlayColor != Color.TRANSPARENT) { + categoryToPackage.put(OVERLAY_CATEGORY_SYSTEM_PALETTE, + ThemeOverlayApplier.MONET_SYSTEM_PALETTE_PACKAGE + + Integer.toHexString(mSystemOverlayColor).toUpperCase()); + } + // Same for the accent color + if (!hasAccentColor && mAccentOverlayColor != Color.TRANSPARENT) { + categoryToPackage.put(OVERLAY_CATEGORY_ACCENT_COLOR, + ThemeOverlayApplier.MONET_ACCENT_COLOR_PACKAGE + + Integer.toHexString(mAccentOverlayColor).toUpperCase()); + } + Set userHandles = Sets.newHashSet(UserHandle.of(currentUser)); for (UserInfo userInfo : mUserManager.getEnabledProfiles(currentUser)) { if (userInfo.isManagedProfile()) { @@ -140,4 +324,14 @@ public class ThemeOverlayController extends SystemUI { } mThemeManager.applyCurrentUserOverlays(categoryToPackage, userHandles); } + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("mLockColors=" + mLockColors); + pw.println("mSystemColors=" + mSystemColors); + pw.println("mMainWallpaperColor=" + Integer.toHexString(mMainWallpaperColor)); + pw.println("mWallpaperAccentColor=" + Integer.toHexString(mWallpaperAccentColor)); + pw.println("mSystemOverlayColor=" + Integer.toHexString(mSystemOverlayColor)); + pw.println("mAccentOverlayColor=" + Integer.toHexString(mAccentOverlayColor)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java similarity index 85% rename from packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayManagerTest.java rename to packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java index c99deb606459c..edaff5ff0f101 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayApplierTest.java @@ -15,22 +15,24 @@ */ package com.android.systemui.theme; -import static com.android.systemui.theme.ThemeOverlayManager.ANDROID_PACKAGE; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_COLOR; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_FONT; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_ICON_ANDROID; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_ICON_LAUNCHER; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_ICON_SETTINGS; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_ICON_SYSUI; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_ICON_THEME_PICKER; -import static com.android.systemui.theme.ThemeOverlayManager.OVERLAY_CATEGORY_SHAPE; -import static com.android.systemui.theme.ThemeOverlayManager.SETTINGS_PACKAGE; -import static com.android.systemui.theme.ThemeOverlayManager.SYSTEM_USER_CATEGORIES; -import static com.android.systemui.theme.ThemeOverlayManager.SYSUI_PACKAGE; -import static com.android.systemui.theme.ThemeOverlayManager.THEME_CATEGORIES; +import static com.android.systemui.theme.ThemeOverlayApplier.ANDROID_PACKAGE; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ACCENT_COLOR; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_FONT; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ICON_ANDROID; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ICON_LAUNCHER; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ICON_SETTINGS; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ICON_SYSUI; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ICON_THEME_PICKER; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SHAPE; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SYSTEM_PALETTE; +import static com.android.systemui.theme.ThemeOverlayApplier.SETTINGS_PACKAGE; +import static com.android.systemui.theme.ThemeOverlayApplier.SYSTEM_USER_CATEGORIES; +import static com.android.systemui.theme.ThemeOverlayApplier.SYSUI_PACKAGE; +import static com.android.systemui.theme.ThemeOverlayApplier.THEME_CATEGORIES; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,6 +46,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.google.android.collect.Maps; import com.google.common.collect.Lists; @@ -63,7 +66,7 @@ import java.util.Set; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class ThemeOverlayManagerTest extends SysuiTestCase { +public class ThemeOverlayApplierTest extends SysuiTestCase { private static final String TEST_DISABLED_PREFIX = "com.example."; private static final String TEST_ENABLED_PREFIX = "com.example.enabled."; @@ -82,26 +85,32 @@ public class ThemeOverlayManagerTest extends SysuiTestCase { @Mock OverlayManager mOverlayManager; + @Mock + DumpManager mDumpManager; - private ThemeOverlayManager mManager; + private ThemeOverlayApplier mManager; @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); - mManager = new ThemeOverlayManager(mOverlayManager, MoreExecutors.directExecutor(), - LAUNCHER_PACKAGE, THEMEPICKER_PACKAGE); + mManager = new ThemeOverlayApplier(mOverlayManager, MoreExecutors.directExecutor(), + LAUNCHER_PACKAGE, THEMEPICKER_PACKAGE, mDumpManager); when(mOverlayManager.getOverlayInfosForTarget(ANDROID_PACKAGE, UserHandle.SYSTEM)) .thenReturn(Lists.newArrayList( - createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_COLOR, - ANDROID_PACKAGE, OVERLAY_CATEGORY_COLOR, false), + createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_ACCENT_COLOR, + ANDROID_PACKAGE, OVERLAY_CATEGORY_ACCENT_COLOR, false), + createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_SYSTEM_PALETTE, + ANDROID_PACKAGE, OVERLAY_CATEGORY_SYSTEM_PALETTE, false), createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_FONT, ANDROID_PACKAGE, OVERLAY_CATEGORY_FONT, false), createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_SHAPE, ANDROID_PACKAGE, OVERLAY_CATEGORY_SHAPE, false), createOverlayInfo(TEST_DISABLED_PREFIX + OVERLAY_CATEGORY_ICON_ANDROID, ANDROID_PACKAGE, OVERLAY_CATEGORY_ICON_ANDROID, false), - createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_COLOR, - ANDROID_PACKAGE, OVERLAY_CATEGORY_COLOR, true), + createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_ACCENT_COLOR, + ANDROID_PACKAGE, OVERLAY_CATEGORY_ACCENT_COLOR, true), + createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_SYSTEM_PALETTE, + ANDROID_PACKAGE, OVERLAY_CATEGORY_SYSTEM_PALETTE, true), createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_FONT, ANDROID_PACKAGE, OVERLAY_CATEGORY_FONT, true), createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_SHAPE, @@ -132,6 +141,8 @@ public class ThemeOverlayManagerTest extends SysuiTestCase { THEMEPICKER_PACKAGE, OVERLAY_CATEGORY_ICON_THEME_PICKER, false), createOverlayInfo(TEST_ENABLED_PREFIX + OVERLAY_CATEGORY_ICON_THEME_PICKER, THEMEPICKER_PACKAGE, OVERLAY_CATEGORY_ICON_THEME_PICKER, true))); + clearInvocations(mOverlayManager); + verify(mDumpManager).registerDumpable(any(), any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java new file mode 100644 index 0000000000000..aee884022d95f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 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.systemui.theme; + +import static com.android.systemui.theme.ThemeOverlayApplier.MONET_ACCENT_COLOR_PACKAGE; +import static com.android.systemui.theme.ThemeOverlayApplier.MONET_SYSTEM_PALETTE_PACKAGE; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ACCENT_COLOR; +import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SYSTEM_PALETTE; +import static com.android.systemui.theme.ThemeOverlayController.USE_LOCK_SCREEN_WALLPAPER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.graphics.Color; +import android.os.Handler; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.settings.SecureSettings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ThemeOverlayControllerTest extends SysuiTestCase { + + private ThemeOverlayController mThemeOverlayController; + @Mock + private Executor mBgExecutor; + @Mock + private Executor mMainExecutor; + @Mock + private BroadcastDispatcher mBroadcastDispatcher; + @Mock + private Handler mBgHandler; + @Mock + private ThemeOverlayApplier mThemeOverlayApplier; + @Mock + private SecureSettings mSecureSettings; + @Mock + private WallpaperManager mWallpaperManager; + @Mock + private UserManager mUserManager; + @Mock + private KeyguardStateController mKeyguardStateController; + @Mock + private DumpManager mDumpManager; + @Captor + private ArgumentCaptor mKeyguardStateControllerCallback; + @Captor + private ArgumentCaptor mColorsListener; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mThemeOverlayController = new ThemeOverlayController(null /* context */, + mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier, + mSecureSettings, mWallpaperManager, mUserManager, mKeyguardStateController, + mDumpManager); + + mThemeOverlayController.start(); + if (USE_LOCK_SCREEN_WALLPAPER) { + verify(mKeyguardStateController).addCallback( + mKeyguardStateControllerCallback.capture()); + } + verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null), + eq(UserHandle.USER_ALL)); + verify(mDumpManager).registerDumpable(any(), any()); + + List colorList = List.of(Color.RED, Color.BLUE); + when(mThemeOverlayApplier.getAvailableAccentColors()).thenReturn(colorList); + when(mThemeOverlayApplier.getAvailableSystemColors()).thenReturn(colorList); + } + + @Test + public void start_checksWallpaper() { + ArgumentCaptor registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mBgExecutor).execute(registrationRunnable.capture()); + + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_LOCK)); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + } + + @Test + public void onWallpaperColorsChanged_setsTheme() { + // Should ask for a new theme when wallpaper colors change + WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), + Color.valueOf(Color.BLUE), null); + mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM); + ArgumentCaptor> themeOverlays = ArgumentCaptor.forClass(Map.class); + + verify(mThemeOverlayApplier).getAvailableSystemColors(); + verify(mThemeOverlayApplier).getAvailableAccentColors(); + verify(mThemeOverlayApplier).applyCurrentUserOverlays(themeOverlays.capture(), any()); + + // Assert that we received the colors that we were expecting + assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE)) + .isEqualTo(MONET_SYSTEM_PALETTE_PACKAGE + + Integer.toHexString(Color.RED).toUpperCase()); + assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_ACCENT_COLOR)) + .isEqualTo(MONET_ACCENT_COLOR_PACKAGE + + Integer.toHexString(Color.BLUE).toUpperCase()); + + // Should not ask again if changed to same value + mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM); + verifyNoMoreInteractions(mThemeOverlayApplier); + } + + @Test + public void onWallpaperColorsChanged_preservesWallpaperPickerTheme() { + // Should ask for a new theme when wallpaper colors change + WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), + Color.valueOf(Color.BLUE), null); + + String jsonString = + "{\"android.theme.customization.system_palette\":\"override.package.name\"}"; + when(mSecureSettings.getStringForUser( + eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) + .thenReturn(jsonString); + + mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM); + ArgumentCaptor> themeOverlays = ArgumentCaptor.forClass(Map.class); + + verify(mThemeOverlayApplier).getAvailableSystemColors(); + verify(mThemeOverlayApplier).getAvailableAccentColors(); + verify(mThemeOverlayApplier).applyCurrentUserOverlays(themeOverlays.capture(), any()); + + // Assert that we received the colors that we were expecting + assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE)) + .isEqualTo("override.package.name"); + } +}