• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.wallpaper.customization.ui.viewmodel
17 
18 import android.content.Context
19 import android.content.res.Resources
20 import android.graphics.drawable.Drawable
21 import androidx.core.graphics.ColorUtils
22 import com.android.customization.model.color.ColorOption
23 import com.android.customization.model.color.ColorOptionImpl
24 import com.android.customization.module.logging.ThemesUserEventLogger
25 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
26 import com.android.customization.picker.clock.shared.ClockSize
27 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
28 import com.android.customization.picker.clock.ui.viewmodel.ClockColorViewModel
29 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor2
30 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
31 import com.android.systemui.plugins.clocks.AxisPresetConfig
32 import com.android.systemui.plugins.clocks.AxisPresetConfig.IndexedStyle
33 import com.android.systemui.plugins.clocks.ClockAxisStyle
34 import com.android.themepicker.R
35 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
36 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
37 import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel
38 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
39 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2
40 import dagger.assisted.Assisted
41 import dagger.assisted.AssistedFactory
42 import dagger.assisted.AssistedInject
43 import dagger.hilt.android.qualifiers.ApplicationContext
44 import dagger.hilt.android.scopes.ViewModelScoped
45 import kotlin.math.roundToInt
46 import kotlinx.coroutines.CoroutineDispatcher
47 import kotlinx.coroutines.CoroutineScope
48 import kotlinx.coroutines.ExperimentalCoroutinesApi
49 import kotlinx.coroutines.delay
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.SharingStarted
53 import kotlinx.coroutines.flow.StateFlow
54 import kotlinx.coroutines.flow.asStateFlow
55 import kotlinx.coroutines.flow.combine
56 import kotlinx.coroutines.flow.distinctUntilChanged
57 import kotlinx.coroutines.flow.filterNotNull
58 import kotlinx.coroutines.flow.flowOn
59 import kotlinx.coroutines.flow.map
60 import kotlinx.coroutines.flow.mapLatest
61 import kotlinx.coroutines.flow.shareIn
62 import kotlinx.coroutines.flow.stateIn
63 
64 /** View model for the clock customization screen. */
65 class ClockPickerViewModel
66 @AssistedInject
67 constructor(
68     @ApplicationContext context: Context,
69     resources: Resources,
70     private val clockPickerInteractor: ClockPickerInteractor,
71     colorPickerInteractor: ColorPickerInteractor2,
72     private val logger: ThemesUserEventLogger,
73     @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher,
74     @Assisted private val viewModelScope: CoroutineScope,
75 ) {
76 
77     enum class Tab {
78         STYLE,
79         COLOR,
80         SIZE,
81     }
82 
83     private val colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
84 
85     // Tabs
86     private val _selectedTab = MutableStateFlow(Tab.STYLE)
87     val selectedTab: StateFlow<Tab> = _selectedTab.asStateFlow()
88     val tabs: Flow<List<FloatingToolbarTabViewModel>> =
89         selectedTab.map {
90             listOf(
91                 FloatingToolbarTabViewModel(
92                     icon =
93                         Icon.Resource(
94                             res = R.drawable.ic_clock_filled_24px,
95                             contentDescription = Text.Resource(R.string.clock_style),
96                         ),
97                     text = context.getString(R.string.clock_style),
98                     isSelected = it == Tab.STYLE,
99                     onClick =
100                         if (it == Tab.STYLE) null
101                         else {
102                             { _selectedTab.value = Tab.STYLE }
103                         },
104                 ),
105                 FloatingToolbarTabViewModel(
106                     icon =
107                         Icon.Resource(
108                             res = R.drawable.ic_palette_filled_24px,
109                             contentDescription = Text.Resource(R.string.clock_color),
110                         ),
111                     text = context.getString(R.string.clock_color),
112                     isSelected = it == Tab.COLOR,
113                     onClick =
114                         if (it == Tab.COLOR) null
115                         else {
116                             { _selectedTab.value = Tab.COLOR }
117                         },
118                 ),
119                 FloatingToolbarTabViewModel(
120                     icon =
121                         Icon.Resource(
122                             res = R.drawable.ic_font_size_filled_24px,
123                             contentDescription = Text.Resource(R.string.clock_size),
124                         ),
125                     text = context.getString(R.string.clock_size),
126                     isSelected = it == Tab.SIZE,
127                     onClick =
128                         if (it == Tab.SIZE) null
129                         else {
130                             { _selectedTab.value = Tab.SIZE }
131                         },
132                 ),
133             )
134         }
135 
136     // Clock style
137     private val overridingClock = MutableStateFlow<ClockMetadataModel?>(null)
138     val selectedClock = clockPickerInteractor.selectedClock
139     val previewingClock =
140         combine(overridingClock, selectedClock) { overridingClock, selectedClock ->
141                 (overridingClock ?: selectedClock)
142             }
143             .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
144     private val isClockEdited =
145         combine(overridingClock, selectedClock) { overridingClock, selectedClock ->
146             overridingClock != null && overridingClock.clockId != selectedClock.clockId
147         }
148 
149     suspend fun getIsShadeLayoutWide() = clockPickerInteractor.getIsShadeLayoutWide()
150 
151     suspend fun getUdfpsLocation() = clockPickerInteractor.getUdfpsLocation()
152 
153     data class ClockStyleModel(val thumbnail: Drawable)
154 
155     @OptIn(ExperimentalCoroutinesApi::class)
156     val clockStyleOptions: StateFlow<List<OptionItemViewModel2<ClockStyleModel>>> =
157         clockPickerInteractor.allClocks
158             .mapLatest { allClocks ->
159                 // Delay to avoid the case that the full list of clocks is not initiated.
160                 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
161                 val allClockMap = allClocks.groupBy { it.axisPresetConfig != null }
162                 buildList {
163                     allClockMap[true]?.map { add(it.toOption(resources)) }
164                     allClockMap[false]?.map { add(it.toOption(resources)) }
165                 }
166             }
167             // makes sure that the operations above this statement are executed on I/O dispatcher
168             // while parallelism limits the number of threads this can run on which makes sure that
169             // the flows run sequentially
170             .flowOn(backgroundDispatcher.limitedParallelism(1))
171             .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
172 
173     // Clock font presets
174     private val overridingClockPresetIndexedStyle: MutableStateFlow<IndexedStyle?> =
175         MutableStateFlow(null)
176     private val selectedClockPresetIndexedStyle: Flow<IndexedStyle?> =
177         previewingClock
178             .map { it.axisPresetConfig?.current }
179             .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
180     val previewingClockPresetIndexedStyle: Flow<IndexedStyle?> =
181         combine(overridingClockPresetIndexedStyle, selectedClockPresetIndexedStyle) {
182             overridingClockPresetIndexedStyle,
183             selectedClockPresetIndexedStyle ->
184             overridingClockPresetIndexedStyle ?: selectedClockPresetIndexedStyle
185         }
186     private val isClockAxisStyleEdited: Flow<Boolean> =
187         combine(overridingClockPresetIndexedStyle, selectedClockPresetIndexedStyle) {
188             overridingClockPresetIndexedStyle,
189             selectedClockPresetIndexedStyle ->
190             overridingClockPresetIndexedStyle != null &&
191                 (overridingClockPresetIndexedStyle.style != selectedClockPresetIndexedStyle?.style)
192         }
193 
194     private val groups: Flow<List<AxisPresetConfig.Group>?> =
195         previewingClock.map { it.axisPresetConfig?.groups }
196     private val previewingClockPresetGroupIndex: Flow<Int> =
197         previewingClockPresetIndexedStyle.map { it?.groupIndex ?: 0 }.distinctUntilChanged()
198     val shouldShowPresetSlider: Flow<Boolean> = previewingClock.map { it.axisPresetConfig != null }
199     val axisPresetsSliderViewModel: Flow<ClockAxisPresetSliderViewModel?> =
200         combine(groups, previewingClockPresetGroupIndex) { groups, previewingClockPresetGroupIndex
201             ->
202             if (groups.isNullOrEmpty()) {
203                 null
204             } else {
205                 val group = groups[previewingClockPresetGroupIndex]
206                 ClockAxisPresetSliderViewModel(
207                     valueFrom = 0F,
208                     valueTo = (group.presets.size - 1).toFloat(),
209                     stepSize = 1F,
210                     onSliderStopTrackingTouch = { value ->
211                         val presetIndex = value.roundToInt()
212                         overridingClockPresetIndexedStyle.value =
213                             IndexedStyle(
214                                 groupIndex = previewingClockPresetGroupIndex,
215                                 presetIndex = presetIndex,
216                                 style = group.presets[presetIndex],
217                             )
218                     },
219                 )
220             }
221         }
222     val axisPresetsSliderSelectedValue: Flow<Float> =
223         previewingClockPresetIndexedStyle.map { it?.presetIndex?.toFloat() }.filterNotNull()
224     val onClockFaceClicked: Flow<() -> Unit> =
225         combine(groups, previewingClockPresetIndexedStyle) { groups, previewingIndexedStyle ->
226             if (groups.isNullOrEmpty()) {
227                 {}
228             } else {
229                 val groupCount = groups.size
230                 if (groupCount == 1) {
231                     {}
232                 } else {
233                     val currentGroupIndex = previewingIndexedStyle?.groupIndex ?: 0
234                     val nextGroupIndex = (currentGroupIndex + 1) % groupCount
235                     val nextPresetIndex = previewingIndexedStyle?.presetIndex ?: (groupCount / 2)
236                     val nextGroup = groups[nextGroupIndex]
237                     {
238                         overridingClockPresetIndexedStyle.value =
239                             IndexedStyle(
240                                 groupIndex = nextGroupIndex,
241                                 presetIndex = nextPresetIndex,
242                                 style = nextGroup.presets[nextPresetIndex],
243                             )
244                     }
245                 }
246             }
247         }
248 
249     private suspend fun ClockMetadataModel.toOption(
250         resources: Resources
251     ): OptionItemViewModel2<ClockStyleModel> {
252         val isSelectedFlow = previewingClock.map { it.clockId == clockId }.stateIn(viewModelScope)
253         val contentDescription =
254             resources.getString(R.string.select_clock_action_description, description)
255         return OptionItemViewModel2<ClockStyleModel>(
256             key = MutableStateFlow(clockId) as StateFlow<String>,
257             payload = ClockStyleModel(thumbnail = thumbnail),
258             text = Text.Loaded(contentDescription),
259             isTextUserVisible = false,
260             isSelected = isSelectedFlow,
261             onClicked =
262                 isSelectedFlow.map { isSelected ->
263                     if (isSelected) {
264                         null
265                     } else {
266                         fun() {
267                             overridingClock.value = this
268                         }
269                     }
270                 },
271         )
272     }
273 
274     // Clock size
275     private val overridingClockSize = MutableStateFlow<ClockSize?>(null)
276     private val isClockSizeEdited =
277         combine(overridingClockSize, clockPickerInteractor.selectedClockSize) {
278             overridingClockSize,
279             selectedClockSize ->
280             overridingClockSize != null && overridingClockSize != selectedClockSize
281         }
282     val previewingClockSize =
283         combine(overridingClockSize, clockPickerInteractor.selectedClockSize) {
284             overridingClockSize,
285             selectedClockSize ->
286             overridingClockSize ?: selectedClockSize
287         }
288     val onClockSizeSwitchCheckedChange: Flow<(() -> Unit)> =
289         previewingClockSize.map {
290             {
291                 when (it) {
292                     ClockSize.DYNAMIC -> overridingClockSize.value = ClockSize.SMALL
293                     ClockSize.SMALL -> overridingClockSize.value = ClockSize.DYNAMIC
294                 }
295             }
296         }
297 
298     // Clock color
299     private val overridingClockColorId = MutableStateFlow<String?>(null)
300     private val isClockColorIdEdited =
301         combine(overridingClockColorId, clockPickerInteractor.selectedColorId) {
302             overridingClockColorId,
303             selectedColorId ->
304             overridingClockColorId != null && (overridingClockColorId != selectedColorId)
305         }
306     private val previewingClockColorId =
307         combine(overridingClockColorId, clockPickerInteractor.selectedColorId) {
308             overridingClockColorId,
309             selectedColorId ->
310             overridingClockColorId ?: selectedColorId ?: DEFAULT_CLOCK_COLOR_ID
311         }
312 
313     // Clock color slider progress. Range is 0 - 100.
314     private val overridingSliderProgress = MutableStateFlow<Int?>(null)
315     private val isSliderProgressEdited =
316         combine(overridingSliderProgress, clockPickerInteractor.colorToneProgress) {
317             overridingSliderProgress,
318             colorToneProgress ->
319             overridingSliderProgress != null && (overridingSliderProgress != colorToneProgress)
320         }
321     val previewingSliderProgress: Flow<Int> =
322         combine(overridingSliderProgress, clockPickerInteractor.colorToneProgress) {
323             overridingSliderProgress,
324             colorToneProgress ->
325             overridingSliderProgress ?: colorToneProgress
326         }
327     val isSliderEnabled: Flow<Boolean> =
328         combine(previewingClock, previewingClockColorId) { clock, clockColorId ->
329                 clock.isReactiveToTone && clockColorId != DEFAULT_CLOCK_COLOR_ID
330             }
331             .distinctUntilChanged()
332 
333     fun onSliderProgressChanged(progress: Int) {
334         overridingSliderProgress.value = progress
335     }
336 
337     val previewingSeedColor: Flow<Int?> =
338         combine(previewingClockColorId, previewingSliderProgress) { clockColorId, sliderProgress ->
339             val clockColorViewModel =
340                 if (clockColorId == DEFAULT_CLOCK_COLOR_ID) null else colorMap[clockColorId]
341             if (clockColorViewModel == null) {
342                 null
343             } else {
344                 blendColorWithTone(
345                     color = clockColorViewModel.color,
346                     colorTone = clockColorViewModel.getColorTone(sliderProgress),
347                 )
348             }
349         }
350 
351     val clockColorOptions: Flow<List<OptionItemViewModel2<ColorOptionIconViewModel>>> =
352         colorPickerInteractor.selectedColorOption.map { selectedColorOption ->
353             // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
354             // events from ClockRegistry upstream, caused by sliding the saturation level bar.
355             delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
356             buildList {
357                 selectedColorOption?.let { add(it.toOptionItemViewModel(context)) }
358 
359                 colorMap.values.forEachIndexed { index, colorModel ->
360                     val isSelectedFlow =
361                         previewingClockColorId
362                             .map { colorMap.keys.indexOf(it) == index }
363                             .stateIn(viewModelScope)
364                     add(
365                         OptionItemViewModel2<ColorOptionIconViewModel>(
366                             key = MutableStateFlow(colorModel.colorId) as StateFlow<String>,
367                             payload =
368                                 ColorOptionIconViewModel(
369                                     lightThemeColor0 = colorModel.color,
370                                     lightThemeColor1 = colorModel.color,
371                                     lightThemeColor2 = colorModel.color,
372                                     lightThemeColor3 = colorModel.color,
373                                     darkThemeColor0 = colorModel.color,
374                                     darkThemeColor1 = colorModel.color,
375                                     darkThemeColor2 = colorModel.color,
376                                     darkThemeColor3 = colorModel.color,
377                                 ),
378                             text =
379                                 Text.Loaded(
380                                     context.getString(
381                                         R.string.content_description_color_option,
382                                         index,
383                                     )
384                                 ),
385                             isTextUserVisible = false,
386                             isSelected = isSelectedFlow,
387                             onClicked =
388                                 isSelectedFlow.map { isSelected ->
389                                     if (isSelected) {
390                                         null
391                                     } else {
392                                         {
393                                             overridingClockColorId.value = colorModel.colorId
394                                             overridingSliderProgress.value =
395                                                 ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
396                                         }
397                                     }
398                                 },
399                         )
400                     )
401                 }
402             }
403         }
404 
405     private suspend fun ColorOption.toOptionItemViewModel(
406         context: Context
407     ): OptionItemViewModel2<ColorOptionIconViewModel> {
408         val lightThemeColors =
409             (this as ColorOptionImpl)
410                 .previewInfo
411                 .resolveColors(
412                     /** darkTheme= */
413                     false
414                 )
415         val darkThemeColors =
416             this.previewInfo.resolveColors(
417                 /** darkTheme= */
418                 true
419             )
420         val isSelectedFlow =
421             previewingClockColorId.map { it == DEFAULT_CLOCK_COLOR_ID }.stateIn(viewModelScope)
422         val key = "${this.type}::${this.style}::${this.serializedPackages}"
423         return OptionItemViewModel2<ColorOptionIconViewModel>(
424             key = MutableStateFlow(key) as StateFlow<String>,
425             payload =
426                 ColorOptionIconViewModel(
427                     lightThemeColor0 = lightThemeColors[0],
428                     lightThemeColor1 = lightThemeColors[1],
429                     lightThemeColor2 = lightThemeColors[2],
430                     lightThemeColor3 = lightThemeColors[3],
431                     darkThemeColor0 = darkThemeColors[0],
432                     darkThemeColor1 = darkThemeColors[1],
433                     darkThemeColor2 = darkThemeColors[2],
434                     darkThemeColor3 = darkThemeColors[3],
435                 ),
436             text = Text.Loaded(context.getString(R.string.default_theme_title)),
437             isTextUserVisible = true,
438             isSelected = isSelectedFlow,
439             onClicked =
440                 isSelectedFlow.map { isSelected ->
441                     if (isSelected) {
442                         null
443                     } else {
444                         {
445                             overridingClockColorId.value = DEFAULT_CLOCK_COLOR_ID
446                             overridingSliderProgress.value =
447                                 ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
448                         }
449                     }
450                 },
451         )
452     }
453 
454     private val isEdited =
455         combine(
456             isClockEdited,
457             isClockAxisStyleEdited,
458             isClockSizeEdited,
459             isClockColorIdEdited,
460             isSliderProgressEdited,
461         ) {
462             isClockEdited,
463             isClockAxisStyleEdited,
464             isClockSizeEdited,
465             isClockColorEdited,
466             isSliderProgressEdited ->
467             isClockEdited ||
468                 isClockAxisStyleEdited ||
469                 isClockSizeEdited ||
470                 isClockColorEdited ||
471                 isSliderProgressEdited
472         }
473     private val onApplyClicked: MutableStateFlow<Boolean> = MutableStateFlow(false)
474     val onApply: Flow<(suspend () -> Unit)?> =
475         combine(
476             onApplyClicked,
477             isEdited,
478             previewingClock,
479             previewingClockSize,
480             previewingClockColorId,
481             previewingSliderProgress,
482             previewingClockPresetIndexedStyle,
483         ) { array ->
484             val onApplyClicked: Boolean = array[0] as Boolean
485             val isEdited: Boolean = array[1] as Boolean
486             val clock: ClockMetadataModel = array[2] as ClockMetadataModel
487             val size: ClockSize = array[3] as ClockSize
488             val previewingColorId: String = array[4] as String
489             val previewProgress: Int = array[5] as Int
490             val clockAxisStyle: ClockAxisStyle =
491                 (array[6] as? IndexedStyle)?.style ?: ClockAxisStyle()
492             if (isEdited && !onApplyClicked) {
493                 {
494                     this.onApplyClicked.value = true
495                     clockPickerInteractor.applyClock(
496                         clockId = clock.clockId,
497                         size = size,
498                         selectedColorId = previewingColorId,
499                         colorToneProgress = previewProgress,
500                         seedColor =
501                             colorMap[previewingColorId]?.let {
502                                 blendColorWithTone(
503                                     color = it.color,
504                                     colorTone = it.getColorTone(previewProgress),
505                                 )
506                             },
507                         axisSettings = clockAxisStyle,
508                     )
509                 }
510             } else {
511                 null
512             }
513         }
514 
515     fun resetPreview() {
516         overridingClock.value = null
517         overridingClockSize.value = null
518         overridingClockColorId.value = null
519         overridingSliderProgress.value = null
520         overridingClockPresetIndexedStyle.value = null
521         _selectedTab.value = Tab.STYLE
522         onApplyClicked.value = false
523     }
524 
525     companion object {
526         private const val DEFAULT_CLOCK_COLOR_ID = "DEFAULT"
527         private val helperColorLab: DoubleArray by lazy { DoubleArray(3) }
528 
529         fun blendColorWithTone(color: Int, colorTone: Double): Int {
530             ColorUtils.colorToLAB(color, helperColorLab)
531             return ColorUtils.LABToColor(colorTone, helperColorLab[1], helperColorLab[2])
532         }
533 
534         const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
535         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
536     }
537 
538     @ViewModelScoped
539     @AssistedFactory
540     interface Factory {
541         fun create(viewModelScope: CoroutineScope): ClockPickerViewModel
542     }
543 }
544