1 /*
<lambda>null2  * Copyright (C) 2023 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.picker.clock.ui.viewmodel
17 
18 import android.content.Context
19 import androidx.core.graphics.ColorUtils
20 import androidx.lifecycle.ViewModel
21 import androidx.lifecycle.ViewModelProvider
22 import androidx.lifecycle.viewModelScope
23 import com.android.customization.model.color.ColorOptionImpl
24 import com.android.customization.module.logging.ThemesUserEventLogger
25 import com.android.customization.module.logging.ThemesUserEventLogger.Companion.NULL_SEED_COLOR
26 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
27 import com.android.customization.picker.clock.shared.ClockSize
28 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
29 import com.android.customization.picker.clock.shared.toClockSizeForLogging
30 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
31 import com.android.customization.picker.color.shared.model.ColorOptionModel
32 import com.android.customization.picker.color.shared.model.ColorType
33 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
34 import com.android.themepicker.R
35 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
36 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.delay
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.MutableStateFlow
41 import kotlinx.coroutines.flow.SharingStarted
42 import kotlinx.coroutines.flow.StateFlow
43 import kotlinx.coroutines.flow.asStateFlow
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.distinctUntilChanged
46 import kotlinx.coroutines.flow.flatMapLatest
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.merge
49 import kotlinx.coroutines.flow.stateIn
50 import kotlinx.coroutines.launch
51 
52 /** View model for the clock settings screen. */
53 class ClockSettingsViewModel
54 private constructor(
55     context: Context,
56     private val clockPickerInteractor: ClockPickerInteractor,
57     private val colorPickerInteractor: ColorPickerInteractor,
58     private val getIsReactiveToTone: (clockId: String?) -> Boolean,
59     private val logger: ThemesUserEventLogger,
60 ) : ViewModel() {
61 
62     enum class Tab {
63         COLOR,
64         SIZE,
65     }
66 
67     private val colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
68 
69     val selectedClockId: StateFlow<String?> =
70         clockPickerInteractor.selectedClockId
71             .distinctUntilChanged()
72             .stateIn(viewModelScope, SharingStarted.Eagerly, null)
73 
74     private val selectedColorId: StateFlow<String?> =
75         clockPickerInteractor.selectedColorId.stateIn(viewModelScope, SharingStarted.Eagerly, null)
76 
77     private val sliderColorToneProgress =
78         MutableStateFlow(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
79     val isSliderEnabled: Flow<Boolean> =
80         combine(selectedClockId, clockPickerInteractor.selectedColorId) { clockId, colorId ->
81                 if (colorId == null) {
82                     false
83                 } else {
84                     getIsReactiveToTone(clockId)
85                 }
86             }
87             .distinctUntilChanged()
88     val sliderProgress: Flow<Int> =
89         merge(clockPickerInteractor.colorToneProgress, sliderColorToneProgress)
90 
91     private val _seedColor: MutableStateFlow<Int?> = MutableStateFlow(null)
92     val seedColor: Flow<Int?> = merge(clockPickerInteractor.seedColor, _seedColor)
93 
94     /**
95      * The slider color tone updates are quick. Do not set color tone and the blended color to the
96      * settings until [onSliderProgressStop] is called. Update to a locally cached temporary
97      * [sliderColorToneProgress] and [_seedColor] instead.
98      */
99     fun onSliderProgressChanged(progress: Int) {
100         sliderColorToneProgress.value = progress
101         val selectedColorId = selectedColorId.value ?: return
102         val clockColorViewModel = colorMap[selectedColorId] ?: return
103         _seedColor.value =
104             blendColorWithTone(
105                 color = clockColorViewModel.color,
106                 colorTone = clockColorViewModel.getColorTone(progress),
107             )
108     }
109 
110     suspend fun onSliderProgressStop(progress: Int) {
111         val selectedColorId = selectedColorId.value ?: return
112         val clockColorViewModel = colorMap[selectedColorId] ?: return
113         val seedColor =
114             blendColorWithTone(
115                 color = clockColorViewModel.color,
116                 colorTone = clockColorViewModel.getColorTone(progress),
117             )
118         clockPickerInteractor.setClockColor(
119             selectedColorId = selectedColorId,
120             colorToneProgress = progress,
121             seedColor = seedColor,
122         )
123         logger.logClockColorApplied(seedColor)
124     }
125 
126     @OptIn(ExperimentalCoroutinesApi::class)
127     val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
128         colorPickerInteractor.colorOptions.map { colorOptions ->
129             // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
130             // events from ClockRegistry upstream, caused by sliding the saturation level bar.
131             delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
132             buildList {
133                 val defaultThemeColorOptionViewModel =
134                     (colorOptions[ColorType.WALLPAPER_COLOR]?.find { it.isSelected })
135                         ?.toOptionItemViewModel(context)
136                         ?: (colorOptions[ColorType.PRESET_COLOR]?.find { it.isSelected })
137                             ?.toOptionItemViewModel(context)
138                 if (defaultThemeColorOptionViewModel != null) {
139                     add(defaultThemeColorOptionViewModel)
140                 }
141 
142                 colorMap.values.forEachIndexed { index, colorModel ->
143                     val isSelectedFlow =
144                         selectedColorId
145                             .map { colorMap.keys.indexOf(it) == index }
146                             .stateIn(viewModelScope)
147                     val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
148                     add(
149                         OptionItemViewModel<ColorOptionIconViewModel>(
150                             key = MutableStateFlow(colorModel.colorId) as StateFlow<String>,
151                             payload =
152                                 ColorOptionIconViewModel(
153                                     lightThemeColor0 = colorModel.color,
154                                     lightThemeColor1 = colorModel.color,
155                                     lightThemeColor2 = colorModel.color,
156                                     lightThemeColor3 = colorModel.color,
157                                     darkThemeColor0 = colorModel.color,
158                                     darkThemeColor1 = colorModel.color,
159                                     darkThemeColor2 = colorModel.color,
160                                     darkThemeColor3 = colorModel.color,
161                                 ),
162                             text =
163                                 Text.Loaded(
164                                     context.getString(
165                                         R.string.content_description_color_option,
166                                         index,
167                                     )
168                                 ),
169                             isTextUserVisible = false,
170                             isSelected = isSelectedFlow,
171                             onClicked =
172                                 isSelectedFlow.map { isSelected ->
173                                     if (isSelected) {
174                                         null
175                                     } else {
176                                         {
177                                             viewModelScope.launch {
178                                                 val seedColor =
179                                                     blendColorWithTone(
180                                                         color = colorModel.color,
181                                                         colorTone =
182                                                             colorModel.getColorTone(
183                                                                 colorToneProgress,
184                                                             ),
185                                                     )
186                                                 clockPickerInteractor.setClockColor(
187                                                     selectedColorId = colorModel.colorId,
188                                                     colorToneProgress = colorToneProgress,
189                                                     seedColor = seedColor,
190                                                 )
191                                                 logger.logClockColorApplied(seedColor)
192                                             }
193                                         }
194                                     }
195                                 },
196                         )
197                     )
198                 }
199             }
200         }
201 
202     @OptIn(ExperimentalCoroutinesApi::class)
203     val selectedColorOptionPosition: Flow<Int> =
204         colorOptions.flatMapLatest { colorOptions ->
205             combine(colorOptions.map { colorOption -> colorOption.isSelected }) { selectedFlags ->
206                 selectedFlags.indexOfFirst { it }
207             }
208         }
209 
210     private suspend fun ColorOptionModel.toOptionItemViewModel(
211         context: Context
212     ): OptionItemViewModel<ColorOptionIconViewModel> {
213         val lightThemeColors =
214             (colorOption as ColorOptionImpl)
215                 .previewInfo
216                 .resolveColors(
217                     /** darkTheme= */
218                     false
219                 )
220         val darkThemeColors =
221             colorOption.previewInfo.resolveColors(
222                 /** darkTheme= */
223                 true
224             )
225         val isSelectedFlow = selectedColorId.map { it == null }.stateIn(viewModelScope)
226         return OptionItemViewModel<ColorOptionIconViewModel>(
227             key = MutableStateFlow(key) as StateFlow<String>,
228             payload =
229                 ColorOptionIconViewModel(
230                     lightThemeColor0 = lightThemeColors[0],
231                     lightThemeColor1 = lightThemeColors[1],
232                     lightThemeColor2 = lightThemeColors[2],
233                     lightThemeColor3 = lightThemeColors[3],
234                     darkThemeColor0 = darkThemeColors[0],
235                     darkThemeColor1 = darkThemeColors[1],
236                     darkThemeColor2 = darkThemeColors[2],
237                     darkThemeColor3 = darkThemeColors[3],
238                 ),
239             text = Text.Loaded(context.getString(R.string.default_theme_title)),
240             isTextUserVisible = true,
241             isSelected = isSelectedFlow,
242             onClicked =
243                 isSelectedFlow.map { isSelected ->
244                     if (isSelected) {
245                         null
246                     } else {
247                         {
248                             viewModelScope.launch {
249                                 clockPickerInteractor.setClockColor(
250                                     selectedColorId = null,
251                                     colorToneProgress =
252                                         ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
253                                     seedColor = null,
254                                 )
255                                 logger.logClockColorApplied(NULL_SEED_COLOR)
256                             }
257                         }
258                     }
259                 },
260         )
261     }
262 
263     val selectedClockSize: Flow<ClockSize> = clockPickerInteractor.selectedClockSize
264 
265     fun setClockSize(size: ClockSize) {
266         viewModelScope.launch {
267             clockPickerInteractor.setClockSize(size)
268             logger.logClockSizeApplied(size.toClockSizeForLogging())
269         }
270     }
271 
272     private val _selectedTabPosition = MutableStateFlow(Tab.COLOR)
273     val selectedTab: StateFlow<Tab> = _selectedTabPosition.asStateFlow()
274     val tabs: Flow<List<ClockSettingsTabViewModel>> =
275         selectedTab.map {
276             listOf(
277                 ClockSettingsTabViewModel(
278                     name = context.resources.getString(R.string.clock_color),
279                     isSelected = it == Tab.COLOR,
280                     onClicked =
281                         if (it == Tab.COLOR) {
282                             null
283                         } else {
284                             { _selectedTabPosition.tryEmit(Tab.COLOR) }
285                         }
286                 ),
287                 ClockSettingsTabViewModel(
288                     name = context.resources.getString(R.string.clock_size),
289                     isSelected = it == Tab.SIZE,
290                     onClicked =
291                         if (it == Tab.SIZE) {
292                             null
293                         } else {
294                             { _selectedTabPosition.tryEmit(Tab.SIZE) }
295                         }
296                 ),
297             )
298         }
299 
300     companion object {
301         private val helperColorLab: DoubleArray by lazy { DoubleArray(3) }
302 
303         fun blendColorWithTone(color: Int, colorTone: Double): Int {
304             ColorUtils.colorToLAB(color, helperColorLab)
305             return ColorUtils.LABToColor(
306                 colorTone,
307                 helperColorLab[1],
308                 helperColorLab[2],
309             )
310         }
311 
312         const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
313     }
314 
315     class Factory(
316         private val context: Context,
317         private val clockPickerInteractor: ClockPickerInteractor,
318         private val colorPickerInteractor: ColorPickerInteractor,
319         private val logger: ThemesUserEventLogger,
320         private val getIsReactiveToTone: (clockId: String?) -> Boolean,
321     ) : ViewModelProvider.Factory {
322         override fun <T : ViewModel> create(modelClass: Class<T>): T {
323             @Suppress("UNCHECKED_CAST")
324             return ClockSettingsViewModel(
325                 context = context,
326                 clockPickerInteractor = clockPickerInteractor,
327                 colorPickerInteractor = colorPickerInteractor,
328                 logger = logger,
329                 getIsReactiveToTone = getIsReactiveToTone,
330             )
331                 as T
332         }
333     }
334 }
335