1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.theme;
18 
19 import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
20 
21 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
22 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
23 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
24 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
25 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_ACCENT_COLOR;
26 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_DYNAMIC_COLOR;
27 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_CATEGORY_SYSTEM_PALETTE;
28 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_BOTH;
29 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_INDEX;
30 import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_SOURCE;
31 import static com.android.systemui.theme.ThemeOverlayApplier.TIMESTAMP_FIELD;
32 
33 import android.app.ActivityManager;
34 import android.app.UiModeManager;
35 import android.app.WallpaperColors;
36 import android.app.WallpaperManager;
37 import android.app.WallpaperManager.OnColorsChangedListener;
38 import android.content.BroadcastReceiver;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.IntentFilter;
42 import android.content.om.FabricatedOverlay;
43 import android.content.om.OverlayIdentifier;
44 import android.content.pm.UserInfo;
45 import android.content.res.Configuration;
46 import android.content.res.Resources;
47 import android.database.ContentObserver;
48 import android.graphics.Color;
49 import android.net.Uri;
50 import android.os.Handler;
51 import android.os.UserHandle;
52 import android.os.UserManager;
53 import android.provider.Settings;
54 import android.text.TextUtils;
55 import android.util.ArrayMap;
56 import android.util.ArraySet;
57 import android.util.Log;
58 import android.util.SparseArray;
59 import android.util.SparseIntArray;
60 
61 import androidx.annotation.NonNull;
62 import androidx.annotation.VisibleForTesting;
63 
64 import com.android.internal.graphics.ColorUtils;
65 import com.android.systemui.CoreStartable;
66 import com.android.systemui.Dumpable;
67 import com.android.systemui.broadcast.BroadcastDispatcher;
68 import com.android.systemui.dagger.SysUISingleton;
69 import com.android.systemui.dagger.qualifiers.Background;
70 import com.android.systemui.dagger.qualifiers.Main;
71 import com.android.systemui.dump.DumpManager;
72 import com.android.systemui.flags.FeatureFlags;
73 import com.android.systemui.flags.Flags;
74 import com.android.systemui.keyguard.WakefulnessLifecycle;
75 import com.android.systemui.monet.ColorScheme;
76 import com.android.systemui.monet.Style;
77 import com.android.systemui.monet.TonalPalette;
78 import com.android.systemui.settings.UserTracker;
79 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
80 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
81 import com.android.systemui.util.settings.SecureSettings;
82 
83 import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors;
84 import com.google.ux.material.libmonet.hct.Hct;
85 import com.google.ux.material.libmonet.scheme.DynamicScheme;
86 import com.google.ux.material.libmonet.scheme.SchemeExpressive;
87 import com.google.ux.material.libmonet.scheme.SchemeFruitSalad;
88 import com.google.ux.material.libmonet.scheme.SchemeMonochrome;
89 import com.google.ux.material.libmonet.scheme.SchemeNeutral;
90 import com.google.ux.material.libmonet.scheme.SchemeRainbow;
91 import com.google.ux.material.libmonet.scheme.SchemeTonalSpot;
92 import com.google.ux.material.libmonet.scheme.SchemeVibrant;
93 
94 import org.json.JSONException;
95 import org.json.JSONObject;
96 
97 import java.io.PrintWriter;
98 import java.util.ArrayList;
99 import java.util.Arrays;
100 import java.util.Collection;
101 import java.util.HashSet;
102 import java.util.List;
103 import java.util.Map;
104 import java.util.Set;
105 import java.util.concurrent.Executor;
106 import java.util.stream.Collectors;
107 
108 import javax.inject.Inject;
109 
110 /**
111  * Controls the application of theme overlays across the system for all users.
112  * This service is responsible for:
113  * - Observing changes to Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES and applying the
114  * corresponding overlays across the system
115  * - Observing user switches, applying the overlays for the current user to user 0 (for systemui)
116  * - Observing work profile changes and applying overlays from the primary user to their
117  * associated work profiles
118  */
119 @SysUISingleton
120 public class ThemeOverlayController implements CoreStartable, Dumpable {
121     protected static final String TAG = "ThemeOverlayController";
122     private static final boolean DEBUG = true;
123 
124     private final ThemeOverlayApplier mThemeManager;
125     private final UserManager mUserManager;
126     private final BroadcastDispatcher mBroadcastDispatcher;
127     private final Executor mBgExecutor;
128     private final SecureSettings mSecureSettings;
129     private final Executor mMainExecutor;
130     private final Handler mBgHandler;
131     private final boolean mIsMonochromaticEnabled;
132     private final Context mContext;
133     private final boolean mIsMonetEnabled;
134     private final boolean mIsFidelityEnabled;
135     private final UserTracker mUserTracker;
136     private final DeviceProvisionedController mDeviceProvisionedController;
137     private final Resources mResources;
138     // Current wallpaper colors associated to a user.
139     private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>();
140     private final WallpaperManager mWallpaperManager;
141     private final ActivityManager mActivityManager;
142     @VisibleForTesting
143     protected ColorScheme mColorScheme;
144     // If fabricated overlays were already created for the current theme.
145     private boolean mNeedsOverlayCreation;
146     // Dominant color extracted from wallpaper, NOT the color used on the overlay
147     protected int mMainWallpaperColor = Color.TRANSPARENT;
148     // UI contrast as reported by UiModeManager
149     private float mContrast = 0;
150     // Theme variant: Vibrant, Tonal, Expressive, etc
151     @VisibleForTesting
152     protected Style mThemeStyle = Style.TONAL_SPOT;
153     // Accent colors overlay
154     private FabricatedOverlay mSecondaryOverlay;
155     // Neutral system colors overlay
156     private FabricatedOverlay mNeutralOverlay;
157     // Dynamic colors overlay
158     private FabricatedOverlay mDynamicOverlay;
159     // If wallpaper color event will be accepted and change the UI colors.
160     private boolean mAcceptColorEvents = true;
161     // If non-null (per user), colors that were sent to the framework, and processing was deferred
162     // until the next time the screen is off.
163     private final SparseArray<WallpaperColors> mDeferredWallpaperColors = new SparseArray<>();
164     private final SparseIntArray mDeferredWallpaperColorsFlags = new SparseIntArray();
165     private final WakefulnessLifecycle mWakefulnessLifecycle;
166     private final UiModeManager mUiModeManager;
167     private DynamicScheme mDynamicSchemeDark;
168     private DynamicScheme mDynamicSchemeLight;
169 
170     // Defers changing themes until Setup Wizard is done.
171     private boolean mDeferredThemeEvaluation;
172     // Determines if we should ignore THEME_CUSTOMIZATION_OVERLAY_PACKAGES setting changes.
173     private boolean mSkipSettingChange;
174 
175     private final DeviceProvisionedListener mDeviceProvisionedListener =
176             new DeviceProvisionedListener() {
177                 @Override
178                 public void onUserSetupChanged() {
179                     if (!mDeviceProvisionedController.isCurrentUserSetup()) {
180                         return;
181                     }
182                     if (!mDeferredThemeEvaluation) {
183                         return;
184                     }
185                     Log.i(TAG, "Applying deferred theme");
186                     mDeferredThemeEvaluation = false;
187                     reevaluateSystemTheme(true /* forceReload */);
188                 }
189             };
190 
191     private final OnColorsChangedListener mOnColorsChangedListener = new OnColorsChangedListener() {
192         @Override
193         public void onColorsChanged(WallpaperColors wallpaperColors, int which) {
194             throw new IllegalStateException("This should never be invoked, all messages should "
195                     + "arrive on the overload that has a user id");
196         }
197 
198         @Override
199         public void onColorsChanged(WallpaperColors wallpaperColors, int which, int userId) {
200             WallpaperColors currentColors = mCurrentColors.get(userId);
201             if (wallpaperColors != null && wallpaperColors.equals(currentColors)) {
202                 return;
203             }
204             boolean currentUser = userId == mUserTracker.getUserId();
205             if (currentUser && !mAcceptColorEvents
206                     && mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP) {
207                 mDeferredWallpaperColors.put(userId, wallpaperColors);
208                 mDeferredWallpaperColorsFlags.put(userId, which);
209                 Log.i(TAG, "colors received; processing deferred until screen off: "
210                         + wallpaperColors + " user: " + userId);
211                 return;
212             }
213 
214             if (currentUser && wallpaperColors != null) {
215                 mAcceptColorEvents = false;
216                 // Any cache of colors deferred for process is now stale.
217                 mDeferredWallpaperColors.put(userId, null);
218                 mDeferredWallpaperColorsFlags.put(userId, 0);
219             }
220 
221             handleWallpaperColors(wallpaperColors, which, userId);
222         }
223     };
224 
225     private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() {
226         @Override
227         public void onUserChanged(int newUser, @NonNull Context userContext) {
228             boolean isManagedProfile = mUserManager.isManagedProfile(newUser);
229             if (!mDeviceProvisionedController.isCurrentUserSetup() && isManagedProfile) {
230                 Log.i(TAG, "User setup not finished when new user event was received. "
231                         + "Deferring... Managed profile? " + isManagedProfile);
232                 return;
233             }
234             if (DEBUG) Log.d(TAG, "Updating overlays for user switch / profile added.");
235             reevaluateSystemTheme(true /* forceReload */);
236         }
237     };
238 
getLatestWallpaperType(int userId)239     private int getLatestWallpaperType(int userId) {
240         return mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, userId)
241                 > mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, userId)
242                 ? WallpaperManager.FLAG_LOCK : WallpaperManager.FLAG_SYSTEM;
243     }
244 
isSeedColorSet(JSONObject jsonObject, WallpaperColors newWallpaperColors)245     private boolean isSeedColorSet(JSONObject jsonObject, WallpaperColors newWallpaperColors) {
246         if (newWallpaperColors == null) {
247             return false;
248         }
249         // Gets the color that was overridden in the theme setting if any.
250         String sysPaletteColor = (String) jsonObject.opt(OVERLAY_CATEGORY_SYSTEM_PALETTE);
251         if (sysPaletteColor == null) {
252             return false;
253         }
254         if (!sysPaletteColor.startsWith("#")) {
255             sysPaletteColor = "#" + sysPaletteColor;
256         }
257         final int systemPaletteColorArgb = Color.parseColor(sysPaletteColor);
258         // Gets seed colors from incoming {@link WallpaperColors} instance.
259         List<Integer> seedColors = ColorScheme.getSeedColors(newWallpaperColors);
260         for (int seedColor : seedColors) {
261             // The seed color from incoming {@link WallpaperColors} instance
262             // was set as color override.
263             if (seedColor == systemPaletteColorArgb) {
264                 if (DEBUG) {
265                     Log.d(TAG, "Same as previous set system palette: " + sysPaletteColor);
266                 }
267                 return true;
268             }
269         }
270         return false;
271     }
272 
handleWallpaperColors(WallpaperColors wallpaperColors, int flags, int userId)273     private void handleWallpaperColors(WallpaperColors wallpaperColors, int flags, int userId) {
274         final int currentUser = mUserTracker.getUserId();
275         final boolean hadWallpaperColors = mCurrentColors.get(userId) != null;
276         int latestWallpaperType = getLatestWallpaperType(userId);
277         boolean eventForLatestWallpaper = (flags & latestWallpaperType) != 0;
278         if (eventForLatestWallpaper) {
279             mCurrentColors.put(userId, wallpaperColors);
280             if (DEBUG) Log.d(TAG, "got new colors: " + wallpaperColors + " where: " + flags);
281         }
282 
283         if (userId != currentUser) {
284             Log.d(TAG, "Colors " + wallpaperColors + " for user " + userId + ". "
285                     + "Not for current user: " + currentUser);
286             return;
287         }
288 
289         if (mDeviceProvisionedController != null
290                 && !mDeviceProvisionedController.isCurrentUserSetup()) {
291             if (hadWallpaperColors) {
292                 Log.i(TAG, "Wallpaper color event deferred until setup is finished: "
293                         + wallpaperColors);
294                 mDeferredThemeEvaluation = true;
295                 return;
296             } else if (mDeferredThemeEvaluation) {
297                 Log.i(TAG, "Wallpaper color event received, but we already were deferring eval: "
298                         + wallpaperColors);
299                 return;
300             } else {
301                 if (DEBUG) {
302                     Log.i(TAG, "During user setup, but allowing first color event: had? "
303                             + hadWallpaperColors + " has? " + (mCurrentColors.get(userId) != null));
304                 }
305             }
306         }
307         // Check if we need to reset to default colors (if a color override was set that is sourced
308         // from the wallpaper)
309         String overlayPackageJson = mSecureSettings.getStringForUser(
310                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
311                 currentUser);
312         boolean isDestinationBoth = (flags == (WallpaperManager.FLAG_SYSTEM
313                 | WallpaperManager.FLAG_LOCK));
314         boolean isDestinationHomeOnly = (flags == WallpaperManager.FLAG_SYSTEM);
315         try {
316             JSONObject jsonObject = (overlayPackageJson == null) ? new JSONObject()
317                     : new JSONObject(overlayPackageJson);
318             // The latest applied wallpaper should be the source of system colors when:
319             // There is not preset color applied and the incoming wallpaper color is not applied
320             String wallpaperPickerColorSource = jsonObject.optString(OVERLAY_COLOR_SOURCE);
321             boolean userChosePresetColor = COLOR_SOURCE_PRESET.equals(wallpaperPickerColorSource);
322             boolean userChoseLockScreenColor = COLOR_SOURCE_LOCK.equals(wallpaperPickerColorSource);
323             boolean preserveLockScreenColor = isDestinationHomeOnly && userChoseLockScreenColor;
324 
325             if (!userChosePresetColor && !preserveLockScreenColor && eventForLatestWallpaper
326                     && !isSeedColorSet(jsonObject, wallpaperColors)) {
327                 mSkipSettingChange = true;
328                 if (jsonObject.has(OVERLAY_CATEGORY_ACCENT_COLOR) || jsonObject.has(
329                         OVERLAY_CATEGORY_SYSTEM_PALETTE)) {
330                     jsonObject.remove(OVERLAY_CATEGORY_DYNAMIC_COLOR);
331                     jsonObject.remove(OVERLAY_CATEGORY_ACCENT_COLOR);
332                     jsonObject.remove(OVERLAY_CATEGORY_SYSTEM_PALETTE);
333                     jsonObject.remove(OVERLAY_COLOR_INDEX);
334                 }
335                 // Keep color_both value because users can change either or both home and
336                 // lock screen wallpapers.
337                 jsonObject.put(OVERLAY_COLOR_BOTH, isDestinationBoth ? "1" : "0");
338 
339                 jsonObject.put(OVERLAY_COLOR_SOURCE,
340                         (flags == WallpaperManager.FLAG_LOCK) ? COLOR_SOURCE_LOCK
341                                 : COLOR_SOURCE_HOME);
342                 jsonObject.put(TIMESTAMP_FIELD, System.currentTimeMillis());
343                 if (DEBUG) {
344                     Log.d(TAG, "Updating theme setting from "
345                             + overlayPackageJson + " to " + jsonObject.toString());
346                 }
347                 mSecureSettings.putStringForUser(
348                         Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
349                         jsonObject.toString(), UserHandle.USER_CURRENT);
350             }
351         } catch (JSONException e) {
352             Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e);
353         }
354         reevaluateSystemTheme(false /* forceReload */);
355     }
356 
357     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
358         @Override
359         public void onReceive(Context context, Intent intent) {
360             boolean newWorkProfile = Intent.ACTION_MANAGED_PROFILE_ADDED.equals(intent.getAction());
361             boolean isManagedProfile = mUserManager.isManagedProfile(
362                     intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
363             if (newWorkProfile) {
364                 if (!mDeviceProvisionedController.isCurrentUserSetup() && isManagedProfile) {
365                     Log.i(TAG, "User setup not finished when " + intent.getAction()
366                             + " was received. Deferring... Managed profile? " + isManagedProfile);
367                     return;
368                 }
369                 if (DEBUG) Log.d(TAG, "Updating overlays for user switch / profile added.");
370                 reevaluateSystemTheme(true /* forceReload */);
371             } else if (Intent.ACTION_WALLPAPER_CHANGED.equals(intent.getAction())) {
372                 if (intent.getBooleanExtra(WallpaperManager.EXTRA_FROM_FOREGROUND_APP, false)) {
373                     mAcceptColorEvents = true;
374                     Log.i(TAG, "Wallpaper changed, allowing color events again");
375                 } else {
376                     Log.i(TAG, "Wallpaper changed from background app, "
377                             + "keep deferring color events. Accepting: " + mAcceptColorEvents);
378                 }
379             }
380         }
381     };
382 
383     @Inject
ThemeOverlayController( Context context, BroadcastDispatcher broadcastDispatcher, @Background Handler bgHandler, @Main Executor mainExecutor, @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier, SecureSettings secureSettings, WallpaperManager wallpaperManager, UserManager userManager, DeviceProvisionedController deviceProvisionedController, UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle, UiModeManager uiModeManager, ActivityManager activityManager)384     public ThemeOverlayController(
385             Context context,
386             BroadcastDispatcher broadcastDispatcher,
387             @Background Handler bgHandler,
388             @Main Executor mainExecutor,
389             @Background Executor bgExecutor,
390             ThemeOverlayApplier themeOverlayApplier,
391             SecureSettings secureSettings,
392             WallpaperManager wallpaperManager,
393             UserManager userManager,
394             DeviceProvisionedController deviceProvisionedController,
395             UserTracker userTracker,
396             DumpManager dumpManager,
397             FeatureFlags featureFlags,
398             @Main Resources resources,
399             WakefulnessLifecycle wakefulnessLifecycle,
400             UiModeManager uiModeManager,
401             ActivityManager activityManager) {
402         mContext = context;
403         mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
404         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
405         mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
406         mDeviceProvisionedController = deviceProvisionedController;
407         mBroadcastDispatcher = broadcastDispatcher;
408         mUserManager = userManager;
409         mBgExecutor = bgExecutor;
410         mMainExecutor = mainExecutor;
411         mBgHandler = bgHandler;
412         mThemeManager = themeOverlayApplier;
413         mSecureSettings = secureSettings;
414         mWallpaperManager = wallpaperManager;
415         mUserTracker = userTracker;
416         mResources = resources;
417         mWakefulnessLifecycle = wakefulnessLifecycle;
418         mUiModeManager = uiModeManager;
419         mActivityManager = activityManager;
420         dumpManager.registerDumpable(TAG, this);
421     }
422 
423     @Override
start()424     public void start() {
425         if (DEBUG) Log.d(TAG, "Start");
426         final IntentFilter filter = new IntentFilter();
427         filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
428         filter.addAction(Intent.ACTION_WALLPAPER_CHANGED);
429         mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor,
430                 UserHandle.ALL);
431         mSecureSettings.registerContentObserverForUser(
432                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
433                 false,
434                 new ContentObserver(mBgHandler) {
435                     @Override
436                     public void onChange(boolean selfChange, Collection<Uri> collection, int flags,
437                             int userId) {
438                         if (DEBUG) Log.d(TAG, "Overlay changed for user: " + userId);
439                         if (mUserTracker.getUserId() != userId) {
440                             return;
441                         }
442                         if (!mDeviceProvisionedController.isUserSetup(userId)) {
443                             Log.i(TAG, "Theme application deferred when setting changed.");
444                             mDeferredThemeEvaluation = true;
445                             return;
446                         }
447                         if (mSkipSettingChange) {
448                             if (DEBUG) Log.d(TAG, "Skipping setting change");
449                             mSkipSettingChange = false;
450                             return;
451                         }
452                         reevaluateSystemTheme(true /* forceReload */);
453                     }
454                 },
455                 UserHandle.USER_ALL);
456         mContrast = mUiModeManager.getContrast();
457         mUiModeManager.addContrastChangeListener(mMainExecutor, contrast -> {
458             mContrast = contrast;
459             // Force reload so that we update even when the main color has not changed
460             reevaluateSystemTheme(true /* forceReload */);
461         });
462 
463         if (!mIsMonetEnabled) {
464             return;
465         }
466 
467         mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
468 
469         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
470 
471         // All wallpaper color and keyguard logic only applies when Monet is enabled.
472         if (!mIsMonetEnabled) {
473             return;
474         }
475 
476         // Upon boot, make sure we have the most up to date colors
477         Runnable updateColors = () -> {
478             WallpaperColors systemColor = mWallpaperManager.getWallpaperColors(
479                     getLatestWallpaperType(mUserTracker.getUserId()));
480             Runnable applyColors = () -> {
481                 if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor);
482                 mCurrentColors.put(mUserTracker.getUserId(), systemColor);
483                 reevaluateSystemTheme(false /* forceReload */);
484             };
485             if (mDeviceProvisionedController.isCurrentUserSetup()) {
486                 mMainExecutor.execute(applyColors);
487             } else {
488                 applyColors.run();
489             }
490         };
491 
492         // Whenever we're going directly to setup wizard, we need to process colors synchronously,
493         // otherwise we'll see some jank when the activity is recreated.
494         if (!mDeviceProvisionedController.isCurrentUserSetup()) {
495             updateColors.run();
496         } else {
497             mBgExecutor.execute(updateColors);
498         }
499         mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null,
500                 UserHandle.USER_ALL);
501         mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
502             @Override
503             public void onFinishedGoingToSleep() {
504                 final int userId = mUserTracker.getUserId();
505                 final WallpaperColors colors = mDeferredWallpaperColors.get(userId);
506                 if (colors != null) {
507                     int flags = mDeferredWallpaperColorsFlags.get(userId);
508 
509                     mDeferredWallpaperColors.put(userId, null);
510                     mDeferredWallpaperColorsFlags.put(userId, 0);
511 
512                     handleWallpaperColors(colors, flags, userId);
513                 }
514             }
515         });
516     }
517 
reevaluateSystemTheme(boolean forceReload)518     private void reevaluateSystemTheme(boolean forceReload) {
519         final WallpaperColors currentColors = mCurrentColors.get(mUserTracker.getUserId());
520         final int mainColor;
521         if (currentColors == null) {
522             mainColor = Color.TRANSPARENT;
523         } else {
524             mainColor = getNeutralColor(currentColors);
525         }
526 
527         if (mMainWallpaperColor == mainColor && !forceReload) {
528             return;
529         }
530         mMainWallpaperColor = mainColor;
531 
532         if (mIsMonetEnabled) {
533             mThemeStyle = fetchThemeStyleFromSetting();
534             createOverlays(mMainWallpaperColor);
535             mNeedsOverlayCreation = true;
536             if (DEBUG) {
537                 Log.d(TAG, "fetched overlays. accent: " + mSecondaryOverlay
538                         + " neutral: " + mNeutralOverlay + " dynamic: " + mDynamicOverlay);
539             }
540         }
541 
542         updateThemeOverlays();
543     }
544 
545     /**
546      * Return the main theme color from a given {@link WallpaperColors} instance.
547      */
getNeutralColor(@onNull WallpaperColors wallpaperColors)548     protected int getNeutralColor(@NonNull WallpaperColors wallpaperColors) {
549         return ColorScheme.getSeedColor(wallpaperColors);
550     }
551 
getAccentColor(@onNull WallpaperColors wallpaperColors)552     protected int getAccentColor(@NonNull WallpaperColors wallpaperColors) {
553         return ColorScheme.getSeedColor(wallpaperColors);
554     }
555 
dynamicSchemeFromStyle(Style style, int color, boolean isDark, double contrastLevel)556     private static DynamicScheme dynamicSchemeFromStyle(Style style, int color,
557             boolean isDark, double contrastLevel) {
558         Hct sourceColorHct = Hct.fromInt(color);
559         switch (style) {
560             case EXPRESSIVE:
561                 return new SchemeExpressive(sourceColorHct, isDark, contrastLevel);
562             case SPRITZ:
563                 return new SchemeNeutral(sourceColorHct, isDark, contrastLevel);
564             case TONAL_SPOT:
565                 return new SchemeTonalSpot(sourceColorHct, isDark, contrastLevel);
566             case FRUIT_SALAD:
567                 return new SchemeFruitSalad(sourceColorHct, isDark, contrastLevel);
568             case RAINBOW:
569                 return new SchemeRainbow(sourceColorHct, isDark, contrastLevel);
570             case VIBRANT:
571                 return new SchemeVibrant(sourceColorHct, isDark, contrastLevel);
572             case MONOCHROMATIC:
573                 return new SchemeMonochrome(sourceColorHct, isDark, contrastLevel);
574             default:
575                 return null;
576         }
577     }
578 
579     @VisibleForTesting
isNightMode()580     protected boolean isNightMode() {
581         return (mResources.getConfiguration().uiMode
582                 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
583     }
584 
585     @VisibleForTesting
newFabricatedOverlay(String name)586     protected FabricatedOverlay newFabricatedOverlay(String name) {
587         return new FabricatedOverlay.Builder("com.android.systemui", name, "android").build();
588     }
589 
createOverlays(int color)590     private void createOverlays(int color) {
591         boolean nightMode = isNightMode();
592         mColorScheme = new ColorScheme(color, nightMode, mThemeStyle);
593         mNeutralOverlay = createNeutralOverlay();
594         mSecondaryOverlay = createAccentOverlay();
595 
596         mDynamicSchemeDark = dynamicSchemeFromStyle(
597                 mThemeStyle, color, true /* isDark */, mContrast);
598         mDynamicSchemeLight = dynamicSchemeFromStyle(
599                 mThemeStyle, color, false /* isDark */, mContrast);
600         mDynamicOverlay = createDynamicOverlay();
601     }
602 
createNeutralOverlay()603     protected FabricatedOverlay createNeutralOverlay() {
604         FabricatedOverlay overlay = newFabricatedOverlay("neutral");
605         assignTonalPaletteToOverlay("neutral1", overlay, mColorScheme.getNeutral1());
606         assignTonalPaletteToOverlay("neutral2", overlay, mColorScheme.getNeutral2());
607         return overlay;
608     }
609 
createAccentOverlay()610     protected FabricatedOverlay createAccentOverlay() {
611         FabricatedOverlay overlay = newFabricatedOverlay("accent");
612         assignTonalPaletteToOverlay("accent1", overlay, mColorScheme.getAccent1());
613         assignTonalPaletteToOverlay("accent2", overlay, mColorScheme.getAccent2());
614         assignTonalPaletteToOverlay("accent3", overlay, mColorScheme.getAccent3());
615         return overlay;
616     }
617 
assignTonalPaletteToOverlay(String name, FabricatedOverlay overlay, TonalPalette tonalPalette)618     private void assignTonalPaletteToOverlay(String name, FabricatedOverlay overlay,
619             TonalPalette tonalPalette) {
620         String resourcePrefix = "android:color/system_" + name;
621 
622         tonalPalette.getAllShadesMapped().forEach((key, value) -> {
623             String resourceName = resourcePrefix + "_" + key;
624             int colorValue = ColorUtils.setAlphaComponent(value, 0xFF);
625             overlay.setResourceValue(resourceName, TYPE_INT_COLOR_ARGB8, colorValue,
626                     null /* configuration */);
627         });
628     }
629 
createDynamicOverlay()630     protected FabricatedOverlay createDynamicOverlay() {
631         FabricatedOverlay overlay = newFabricatedOverlay("dynamic");
632         assignDynamicPaletteToOverlay(overlay, true /* isDark */);
633         assignDynamicPaletteToOverlay(overlay, false /* isDark */);
634         assignFixedColorsToOverlay(overlay);
635         return overlay;
636     }
637 
assignDynamicPaletteToOverlay(FabricatedOverlay overlay, boolean isDark)638     private void assignDynamicPaletteToOverlay(FabricatedOverlay overlay, boolean isDark) {
639         String suffix = isDark ? "dark" : "light";
640         DynamicScheme scheme = isDark ? mDynamicSchemeDark : mDynamicSchemeLight;
641         DynamicColors.allDynamicColorsMapped(mIsFidelityEnabled).forEach(p -> {
642             String resourceName = "android:color/system_" + p.first + "_" + suffix;
643             int colorValue = p.second.getArgb(scheme);
644             overlay.setResourceValue(resourceName, TYPE_INT_COLOR_ARGB8, colorValue,
645                     null /* configuration */);
646         });
647     }
648 
assignFixedColorsToOverlay(FabricatedOverlay overlay)649     private void assignFixedColorsToOverlay(FabricatedOverlay overlay) {
650         DynamicColors.getFixedColorsMapped(mIsFidelityEnabled).forEach(p -> {
651             String resourceName = "android:color/system_" + p.first;
652             int colorValue = p.second.getArgb(mDynamicSchemeLight);
653             overlay.setResourceValue(resourceName, TYPE_INT_COLOR_ARGB8, colorValue,
654                     null /* configuration */);
655         });
656     }
657 
658     /**
659      * Checks if the color scheme in mColorScheme matches the current system palettes.
660      * @param managedProfiles List of managed profiles for this user.
661      */
colorSchemeIsApplied(Set<UserHandle> managedProfiles)662     private boolean colorSchemeIsApplied(Set<UserHandle> managedProfiles) {
663         final ArraySet<UserHandle> allProfiles = new ArraySet<>(managedProfiles);
664         allProfiles.add(UserHandle.SYSTEM);
665         for (UserHandle userHandle : allProfiles) {
666             Resources res = userHandle.isSystem()
667                     ? mResources : mContext.createContextAsUser(userHandle, 0).getResources();
668             Resources.Theme theme = mContext.getTheme();
669             MaterialDynamicColors dynamicColors = new MaterialDynamicColors(mIsFidelityEnabled);
670             if (!(res.getColor(android.R.color.system_accent1_500, theme)
671                     == mColorScheme.getAccent1().getS500()
672                     && res.getColor(android.R.color.system_accent2_500, theme)
673                     == mColorScheme.getAccent2().getS500()
674                     && res.getColor(android.R.color.system_accent3_500, theme)
675                     == mColorScheme.getAccent3().getS500()
676                     && res.getColor(android.R.color.system_neutral1_500, theme)
677                     == mColorScheme.getNeutral1().getS500()
678                     && res.getColor(android.R.color.system_neutral2_500, theme)
679                     == mColorScheme.getNeutral2().getS500()
680                     && res.getColor(android.R.color.system_outline_variant_dark, theme)
681                     == dynamicColors.outlineVariant().getArgb(mDynamicSchemeDark)
682                     && res.getColor(android.R.color.system_outline_variant_light, theme)
683                     == dynamicColors.outlineVariant().getArgb(mDynamicSchemeLight)
684                     && res.getColor(android.R.color.system_primary_container_dark, theme)
685                     == dynamicColors.primaryContainer().getArgb(mDynamicSchemeDark)
686                     && res.getColor(android.R.color.system_primary_container_light, theme)
687                     == dynamicColors.primaryContainer().getArgb(mDynamicSchemeLight)
688                     && res.getColor(android.R.color.system_primary_fixed, theme)
689                     == dynamicColors.primaryFixed().getArgb(mDynamicSchemeLight))) {
690                 return false;
691             }
692         }
693         return true;
694     }
695 
updateThemeOverlays()696     private void updateThemeOverlays() {
697         final int currentUser = mUserTracker.getUserId();
698         final String overlayPackageJson = mSecureSettings.getStringForUser(
699                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
700                 currentUser);
701         if (DEBUG) Log.d(TAG, "updateThemeOverlays. Setting: " + overlayPackageJson);
702         final Map<String, OverlayIdentifier> categoryToPackage = new ArrayMap<>();
703         if (!TextUtils.isEmpty(overlayPackageJson)) {
704             try {
705                 JSONObject object = new JSONObject(overlayPackageJson);
706                 for (String category : ThemeOverlayApplier.THEME_CATEGORIES) {
707                     if (object.has(category)) {
708                         OverlayIdentifier identifier =
709                                 new OverlayIdentifier(object.getString(category));
710                         categoryToPackage.put(category, identifier);
711                     }
712                 }
713             } catch (JSONException e) {
714                 Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e);
715             }
716         }
717 
718         // Let's generate system overlay if the style picker decided to override it.
719         OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE);
720         if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) {
721             try {
722                 String colorString =  systemPalette.getPackageName().toLowerCase();
723                 if (!colorString.startsWith("#")) {
724                     colorString = "#" + colorString;
725                 }
726                 createOverlays(Color.parseColor(colorString));
727                 mNeedsOverlayCreation = true;
728                 categoryToPackage.remove(OVERLAY_CATEGORY_SYSTEM_PALETTE);
729                 categoryToPackage.remove(OVERLAY_CATEGORY_ACCENT_COLOR);
730                 categoryToPackage.remove(OVERLAY_CATEGORY_DYNAMIC_COLOR);
731             } catch (Exception e) {
732                 // Color.parseColor doesn't catch any exceptions from the calls it makes
733                 Log.w(TAG, "Invalid color definition: " + systemPalette.getPackageName(), e);
734             }
735         } else if (!mIsMonetEnabled && systemPalette != null) {
736             try {
737                 // It's possible that we flipped the flag off and still have a @ColorInt in the
738                 // setting. We need to sanitize the input, otherwise the overlay transaction will
739                 // fail.
740                 categoryToPackage.remove(OVERLAY_CATEGORY_SYSTEM_PALETTE);
741                 categoryToPackage.remove(OVERLAY_CATEGORY_ACCENT_COLOR);
742                 categoryToPackage.remove(OVERLAY_CATEGORY_DYNAMIC_COLOR);
743             } catch (NumberFormatException e) {
744                 // This is a package name. All good, let's continue
745             }
746         }
747 
748         // Compatibility with legacy themes, where full packages were defined, instead of just
749         // colors.
750         if (!categoryToPackage.containsKey(OVERLAY_CATEGORY_SYSTEM_PALETTE)
751                 && mNeutralOverlay != null) {
752             categoryToPackage.put(OVERLAY_CATEGORY_SYSTEM_PALETTE,
753                     mNeutralOverlay.getIdentifier());
754         }
755         if (!categoryToPackage.containsKey(OVERLAY_CATEGORY_ACCENT_COLOR)
756                 && mSecondaryOverlay != null) {
757             categoryToPackage.put(OVERLAY_CATEGORY_ACCENT_COLOR, mSecondaryOverlay.getIdentifier());
758         }
759         if (!categoryToPackage.containsKey(OVERLAY_CATEGORY_DYNAMIC_COLOR)
760                 && mDynamicOverlay != null) {
761             categoryToPackage.put(OVERLAY_CATEGORY_DYNAMIC_COLOR, mDynamicOverlay.getIdentifier());
762         }
763 
764         Set<UserHandle> managedProfiles = new HashSet<>();
765         for (UserInfo userInfo : mUserManager.getEnabledProfiles(currentUser)) {
766             if (userInfo.isManagedProfile()) {
767                 managedProfiles.add(userInfo.getUserHandle());
768             }
769         }
770 
771         final Runnable onCompleteCallback = () -> {
772             Log.d(TAG, "ThemeHomeDelay: ThemeOverlayController ready");
773             mActivityManager.setThemeOverlayReady(currentUser);
774         };
775 
776         if (colorSchemeIsApplied(managedProfiles)) {
777             Log.d(TAG, "Skipping overlay creation. Theme was already: " + mColorScheme);
778             onCompleteCallback.run();
779             return;
780         }
781 
782         if (DEBUG) {
783             Log.d(TAG, "Applying overlays: " + categoryToPackage.keySet().stream()
784                     .map(key -> key + " -> " + categoryToPackage.get(key)).collect(
785                             Collectors.joining(", ")));
786         }
787 
788         FabricatedOverlay[] fOverlays = null;
789 
790         if (mNeedsOverlayCreation) {
791             mNeedsOverlayCreation = false;
792             fOverlays = new FabricatedOverlay[]{
793                     mSecondaryOverlay, mNeutralOverlay, mDynamicOverlay
794             };
795         }
796 
797         mThemeManager.applyCurrentUserOverlays(categoryToPackage, fOverlays, currentUser,
798                 managedProfiles, onCompleteCallback);
799 
800         }
801 
fetchThemeStyleFromSetting()802     private Style fetchThemeStyleFromSetting() {
803         // Allow-list of Style objects that can be created from a setting string, i.e. can be
804         // used as a system-wide theme.
805         // - Content intentionally excluded, intended for media player, not system-wide
806         List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ,
807                 Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT,
808                 Style.MONOCHROMATIC));
809 
810         Style style = mThemeStyle;
811         final String overlayPackageJson = mSecureSettings.getStringForUser(
812                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
813                 mUserTracker.getUserId());
814         if (!TextUtils.isEmpty(overlayPackageJson)) {
815             try {
816                 JSONObject object = new JSONObject(overlayPackageJson);
817                 style = Style.valueOf(
818                         object.getString(ThemeOverlayApplier.OVERLAY_CATEGORY_THEME_STYLE));
819                 if (!validStyles.contains(style)) {
820                     style = Style.TONAL_SPOT;
821                 }
822             } catch (JSONException | IllegalArgumentException e) {
823                 Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e);
824                 style = Style.TONAL_SPOT;
825             }
826         }
827         return style;
828     }
829 
830     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)831     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
832         pw.println("mSystemColors=" + mCurrentColors);
833         pw.println("mMainWallpaperColor=" + Integer.toHexString(mMainWallpaperColor));
834         pw.println("mSecondaryOverlay=" + mSecondaryOverlay);
835         pw.println("mNeutralOverlay=" + mNeutralOverlay);
836         pw.println("mDynamicOverlay=" + mDynamicOverlay);
837         pw.println("mIsMonetEnabled=" + mIsMonetEnabled);
838         pw.println("mIsFidelityEnabled=" + mIsFidelityEnabled);
839         pw.println("mColorScheme=" + mColorScheme);
840         pw.println("mNeedsOverlayCreation=" + mNeedsOverlayCreation);
841         pw.println("mAcceptColorEvents=" + mAcceptColorEvents);
842         pw.println("mDeferredThemeEvaluation=" + mDeferredThemeEvaluation);
843         pw.println("mThemeStyle=" + mThemeStyle);
844     }
845 }
846