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