• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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