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.themepicker.R 42 import com.android.wallpaper.config.BaseFlags 43 import com.android.wallpaper.module.InjectorProvider 44 import kotlinx.coroutines.CoroutineScope 45 import kotlinx.coroutines.Dispatchers 46 import kotlinx.coroutines.Job 47 import kotlinx.coroutines.SupervisorJob 48 import kotlinx.coroutines.launch 49 import kotlinx.coroutines.withContext 50 51 /** 52 * Default implementation of {@link ColorOptionsProvider} that reads preset colors from a stub APK. 53 * TODO (b/311212666): Make [ColorProvider] and [ColorCustomizationManager] injectable 54 */ 55 class ColorProvider(private val context: Context, stubPackageName: String) : 56 ResourcesApkProvider(context, stubPackageName), ColorOptionsProvider { 57 58 companion object { 59 const val themeStyleEnabled = true 60 val styleSize = if (themeStyleEnabled) Style.values().size else 1 61 private const val TAG = "ColorProvider" 62 private const val MAX_SEED_COLORS = 4 63 private const val MAX_PRESET_COLORS = 4 64 private const val ALPHA_MASK = 0xFF 65 } 66 67 private var loaderJob: Job? = null 68 private val monetEnabled = ColorUtils.isMonetEnabled(context) 69 // TODO(b/202145216): Use style method to fetch the list of style. 70 @Style.Type 71 private var styleList = 72 if (themeStyleEnabled) 73 arrayOf(Style.TONAL_SPOT, Style.SPRITZ, Style.VIBRANT, Style.EXPRESSIVE) 74 else arrayOf(Style.TONAL_SPOT) 75 76 private var monochromeBundleName: String? = null 77 78 private val scope = 79 if (mContext is LifecycleOwner) { 80 mContext.lifecycleScope 81 } else { 82 CoroutineScope(Dispatchers.Default + SupervisorJob()) 83 } 84 85 private var colorsAvailable = true 86 private var presetColorBundles: List<ColorOption>? = null 87 private var wallpaperColorBundles: List<ColorOption>? = null 88 private var homeWallpaperColors: WallpaperColors? = null 89 private var lockWallpaperColors: WallpaperColors? = null 90 isAvailablenull91 override fun isAvailable(): Boolean { 92 return monetEnabled && super.isAvailable() && colorsAvailable 93 } 94 fetchnull95 override fun fetch( 96 callback: OptionsFetchedListener<ColorOption>?, 97 reload: Boolean, 98 homeWallpaperColors: WallpaperColors?, 99 lockWallpaperColors: WallpaperColors?, 100 ) { 101 val isNewPickerUi = BaseFlags.get().isNewPickerUi() 102 if (isNewPickerUi) { 103 val wallpaperColorsChanged = this.homeWallpaperColors != homeWallpaperColors 104 if (wallpaperColorsChanged || reload) { 105 loadSeedColors(homeWallpaperColors) 106 this.homeWallpaperColors = homeWallpaperColors 107 } 108 } else { 109 val wallpaperColorsChanged = 110 this.homeWallpaperColors != homeWallpaperColors || 111 this.lockWallpaperColors != lockWallpaperColors 112 if (wallpaperColorsChanged || reload) { 113 loadSeedColors(homeWallpaperColors, lockWallpaperColors) 114 this.homeWallpaperColors = homeWallpaperColors 115 this.lockWallpaperColors = lockWallpaperColors 116 } 117 } 118 119 scope.launch { 120 // Wait for the previous preset color loading job to finish before evaluating whether to 121 // start a new one 122 loaderJob?.join() 123 if (presetColorBundles == null || reload) { 124 loaderJob = launch { 125 try { 126 loadPreset(isNewPickerUi) 127 callback?.onOptionsLoaded(buildFinalList(isNewPickerUi)) 128 } catch (e: Throwable) { 129 colorsAvailable = false 130 callback?.onError(e) 131 } 132 } 133 } else { 134 callback?.onOptionsLoaded(buildFinalList(isNewPickerUi)) 135 } 136 } 137 } 138 isLockScreenWallpaperLastAppliednull139 private fun isLockScreenWallpaperLastApplied(): Boolean { 140 // The WallpaperId increases every time a new wallpaper is set, so the larger wallpaper id 141 // is the most recently set wallpaper 142 val manager = WallpaperManager.getInstance(mContext) 143 return manager.getWallpaperId(WallpaperManager.FLAG_LOCK) > 144 manager.getWallpaperId(WallpaperManager.FLAG_SYSTEM) 145 } 146 loadSeedColorsnull147 private fun loadSeedColors( 148 homeWallpaperColors: WallpaperColors?, 149 lockWallpaperColors: WallpaperColors? = null, 150 ) { 151 if (homeWallpaperColors == null) return 152 153 val bundles: MutableList<ColorOption> = ArrayList() 154 val colorsPerSource = 155 if (lockWallpaperColors == null) { 156 MAX_SEED_COLORS 157 } else { 158 MAX_SEED_COLORS / 2 159 } 160 161 if (lockWallpaperColors != null) { 162 val shouldLockColorsGoFirst = isLockScreenWallpaperLastApplied() 163 // First half of the colors 164 buildColorSeeds( 165 if (shouldLockColorsGoFirst) lockWallpaperColors else homeWallpaperColors, 166 colorsPerSource, 167 if (shouldLockColorsGoFirst) COLOR_SOURCE_LOCK else COLOR_SOURCE_HOME, 168 true, 169 bundles, 170 ) 171 // Second half of the colors 172 buildColorSeeds( 173 if (shouldLockColorsGoFirst) homeWallpaperColors else lockWallpaperColors, 174 MAX_SEED_COLORS - bundles.size / styleSize, 175 if (shouldLockColorsGoFirst) COLOR_SOURCE_HOME else COLOR_SOURCE_LOCK, 176 false, 177 bundles, 178 ) 179 } else { 180 buildColorSeeds(homeWallpaperColors, colorsPerSource, COLOR_SOURCE_HOME, true, bundles) 181 } 182 wallpaperColorBundles = bundles 183 } 184 buildColorSeedsnull185 private fun buildColorSeeds( 186 wallpaperColors: WallpaperColors, 187 maxColors: Int, 188 source: String, 189 containsDefault: Boolean, 190 bundles: MutableList<ColorOption>, 191 ) { 192 val seedColors = ColorScheme.getSeedColors(wallpaperColors) 193 val defaultSeed = seedColors.first() 194 buildBundle(defaultSeed, 0, containsDefault, source, bundles) 195 for ((i, colorInt) in seedColors.drop(1).take(maxColors - 1).withIndex()) { 196 buildBundle(colorInt, i + 1, false, source, bundles) 197 } 198 } 199 buildBundlenull200 private fun buildBundle( 201 colorInt: Int, 202 i: Int, 203 isDefault: Boolean, 204 source: String, 205 bundles: MutableList<ColorOption>, 206 ) { 207 // TODO(b/202145216): Measure time cost in the loop. 208 for (style in styleList) { 209 val lightColorScheme = ColorScheme(colorInt, /* darkTheme= */ false, style) 210 val darkColorScheme = ColorScheme(colorInt, /* darkTheme= */ true, style) 211 val builder = ColorOptionImpl.Builder() 212 builder.lightColors = getLightColorPreview(lightColorScheme) 213 builder.darkColors = getDarkColorPreview(darkColorScheme) 214 builder.seedColor = colorInt 215 builder.addOverlayPackage( 216 OVERLAY_CATEGORY_SYSTEM_PALETTE, 217 if (isDefault) "" else toColorString(colorInt), 218 ) 219 builder.title = 220 when (style) { 221 Style.TONAL_SPOT -> 222 context.getString(R.string.content_description_dynamic_color_option) 223 Style.SPRITZ -> 224 context.getString(R.string.content_description_neutral_color_option) 225 Style.VIBRANT -> 226 context.getString(R.string.content_description_vibrant_color_option) 227 Style.EXPRESSIVE -> 228 context.getString(R.string.content_description_expressive_color_option) 229 else -> context.getString(R.string.content_description_dynamic_color_option) 230 } 231 builder.source = source 232 builder.style = style 233 // Color option index value starts from 1. 234 builder.index = i + 1 235 builder.isDefault = isDefault 236 builder.type = ColorType.WALLPAPER_COLOR 237 bundles.add(builder.build()) 238 } 239 } 240 241 /** 242 * Returns the light theme preview of a dynamic ColorScheme based on this order: top left, top 243 * right, bottom left, bottom right 244 * 245 * This color mapping corresponds to GM3 colors: Primary (light), Primary (light), Secondary 246 * LStar 85, and Tertiary LStar 70 247 */ 248 @ColorInt getLightColorPreviewnull249 private fun getLightColorPreview(colorScheme: ColorScheme): IntArray { 250 return intArrayOf( 251 setAlphaComponent(colorScheme.accent1.s600, ALPHA_MASK), 252 setAlphaComponent(colorScheme.accent1.s600, ALPHA_MASK), 253 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(85f).colors[0], 254 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 255 ) 256 } 257 258 /** 259 * Returns the dark theme preview of a dynamic ColorScheme based on this order: top left, top 260 * right, bottom left, bottom right 261 * 262 * This color mapping corresponds to GM3 colors: Primary (dark), Primary (dark), Secondary LStar 263 * 35, and Tertiary LStar 70 264 */ 265 @ColorInt getDarkColorPreviewnull266 private fun getDarkColorPreview(colorScheme: ColorScheme): IntArray { 267 return intArrayOf( 268 setAlphaComponent(colorScheme.accent1.s200, ALPHA_MASK), 269 setAlphaComponent(colorScheme.accent1.s200, ALPHA_MASK), 270 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(35f).colors[0], 271 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 272 ) 273 } 274 275 /** 276 * Returns the light theme preview of a monochrome ColorScheme based on this order: top left, 277 * top right, bottom left, bottom right 278 * 279 * This color mapping corresponds to GM3 colors: Primary LStar 0, Primary LStar 0, Secondary 280 * LStar 85, and Tertiary LStar 70 281 */ 282 @ColorInt getLightMonochromePreviewnull283 private fun getLightMonochromePreview(colorScheme: ColorScheme): IntArray { 284 return intArrayOf( 285 setAlphaComponent(colorScheme.accent1.s1000, ALPHA_MASK), 286 setAlphaComponent(colorScheme.accent1.s1000, ALPHA_MASK), 287 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(85f).colors[0], 288 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 289 ) 290 } 291 292 /** 293 * Returns the dark theme preview of a monochrome ColorScheme based on this order: top left, top 294 * right, bottom left, bottom right 295 * 296 * This color mapping corresponds to GM3 colors: Primary LStar 99, Primary LStar 99, Secondary 297 * LStar 35, and Tertiary LStar 70 298 */ 299 @ColorInt getDarkMonochromePreviewnull300 private fun getDarkMonochromePreview(colorScheme: ColorScheme): IntArray { 301 return intArrayOf( 302 setAlphaComponent(colorScheme.accent1.s10, ALPHA_MASK), 303 setAlphaComponent(colorScheme.accent1.s10, ALPHA_MASK), 304 ColorStateList.valueOf(colorScheme.accent2.s500).withLStar(35f).colors[0], 305 setAlphaComponent(colorScheme.accent3.s300, ALPHA_MASK), 306 ) 307 } 308 309 /** 310 * Returns the light theme contrast-adjusted preview of a preset ColorScheme, based on this 311 * order: top left, top right, bottom left, bottom right 312 */ getDarkPresetColorPreviewnull313 private fun getDarkPresetColorPreview(colorScheme: ColorScheme): IntArray { 314 val colors = 315 when (colorScheme.style) { 316 Style.FRUIT_SALAD -> intArrayOf(colorScheme.accent3.s100, colorScheme.accent1.s200) 317 else -> intArrayOf(colorScheme.accent1.s200, colorScheme.accent1.s200) 318 } 319 return intArrayOf(colors[0], colors[1], colors[0], colors[1]) 320 } 321 322 /** 323 * Returns the preview of a preset ColorScheme based on this order: top left, top right, bottom 324 * left, bottom right 325 */ getFixedPresetColorPreviewnull326 private fun getFixedPresetColorPreview(colorScheme: ColorScheme, seed: Int): IntArray { 327 val colors = 328 when (colorScheme.style) { 329 Style.FRUIT_SALAD -> intArrayOf(colorScheme.accent3.s100, colorScheme.accent1.s200) 330 Style.RAINBOW -> intArrayOf(colorScheme.accent1.s200, colorScheme.accent1.s200) 331 else -> intArrayOf(seed, seed) 332 } 333 return intArrayOf(colors[0], colors[1], colors[0], colors[1]) 334 } 335 336 /** 337 * Returns the light theme contrast-adjusted preview of a preset ColorScheme, based on this 338 * order: top left, top right, bottom left, bottom right 339 */ getLightPresetColorPreviewnull340 private fun getLightPresetColorPreview(colorScheme: ColorScheme): IntArray { 341 val colors = 342 when (colorScheme.style) { 343 Style.FRUIT_SALAD -> 344 intArrayOf( 345 colorScheme.accent3.getAtTone(450f), 346 colorScheme.accent1.getAtTone(550f), 347 ) 348 else -> 349 intArrayOf( 350 colorScheme.accent1.getAtTone(450f), 351 colorScheme.accent1.getAtTone(450f), 352 ) 353 } 354 return intArrayOf(colors[0], colors[1], colors[0], colors[1]) 355 } 356 loadPresetnull357 private suspend fun loadPreset(isNewPickerUi: Boolean) = 358 withContext(Dispatchers.IO) { 359 val bundles: MutableList<ColorOption> = ArrayList() 360 361 val bundleNames = 362 if (isAvailable) getItemsFromStub(COLOR_BUNDLES_ARRAY_NAME) else emptyArray() 363 // Color option index value starts from 1. 364 var index = 1 365 val maxPresetColors = if (themeStyleEnabled) bundleNames.size else MAX_PRESET_COLORS 366 367 // keep track of whether monochrome is included in preset colors to determine 368 // inclusion in wallpaper colors 369 var hasMonochrome = false 370 for (bundleName in bundleNames.take(maxPresetColors)) { 371 if (themeStyleEnabled) { 372 val styleName = 373 try { 374 getItemStringFromStub(COLOR_BUNDLE_STYLE_PREFIX, bundleName) 375 } catch (e: Resources.NotFoundException) { 376 null 377 } 378 @Style.Type 379 val style = 380 try { 381 if (styleName != null) Style.valueOf(styleName) else Style.TONAL_SPOT 382 } catch (e: IllegalArgumentException) { 383 Style.TONAL_SPOT 384 } 385 386 if (style == Style.MONOCHROMATIC) { 387 if ( 388 !InjectorProvider.getInjector() 389 .getFlags() 390 .isMonochromaticThemeEnabled(mContext) 391 ) { 392 continue 393 } 394 hasMonochrome = true 395 monochromeBundleName = bundleName 396 } 397 bundles.add( 398 buildPreset( 399 bundleName = bundleName, 400 index = index, 401 style = style, 402 isNewPickerUi = isNewPickerUi, 403 ) 404 ) 405 } else { 406 bundles.add( 407 buildPreset( 408 bundleName = bundleName, 409 index = index, 410 style = null, 411 isNewPickerUi = isNewPickerUi, 412 ) 413 ) 414 } 415 416 index++ 417 } 418 if (!hasMonochrome) { 419 monochromeBundleName = null 420 } 421 422 presetColorBundles = bundles 423 loaderJob = null 424 } 425 buildPresetnull426 private fun buildPreset( 427 bundleName: String, 428 index: Int, 429 @Style.Type style: Int? = null, 430 type: ColorType = ColorType.PRESET_COLOR, 431 isNewPickerUi: Boolean, 432 ): ColorOptionImpl { 433 val builder = ColorOptionImpl.Builder() 434 builder.title = getItemStringFromStub(COLOR_BUNDLE_NAME_PREFIX, bundleName) 435 builder.index = index 436 builder.source = ColorOptionsProvider.COLOR_SOURCE_PRESET 437 builder.type = type 438 val colorFromStub = getItemColorFromStub(COLOR_BUNDLE_MAIN_COLOR_PREFIX, bundleName) 439 var darkColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ true) 440 var lightColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ false) 441 val lightColor = lightColorScheme.accentColor 442 val darkColor = darkColorScheme.accentColor 443 var lightColors = intArrayOf(lightColor, lightColor, lightColor, lightColor) 444 var darkColors = intArrayOf(darkColor, darkColor, darkColor, darkColor) 445 builder.seedColor = colorFromStub 446 builder.addOverlayPackage(OVERLAY_CATEGORY_COLOR, toColorString(colorFromStub)) 447 builder.addOverlayPackage(OVERLAY_CATEGORY_SYSTEM_PALETTE, toColorString(colorFromStub)) 448 if (style != null) { 449 builder.style = style 450 451 lightColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ false, style) 452 darkColorScheme = ColorScheme(colorFromStub, /* darkTheme= */ true, style) 453 454 when (style) { 455 Style.MONOCHROMATIC -> { 456 darkColors = getDarkMonochromePreview(darkColorScheme) 457 lightColors = getLightMonochromePreview(lightColorScheme) 458 } 459 else -> { 460 darkColors = 461 if (isNewPickerUi) { 462 getFixedPresetColorPreview(darkColorScheme, colorFromStub) 463 } else { 464 getDarkPresetColorPreview(darkColorScheme) 465 } 466 lightColors = 467 if (isNewPickerUi) { 468 getFixedPresetColorPreview(lightColorScheme, colorFromStub) 469 } else { 470 getLightPresetColorPreview(lightColorScheme) 471 } 472 } 473 } 474 } 475 builder.lightColors = lightColors 476 builder.darkColors = darkColors 477 return builder.build() 478 } 479 buildFinalListnull480 private fun buildFinalList(isNewPickerUi: Boolean): List<ColorOption> { 481 val presetColors = presetColorBundles ?: emptyList() 482 val wallpaperColors = wallpaperColorBundles?.toMutableList() ?: mutableListOf() 483 // Insert monochrome in the second position if it is enabled and included in preset 484 // colors 485 if (InjectorProvider.getInjector().getFlags().isMonochromaticThemeEnabled(mContext)) { 486 monochromeBundleName?.let { 487 if (wallpaperColors.isNotEmpty()) { 488 wallpaperColors.add( 489 1, 490 buildPreset( 491 bundleName = it, 492 index = -1, 493 style = Style.MONOCHROMATIC, 494 type = ColorType.WALLPAPER_COLOR, 495 isNewPickerUi = isNewPickerUi, 496 ), 497 ) 498 } 499 } 500 } 501 return wallpaperColors + presetColors 502 } 503 } 504