1 /* 2 * Copyright (C) 2022 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 package com.android.customization.model.color 17 18 import android.app.WallpaperColors 19 import android.app.WallpaperManager 20 import android.content.Context 21 import android.content.res.ColorStateList 22 import android.content.res.Resources 23 import androidx.annotation.ColorInt 24 import androidx.core.graphics.ColorUtils.setAlphaComponent 25 import androidx.lifecycle.LifecycleOwner 26 import androidx.lifecycle.lifecycleScope 27 import com.android.customization.model.CustomizationManager.OptionsFetchedListener 28 import com.android.customization.model.ResourceConstants.COLOR_BUNDLES_ARRAY_NAME 29 import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_MAIN_COLOR_PREFIX 30 import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_NAME_PREFIX 31 import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_STYLE_PREFIX 32 import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR 33 import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE 34 import com.android.customization.model.ResourcesApkProvider 35 import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_HOME 36 import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_LOCK 37 import com.android.customization.model.color.ColorUtils.toColorString 38 import com.android.customization.picker.color.shared.model.ColorType 39 import com.android.systemui.monet.ColorScheme 40 import com.android.systemui.monet.Style 41 import com.android.wallpaper.R 42 import com.android.wallpaper.module.InjectorProvider 43 import kotlinx.coroutines.CoroutineScope 44 import kotlinx.coroutines.Dispatchers 45 import kotlinx.coroutines.SupervisorJob 46 import kotlinx.coroutines.launch 47 import kotlinx.coroutines.withContext 48 49 /** 50 * Default implementation of {@link ColorOptionsProvider} that reads preset colors from a stub APK. 51 */ 52 class ColorProvider(private val context: Context, stubPackageName: String) : 53 ResourcesApkProvider(context, stubPackageName), ColorOptionsProvider { 54 55 companion object { 56 const val themeStyleEnabled = true 57 val styleSize = if (themeStyleEnabled) Style.values().size else 1 58 private const val TAG = "ColorProvider" 59 private const val MAX_SEED_COLORS = 4 60 private const val MAX_PRESET_COLORS = 4 61 private const val ALPHA_MASK = 0xFF 62 } 63 64 private val monetEnabled = ColorUtils.isMonetEnabled(context) 65 // TODO(b/202145216): Use style method to fetch the list of style. 66 private var styleList = 67 if (themeStyleEnabled) 68 arrayOf(Style.TONAL_SPOT, Style.SPRITZ, Style.VIBRANT, Style.EXPRESSIVE) 69 else arrayOf(Style.TONAL_SPOT) 70 71 private val monochromeEnabled = 72 InjectorProvider.getInjector().getFlags().isMonochromaticThemeEnabled(mContext) 73 private var monochromeBundleName: String? = null 74 75 private val scope = 76 if (mContext is LifecycleOwner) { 77 mContext.lifecycleScope 78 } else { 79 CoroutineScope(Dispatchers.Default + SupervisorJob()) 80 } 81 82 private var colorsAvailable = true 83 private var colorBundles: List<ColorOption>? = null 84 private var homeWallpaperColors: WallpaperColors? = null 85 private var lockWallpaperColors: WallpaperColors? = null 86 isAvailablenull87 override fun isAvailable(): Boolean { 88 return monetEnabled && super.isAvailable() && colorsAvailable 89 } 90 fetchnull91 override fun fetch( 92 callback: OptionsFetchedListener<ColorOption>?, 93 reload: Boolean, 94 homeWallpaperColors: WallpaperColors?, 95 lockWallpaperColors: WallpaperColors?, 96 shouldUseRevampedUi: Boolean 97 ) { 98 val wallpaperColorsChanged = 99 this.homeWallpaperColors != homeWallpaperColors || 100 this.lockWallpaperColors != lockWallpaperColors 101 if (wallpaperColorsChanged) { 102 this.homeWallpaperColors = homeWallpaperColors 103 this.lockWallpaperColors = lockWallpaperColors 104 } 105 if (colorBundles == null || reload || wallpaperColorsChanged) { 106 scope.launch { 107 try { 108 if (colorBundles == null || reload) { 109 loadPreset(shouldUseRevampedUi) 110 } 111 if (wallpaperColorsChanged || reload) { 112 loadSeedColors( 113 homeWallpaperColors, 114 lockWallpaperColors, 115 shouldUseRevampedUi 116 ) 117 } 118 } catch (e: Throwable) { 119 colorsAvailable = false 120 callback?.onError(e) 121 return@launch 122 } 123 callback?.onOptionsLoaded(colorBundles) 124 } 125 } else { 126 callback?.onOptionsLoaded(colorBundles) 127 } 128 } 129 isLockScreenWallpaperLastAppliednull130 private fun isLockScreenWallpaperLastApplied(): Boolean { 131 // The WallpaperId increases every time a new wallpaper is set, so the larger wallpaper id 132 // is the most recently set wallpaper 133 val manager = WallpaperManager.getInstance(mContext) 134 return manager.getWallpaperId(WallpaperManager.FLAG_LOCK) > 135 manager.getWallpaperId(WallpaperManager.FLAG_SYSTEM) 136 } 137 loadSeedColorsnull138 private fun loadSeedColors( 139 homeWallpaperColors: WallpaperColors?, 140 lockWallpaperColors: WallpaperColors?, 141 shouldUseRevampedUi: Boolean, 142 ) { 143 if (homeWallpaperColors == null) return 144 145 val bundles: MutableList<ColorOption> = ArrayList() 146 val colorsPerSource = 147 if (lockWallpaperColors == null) { 148 MAX_SEED_COLORS 149 } else { 150 MAX_SEED_COLORS / 2 151 } 152 153 if (lockWallpaperColors != null) { 154 val shouldLockColorsGoFirst = isLockScreenWallpaperLastApplied() 155 // First half of the colors 156 buildColorSeeds( 157 if (shouldLockColorsGoFirst) lockWallpaperColors else homeWallpaperColors, 158 colorsPerSource, 159 if (shouldLockColorsGoFirst) COLOR_SOURCE_LOCK else COLOR_SOURCE_HOME, 160 true, 161 bundles, 162 shouldUseRevampedUi 163 ) 164 // Second half of the colors 165 buildColorSeeds( 166 if (shouldLockColorsGoFirst) homeWallpaperColors else lockWallpaperColors, 167 MAX_SEED_COLORS - bundles.size / styleSize, 168 if (shouldLockColorsGoFirst) COLOR_SOURCE_HOME else COLOR_SOURCE_LOCK, 169 false, 170 bundles, 171 shouldUseRevampedUi 172 ) 173 } else { 174 buildColorSeeds( 175 homeWallpaperColors, 176 colorsPerSource, 177 COLOR_SOURCE_HOME, 178 true, 179 bundles, 180 shouldUseRevampedUi 181 ) 182 } 183 184 if (shouldUseRevampedUi) { 185 // Insert monochrome in the second position if it is enabled and included in preset 186 // colors 187 if (monochromeEnabled) { 188 monochromeBundleName?.let { 189 bundles.add( 190 1, 191 buildRevampedUIPreset( 192 it, 193 -1, 194 Style.MONOCHROMATIC, 195 ColorType.WALLPAPER_COLOR 196 ) 197 ) 198 } 199 } 200 bundles.addAll( 201 colorBundles?.filterNot { 202 (it as ColorOptionImpl).type == ColorType.WALLPAPER_COLOR 203 } 204 ?: emptyList() 205 ) 206 } else { 207 bundles.addAll(colorBundles?.filterNot { it is ColorSeedOption } ?: emptyList()) 208 } 209 colorBundles = bundles 210 } 211 buildColorSeedsnull212 private fun buildColorSeeds( 213 wallpaperColors: WallpaperColors, 214 maxColors: Int, 215 source: String, 216 containsDefault: Boolean, 217 bundles: MutableList<ColorOption>, 218 shouldUseRevampedUi: Boolean, 219 ) { 220 val seedColors = ColorScheme.getSeedColors(wallpaperColors) 221 val defaultSeed = seedColors.first() 222 buildBundle(defaultSeed, 0, containsDefault, source, bundles, shouldUseRevampedUi) 223 for ((i, colorInt) in seedColors.drop(1).take(maxColors - 1).withIndex()) { 224 buildBundle(colorInt, i + 1, false, source, bundles, shouldUseRevampedUi) 225 } 226 } 227 buildBundlenull228 private fun buildBundle( 229 colorInt: Int, 230 i: Int, 231 isDefault: Boolean, 232 source: String, 233 bundles: MutableList<ColorOption>, 234 shouldUseRevampedUi: Boolean, 235 ) { 236 // TODO(b/202145216): Measure time cost in the loop. 237 if (shouldUseRevampedUi) { 238 for (style in styleList) { 239 val lightColorScheme = ColorScheme(colorInt, /* darkTheme= */ false, style) 240 val darkColorScheme = ColorScheme(colorInt, /* darkTheme= */ true, style) 241 val builder = ColorOptionImpl.Builder() 242 builder.lightColors = getRevampedUILightColorPreview(lightColorScheme) 243 builder.darkColors = getRevampedUIDarkColorPreview(darkColorScheme) 244 builder.addOverlayPackage( 245 OVERLAY_CATEGORY_SYSTEM_PALETTE, 246 if (isDefault) "" else toColorString(colorInt) 247 ) 248 builder.title = 249 when (style) { 250 Style.TONAL_SPOT -> 251 context.getString(R.string.content_description_dynamic_color_option) 252 Style.SPRITZ -> 253 context.getString(R.string.content_description_neutral_color_option) 254 Style.VIBRANT -> 255 context.getString(R.string.content_description_vibrant_color_option) 256 Style.EXPRESSIVE -> 257 context.getString(R.string.content_description_expressive_color_option) 258 else -> context.getString(R.string.content_description_dynamic_color_option) 259 } 260 builder.source = source 261 builder.style = style 262 // Color option index value starts from 1. 263 builder.index = i + 1 264 builder.isDefault = isDefault 265 builder.type = ColorType.WALLPAPER_COLOR 266 bundles.add(builder.build()) 267 } 268 } else { 269 for (style in styleList) { 270 val lightColorScheme = ColorScheme(colorInt, /* darkTheme= */ false, style) 271 val darkColorScheme = ColorScheme(colorInt, /* darkTheme= */ true, style) 272 val builder = ColorSeedOption.Builder() 273 builder 274 .setLightColors(lightColorScheme.getLightColorPreview()) 275 .setDarkColors(darkColorScheme.getDarkColorPreview()) 276 .addOverlayPackage( 277 OVERLAY_CATEGORY_SYSTEM_PALETTE, 278 if (isDefault) "" else toColorString(colorInt) 279 ) 280 .addOverlayPackage( 281 OVERLAY_CATEGORY_COLOR, 282 if (isDefault) "" else toColorString(colorInt) 283 ) 284 .setSource(source) 285 .setStyle(style) 286 // Color option index value starts from 1. 287 .setIndex(i + 1) 288 289 if (isDefault) builder.asDefault() 290 291 bundles.add(builder.build()) 292 } 293 } 294 } 295 296 /** 297 * Returns the colors for the light theme version of the preview of a ColorScheme based on this 298 * order: top left, top right, bottom left, bottom right 299 */ 300 @ColorInt ColorSchemenull301 private fun ColorScheme.getLightColorPreview(): IntArray { 302 return when (this.style) { 303 Style.EXPRESSIVE -> 304 intArrayOf( 305 setAlphaComponent(this.accent1.s100, ALPHA_MASK), 306 setAlphaComponent(this.accent1.s100, ALPHA_MASK), 307 ColorStateList.valueOf(this.neutral2.s500).withLStar(80f).colors[0], 308 setAlphaComponent(this.accent2.s500, ALPHA_MASK) 309 ) 310 else -> 311 intArrayOf( 312 setAlphaComponent(this.accent1.s100, ALPHA_MASK), 313 setAlphaComponent(this.accent1.s100, ALPHA_MASK), 314 ColorStateList.valueOf(this.accent3.s500).withLStar(85f).colors[0], 315 setAlphaComponent(this.accent1.s500, ALPHA_MASK) 316 ) 317 } 318 } 319 320 /** 321 * Returns the color for the dark theme version of the preview of a ColorScheme based on this 322 * order: top left, top right, bottom left, bottom right 323 */ 324 @ColorInt getDarkColorPreviewnull325 private fun ColorScheme.getDarkColorPreview(): IntArray { 326 return getLightColorPreview() 327 } 328 329 /** 330 * Returns the light theme version of the Revamped UI preview of a ColorScheme based on this 331 * order: top left, top right, bottom left, bottom right 332 * 333 * This color mapping corresponds to GM3 colors: Primary (light), Primary (light), Secondary 334 * LStar 85, and Tertiary LStar 70 335 */ 336 @ColorInt getRevampedUILightColorPreviewnull337 private fun getRevampedUILightColorPreview(colorScheme: ColorScheme): IntArray { 338 return intArrayOf( 339 setAlphaComponent(colorScheme.accent1.s600, ALPHA_MASK), 340 setAlphaComponent(colorScheme.accent1.s600, ALPHA_MASK), 341 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(85f).colors[0], 342 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 343 ) 344 } 345 346 /** 347 * Returns the dark theme version of the Revamped UI preview of a ColorScheme based on this 348 * order: top left, top right, bottom left, bottom right 349 * 350 * This color mapping corresponds to GM3 colors: Primary (dark), Primary (dark), Secondary LStar 351 * 35, and Tertiary LStar 70 352 */ 353 @ColorInt getRevampedUIDarkColorPreviewnull354 private fun getRevampedUIDarkColorPreview(colorScheme: ColorScheme): IntArray { 355 return intArrayOf( 356 setAlphaComponent(colorScheme.accent1.s200, ALPHA_MASK), 357 setAlphaComponent(colorScheme.accent1.s200, ALPHA_MASK), 358 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(35f).colors[0], 359 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 360 ) 361 } 362 363 /** 364 * Returns the light theme version of the Revamped UI preview of a ColorScheme based on this 365 * order: top left, top right, bottom left, bottom right 366 * 367 * This color mapping corresponds to GM3 colors: Primary LStar 0, Primary LStar 0, Secondary 368 * LStar 85, and Tertiary LStar 70 369 */ 370 @ColorInt getRevampedUILightMonochromePreviewnull371 private fun getRevampedUILightMonochromePreview(colorScheme: ColorScheme): IntArray { 372 return intArrayOf( 373 setAlphaComponent(colorScheme.accent1.s1000, ALPHA_MASK), 374 setAlphaComponent(colorScheme.accent1.s1000, ALPHA_MASK), 375 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(85f).colors[0], 376 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 377 ) 378 } 379 380 /** 381 * Returns the dark theme version of the Revamped UI preview of a ColorScheme based on this 382 * order: top left, top right, bottom left, bottom right 383 * 384 * This color mapping corresponds to GM3 colors: Primary LStar 99, Primary LStar 99, Secondary 385 * LStar 35, and Tertiary LStar 70 386 */ 387 @ColorInt getRevampedUIDarkMonochromePreviewnull388 private fun getRevampedUIDarkMonochromePreview(colorScheme: ColorScheme): IntArray { 389 return intArrayOf( 390 setAlphaComponent(colorScheme.accent1.s10, ALPHA_MASK), 391 setAlphaComponent(colorScheme.accent1.s10, ALPHA_MASK), 392 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(35f).colors[0], 393 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 394 ) 395 } 396 397 /** 398 * Returns the Revamped UI preview of a preset ColorScheme based on this order: top left, top 399 * right, bottom left, bottom right 400 */ getRevampedUIPresetColorPreviewnull401 private fun getRevampedUIPresetColorPreview(colorScheme: ColorScheme, seed: Int): IntArray { 402 val colors = 403 when (colorScheme.style) { 404 Style.FRUIT_SALAD -> intArrayOf(seed, colorScheme.accent1.s200) 405 Style.TONAL_SPOT -> intArrayOf(colorScheme.accentColor, colorScheme.accentColor) 406 Style.RAINBOW -> intArrayOf(colorScheme.accent1.s200, colorScheme.accent1.s200) 407 else -> intArrayOf(colorScheme.accent1.s100, colorScheme.accent1.s100) 408 } 409 return intArrayOf( 410 colors[0], 411 colors[1], 412 colors[0], 413 colors[1], 414 ) 415 } 416 getPresetColorPreviewnull417 private fun ColorScheme.getPresetColorPreview(seed: Int): IntArray { 418 return when (this.style) { 419 Style.FRUIT_SALAD -> intArrayOf(seed, this.accent1.s100) 420 Style.TONAL_SPOT -> intArrayOf(this.accentColor, this.accentColor) 421 Style.MONOCHROMATIC -> 422 intArrayOf( 423 setAlphaComponent(0x000000, 255), 424 setAlphaComponent(0xFFFFFF, 255), 425 ) 426 else -> intArrayOf(this.accent1.s100, this.accent1.s100) 427 } 428 } 429 loadPresetnull430 private suspend fun loadPreset(shouldUseRevampedUi: Boolean) = 431 withContext(Dispatchers.IO) { 432 val extractor = ColorBundlePreviewExtractor(mContext) 433 val bundles: MutableList<ColorOption> = ArrayList() 434 435 val bundleNames = 436 if (isAvailable) getItemsFromStub(COLOR_BUNDLES_ARRAY_NAME) else emptyArray() 437 // Color option index value starts from 1. 438 var index = 1 439 val maxPresetColors = if (themeStyleEnabled) bundleNames.size else MAX_PRESET_COLORS 440 441 if (shouldUseRevampedUi) { 442 // keep track of whether monochrome is included in preset colors to determine 443 // inclusion in wallpaper colors 444 var hasMonochrome = false 445 for (bundleName in bundleNames.take(maxPresetColors)) { 446 if (themeStyleEnabled) { 447 val styleName = 448 try { 449 getItemStringFromStub(COLOR_BUNDLE_STYLE_PREFIX, bundleName) 450 } catch (e: Resources.NotFoundException) { 451 null 452 } 453 val style = 454 try { 455 if (styleName != null) Style.valueOf(styleName) 456 else Style.TONAL_SPOT 457 } catch (e: IllegalArgumentException) { 458 Style.TONAL_SPOT 459 } 460 461 if (style == Style.MONOCHROMATIC) { 462 if (!monochromeEnabled) { 463 continue 464 } 465 hasMonochrome = true 466 monochromeBundleName = bundleName 467 } 468 bundles.add(buildRevampedUIPreset(bundleName, index, style)) 469 } else { 470 bundles.add(buildRevampedUIPreset(bundleName, index, null)) 471 } 472 473 index++ 474 } 475 if (!hasMonochrome) { 476 monochromeBundleName = null 477 } 478 } else { 479 for (bundleName in bundleNames.take(maxPresetColors)) { 480 val builder = ColorBundle.Builder() 481 builder.title = getItemStringFromStub(COLOR_BUNDLE_NAME_PREFIX, bundleName) 482 builder.setIndex(index) 483 val colorFromStub = 484 getItemColorFromStub(COLOR_BUNDLE_MAIN_COLOR_PREFIX, bundleName) 485 extractor.addPrimaryColor(builder, colorFromStub) 486 extractor.addSecondaryColor(builder, colorFromStub) 487 if (themeStyleEnabled) { 488 val styleName = 489 try { 490 getItemStringFromStub(COLOR_BUNDLE_STYLE_PREFIX, bundleName) 491 } catch (e: Resources.NotFoundException) { 492 null 493 } 494 extractor.addColorStyle(builder, styleName) 495 val style = 496 try { 497 if (styleName != null) Style.valueOf(styleName) 498 else Style.TONAL_SPOT 499 } catch (e: IllegalArgumentException) { 500 Style.TONAL_SPOT 501 } 502 503 if (style == Style.MONOCHROMATIC && !monochromeEnabled) { 504 continue 505 } 506 507 val darkColors = 508 ColorScheme(colorFromStub, /* darkTheme= */ true, style) 509 .getPresetColorPreview(colorFromStub) 510 val lightColors = 511 ColorScheme(colorFromStub, /* darkTheme= */ false, style) 512 .getPresetColorPreview(colorFromStub) 513 builder 514 .setColorPrimaryDark(darkColors[0]) 515 .setColorSecondaryDark(darkColors[1]) 516 builder 517 .setColorPrimaryLight(lightColors[0]) 518 .setColorSecondaryLight(lightColors[1]) 519 } 520 521 bundles.add(builder.build(mContext)) 522 index++ 523 } 524 } 525 526 colorBundles = bundles 527 } 528 buildRevampedUIPresetnull529 private fun buildRevampedUIPreset( 530 bundleName: String, 531 index: Int, 532 style: Style? = null, 533 type: ColorType = ColorType.PRESET_COLOR, 534 ): ColorOptionImpl { 535 val builder = ColorOptionImpl.Builder() 536 builder.title = getItemStringFromStub(COLOR_BUNDLE_NAME_PREFIX, bundleName) 537 builder.index = index 538 builder.source = ColorOptionsProvider.COLOR_SOURCE_PRESET 539 builder.type = type 540 val colorFromStub = getItemColorFromStub(COLOR_BUNDLE_MAIN_COLOR_PREFIX, bundleName) 541 var darkColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ true) 542 var lightColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ false) 543 val lightColor = lightColorScheme.accentColor 544 val darkColor = darkColorScheme.accentColor 545 var lightColors = intArrayOf(lightColor, lightColor, lightColor, lightColor) 546 var darkColors = intArrayOf(darkColor, darkColor, darkColor, darkColor) 547 builder.addOverlayPackage(OVERLAY_CATEGORY_COLOR, toColorString(colorFromStub)) 548 builder.addOverlayPackage(OVERLAY_CATEGORY_SYSTEM_PALETTE, toColorString(colorFromStub)) 549 if (style != null) { 550 builder.style = style 551 552 lightColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ false, style) 553 darkColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ true, style) 554 555 when (style) { 556 Style.MONOCHROMATIC -> { 557 darkColors = getRevampedUIDarkMonochromePreview(darkColorScheme) 558 lightColors = getRevampedUILightMonochromePreview(lightColorScheme) 559 } 560 else -> { 561 darkColors = getRevampedUIPresetColorPreview(darkColorScheme, colorFromStub) 562 lightColors = getRevampedUIPresetColorPreview(lightColorScheme, colorFromStub) 563 } 564 } 565 } 566 builder.lightColors = lightColors 567 builder.darkColors = darkColors 568 return builder.build() 569 } 570 } 571