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