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