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