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