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