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