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