• 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.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