1 /* <lambda>null2 * Copyright (C) 2024 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.wallpaper.customization.ui.viewmodel 17 18 import android.content.Context 19 import android.content.res.Resources 20 import android.graphics.drawable.Drawable 21 import androidx.core.graphics.ColorUtils 22 import com.android.customization.model.color.ColorOption 23 import com.android.customization.model.color.ColorOptionImpl 24 import com.android.customization.module.logging.ThemesUserEventLogger 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.clock.ui.viewmodel.ClockColorViewModel 29 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor2 30 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel 31 import com.android.systemui.plugins.clocks.AxisPresetConfig 32 import com.android.systemui.plugins.clocks.AxisPresetConfig.IndexedStyle 33 import com.android.systemui.plugins.clocks.ClockAxisStyle 34 import com.android.themepicker.R 35 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon 36 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text 37 import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel 38 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher 39 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2 40 import dagger.assisted.Assisted 41 import dagger.assisted.AssistedFactory 42 import dagger.assisted.AssistedInject 43 import dagger.hilt.android.qualifiers.ApplicationContext 44 import dagger.hilt.android.scopes.ViewModelScoped 45 import kotlin.math.roundToInt 46 import kotlinx.coroutines.CoroutineDispatcher 47 import kotlinx.coroutines.CoroutineScope 48 import kotlinx.coroutines.ExperimentalCoroutinesApi 49 import kotlinx.coroutines.delay 50 import kotlinx.coroutines.flow.Flow 51 import kotlinx.coroutines.flow.MutableStateFlow 52 import kotlinx.coroutines.flow.SharingStarted 53 import kotlinx.coroutines.flow.StateFlow 54 import kotlinx.coroutines.flow.asStateFlow 55 import kotlinx.coroutines.flow.combine 56 import kotlinx.coroutines.flow.distinctUntilChanged 57 import kotlinx.coroutines.flow.filterNotNull 58 import kotlinx.coroutines.flow.flowOn 59 import kotlinx.coroutines.flow.map 60 import kotlinx.coroutines.flow.mapLatest 61 import kotlinx.coroutines.flow.shareIn 62 import kotlinx.coroutines.flow.stateIn 63 64 /** View model for the clock customization screen. */ 65 class ClockPickerViewModel 66 @AssistedInject 67 constructor( 68 @ApplicationContext context: Context, 69 resources: Resources, 70 private val clockPickerInteractor: ClockPickerInteractor, 71 colorPickerInteractor: ColorPickerInteractor2, 72 private val logger: ThemesUserEventLogger, 73 @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher, 74 @Assisted private val viewModelScope: CoroutineScope, 75 ) { 76 77 enum class Tab { 78 STYLE, 79 COLOR, 80 SIZE, 81 } 82 83 private val colorMap = ClockColorViewModel.getPresetColorMap(context.resources) 84 85 // Tabs 86 private val _selectedTab = MutableStateFlow(Tab.STYLE) 87 val selectedTab: StateFlow<Tab> = _selectedTab.asStateFlow() 88 val tabs: Flow<List<FloatingToolbarTabViewModel>> = 89 selectedTab.map { 90 listOf( 91 FloatingToolbarTabViewModel( 92 icon = 93 Icon.Resource( 94 res = R.drawable.ic_clock_filled_24px, 95 contentDescription = Text.Resource(R.string.clock_style), 96 ), 97 text = context.getString(R.string.clock_style), 98 isSelected = it == Tab.STYLE, 99 onClick = 100 if (it == Tab.STYLE) null 101 else { 102 { _selectedTab.value = Tab.STYLE } 103 }, 104 ), 105 FloatingToolbarTabViewModel( 106 icon = 107 Icon.Resource( 108 res = R.drawable.ic_palette_filled_24px, 109 contentDescription = Text.Resource(R.string.clock_color), 110 ), 111 text = context.getString(R.string.clock_color), 112 isSelected = it == Tab.COLOR, 113 onClick = 114 if (it == Tab.COLOR) null 115 else { 116 { _selectedTab.value = Tab.COLOR } 117 }, 118 ), 119 FloatingToolbarTabViewModel( 120 icon = 121 Icon.Resource( 122 res = R.drawable.ic_font_size_filled_24px, 123 contentDescription = Text.Resource(R.string.clock_size), 124 ), 125 text = context.getString(R.string.clock_size), 126 isSelected = it == Tab.SIZE, 127 onClick = 128 if (it == Tab.SIZE) null 129 else { 130 { _selectedTab.value = Tab.SIZE } 131 }, 132 ), 133 ) 134 } 135 136 // Clock style 137 private val overridingClock = MutableStateFlow<ClockMetadataModel?>(null) 138 val selectedClock = clockPickerInteractor.selectedClock 139 val previewingClock = 140 combine(overridingClock, selectedClock) { overridingClock, selectedClock -> 141 (overridingClock ?: selectedClock) 142 } 143 .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) 144 private val isClockEdited = 145 combine(overridingClock, selectedClock) { overridingClock, selectedClock -> 146 overridingClock != null && overridingClock.clockId != selectedClock.clockId 147 } 148 149 suspend fun getIsShadeLayoutWide() = clockPickerInteractor.getIsShadeLayoutWide() 150 151 suspend fun getUdfpsLocation() = clockPickerInteractor.getUdfpsLocation() 152 153 data class ClockStyleModel(val thumbnail: Drawable) 154 155 @OptIn(ExperimentalCoroutinesApi::class) 156 val clockStyleOptions: StateFlow<List<OptionItemViewModel2<ClockStyleModel>>> = 157 clockPickerInteractor.allClocks 158 .mapLatest { allClocks -> 159 // Delay to avoid the case that the full list of clocks is not initiated. 160 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS) 161 val allClockMap = allClocks.groupBy { it.axisPresetConfig != null } 162 buildList { 163 allClockMap[true]?.map { add(it.toOption(resources)) } 164 allClockMap[false]?.map { add(it.toOption(resources)) } 165 } 166 } 167 // makes sure that the operations above this statement are executed on I/O dispatcher 168 // while parallelism limits the number of threads this can run on which makes sure that 169 // the flows run sequentially 170 .flowOn(backgroundDispatcher.limitedParallelism(1)) 171 .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) 172 173 // Clock font presets 174 private val overridingClockPresetIndexedStyle: MutableStateFlow<IndexedStyle?> = 175 MutableStateFlow(null) 176 private val selectedClockPresetIndexedStyle: Flow<IndexedStyle?> = 177 previewingClock 178 .map { it.axisPresetConfig?.current } 179 .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) 180 val previewingClockPresetIndexedStyle: Flow<IndexedStyle?> = 181 combine(overridingClockPresetIndexedStyle, selectedClockPresetIndexedStyle) { 182 overridingClockPresetIndexedStyle, 183 selectedClockPresetIndexedStyle -> 184 overridingClockPresetIndexedStyle ?: selectedClockPresetIndexedStyle 185 } 186 private val isClockAxisStyleEdited: Flow<Boolean> = 187 combine(overridingClockPresetIndexedStyle, selectedClockPresetIndexedStyle) { 188 overridingClockPresetIndexedStyle, 189 selectedClockPresetIndexedStyle -> 190 overridingClockPresetIndexedStyle != null && 191 (overridingClockPresetIndexedStyle.style != selectedClockPresetIndexedStyle?.style) 192 } 193 194 private val groups: Flow<List<AxisPresetConfig.Group>?> = 195 previewingClock.map { it.axisPresetConfig?.groups } 196 private val previewingClockPresetGroupIndex: Flow<Int> = 197 previewingClockPresetIndexedStyle.map { it?.groupIndex ?: 0 }.distinctUntilChanged() 198 val shouldShowPresetSlider: Flow<Boolean> = previewingClock.map { it.axisPresetConfig != null } 199 val axisPresetsSliderViewModel: Flow<ClockAxisPresetSliderViewModel?> = 200 combine(groups, previewingClockPresetGroupIndex) { groups, previewingClockPresetGroupIndex 201 -> 202 if (groups.isNullOrEmpty()) { 203 null 204 } else { 205 val group = groups[previewingClockPresetGroupIndex] 206 ClockAxisPresetSliderViewModel( 207 valueFrom = 0F, 208 valueTo = (group.presets.size - 1).toFloat(), 209 stepSize = 1F, 210 onSliderStopTrackingTouch = { value -> 211 val presetIndex = value.roundToInt() 212 overridingClockPresetIndexedStyle.value = 213 IndexedStyle( 214 groupIndex = previewingClockPresetGroupIndex, 215 presetIndex = presetIndex, 216 style = group.presets[presetIndex], 217 ) 218 }, 219 ) 220 } 221 } 222 val axisPresetsSliderSelectedValue: Flow<Float> = 223 previewingClockPresetIndexedStyle.map { it?.presetIndex?.toFloat() }.filterNotNull() 224 val onClockFaceClicked: Flow<() -> Unit> = 225 combine(groups, previewingClockPresetIndexedStyle) { groups, previewingIndexedStyle -> 226 if (groups.isNullOrEmpty()) { 227 {} 228 } else { 229 val groupCount = groups.size 230 if (groupCount == 1) { 231 {} 232 } else { 233 val currentGroupIndex = previewingIndexedStyle?.groupIndex ?: 0 234 val nextGroupIndex = (currentGroupIndex + 1) % groupCount 235 val nextPresetIndex = previewingIndexedStyle?.presetIndex ?: (groupCount / 2) 236 val nextGroup = groups[nextGroupIndex] 237 { 238 overridingClockPresetIndexedStyle.value = 239 IndexedStyle( 240 groupIndex = nextGroupIndex, 241 presetIndex = nextPresetIndex, 242 style = nextGroup.presets[nextPresetIndex], 243 ) 244 } 245 } 246 } 247 } 248 249 private suspend fun ClockMetadataModel.toOption( 250 resources: Resources 251 ): OptionItemViewModel2<ClockStyleModel> { 252 val isSelectedFlow = previewingClock.map { it.clockId == clockId }.stateIn(viewModelScope) 253 val contentDescription = 254 resources.getString(R.string.select_clock_action_description, description) 255 return OptionItemViewModel2<ClockStyleModel>( 256 key = MutableStateFlow(clockId) as StateFlow<String>, 257 payload = ClockStyleModel(thumbnail = thumbnail), 258 text = Text.Loaded(contentDescription), 259 isTextUserVisible = false, 260 isSelected = isSelectedFlow, 261 onClicked = 262 isSelectedFlow.map { isSelected -> 263 if (isSelected) { 264 null 265 } else { 266 fun() { 267 overridingClock.value = this 268 } 269 } 270 }, 271 ) 272 } 273 274 // Clock size 275 private val overridingClockSize = MutableStateFlow<ClockSize?>(null) 276 private val isClockSizeEdited = 277 combine(overridingClockSize, clockPickerInteractor.selectedClockSize) { 278 overridingClockSize, 279 selectedClockSize -> 280 overridingClockSize != null && overridingClockSize != selectedClockSize 281 } 282 val previewingClockSize = 283 combine(overridingClockSize, clockPickerInteractor.selectedClockSize) { 284 overridingClockSize, 285 selectedClockSize -> 286 overridingClockSize ?: selectedClockSize 287 } 288 val onClockSizeSwitchCheckedChange: Flow<(() -> Unit)> = 289 previewingClockSize.map { 290 { 291 when (it) { 292 ClockSize.DYNAMIC -> overridingClockSize.value = ClockSize.SMALL 293 ClockSize.SMALL -> overridingClockSize.value = ClockSize.DYNAMIC 294 } 295 } 296 } 297 298 // Clock color 299 private val overridingClockColorId = MutableStateFlow<String?>(null) 300 private val isClockColorIdEdited = 301 combine(overridingClockColorId, clockPickerInteractor.selectedColorId) { 302 overridingClockColorId, 303 selectedColorId -> 304 overridingClockColorId != null && (overridingClockColorId != selectedColorId) 305 } 306 private val previewingClockColorId = 307 combine(overridingClockColorId, clockPickerInteractor.selectedColorId) { 308 overridingClockColorId, 309 selectedColorId -> 310 overridingClockColorId ?: selectedColorId ?: DEFAULT_CLOCK_COLOR_ID 311 } 312 313 // Clock color slider progress. Range is 0 - 100. 314 private val overridingSliderProgress = MutableStateFlow<Int?>(null) 315 private val isSliderProgressEdited = 316 combine(overridingSliderProgress, clockPickerInteractor.colorToneProgress) { 317 overridingSliderProgress, 318 colorToneProgress -> 319 overridingSliderProgress != null && (overridingSliderProgress != colorToneProgress) 320 } 321 val previewingSliderProgress: Flow<Int> = 322 combine(overridingSliderProgress, clockPickerInteractor.colorToneProgress) { 323 overridingSliderProgress, 324 colorToneProgress -> 325 overridingSliderProgress ?: colorToneProgress 326 } 327 val isSliderEnabled: Flow<Boolean> = 328 combine(previewingClock, previewingClockColorId) { clock, clockColorId -> 329 clock.isReactiveToTone && clockColorId != DEFAULT_CLOCK_COLOR_ID 330 } 331 .distinctUntilChanged() 332 333 fun onSliderProgressChanged(progress: Int) { 334 overridingSliderProgress.value = progress 335 } 336 337 val previewingSeedColor: Flow<Int?> = 338 combine(previewingClockColorId, previewingSliderProgress) { clockColorId, sliderProgress -> 339 val clockColorViewModel = 340 if (clockColorId == DEFAULT_CLOCK_COLOR_ID) null else colorMap[clockColorId] 341 if (clockColorViewModel == null) { 342 null 343 } else { 344 blendColorWithTone( 345 color = clockColorViewModel.color, 346 colorTone = clockColorViewModel.getColorTone(sliderProgress), 347 ) 348 } 349 } 350 351 val clockColorOptions: Flow<List<OptionItemViewModel2<ColorOptionIconViewModel>>> = 352 colorPickerInteractor.selectedColorOption.map { selectedColorOption -> 353 // Use mapLatest and delay(100) here to prevent too many selectedClockColor update 354 // events from ClockRegistry upstream, caused by sliding the saturation level bar. 355 delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS) 356 buildList { 357 selectedColorOption?.let { add(it.toOptionItemViewModel(context)) } 358 359 colorMap.values.forEachIndexed { index, colorModel -> 360 val isSelectedFlow = 361 previewingClockColorId 362 .map { colorMap.keys.indexOf(it) == index } 363 .stateIn(viewModelScope) 364 add( 365 OptionItemViewModel2<ColorOptionIconViewModel>( 366 key = MutableStateFlow(colorModel.colorId) as StateFlow<String>, 367 payload = 368 ColorOptionIconViewModel( 369 lightThemeColor0 = colorModel.color, 370 lightThemeColor1 = colorModel.color, 371 lightThemeColor2 = colorModel.color, 372 lightThemeColor3 = colorModel.color, 373 darkThemeColor0 = colorModel.color, 374 darkThemeColor1 = colorModel.color, 375 darkThemeColor2 = colorModel.color, 376 darkThemeColor3 = colorModel.color, 377 ), 378 text = 379 Text.Loaded( 380 context.getString( 381 R.string.content_description_color_option, 382 index, 383 ) 384 ), 385 isTextUserVisible = false, 386 isSelected = isSelectedFlow, 387 onClicked = 388 isSelectedFlow.map { isSelected -> 389 if (isSelected) { 390 null 391 } else { 392 { 393 overridingClockColorId.value = colorModel.colorId 394 overridingSliderProgress.value = 395 ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS 396 } 397 } 398 }, 399 ) 400 ) 401 } 402 } 403 } 404 405 private suspend fun ColorOption.toOptionItemViewModel( 406 context: Context 407 ): OptionItemViewModel2<ColorOptionIconViewModel> { 408 val lightThemeColors = 409 (this as ColorOptionImpl) 410 .previewInfo 411 .resolveColors( 412 /** darkTheme= */ 413 false 414 ) 415 val darkThemeColors = 416 this.previewInfo.resolveColors( 417 /** darkTheme= */ 418 true 419 ) 420 val isSelectedFlow = 421 previewingClockColorId.map { it == DEFAULT_CLOCK_COLOR_ID }.stateIn(viewModelScope) 422 val key = "${this.type}::${this.style}::${this.serializedPackages}" 423 return OptionItemViewModel2<ColorOptionIconViewModel>( 424 key = MutableStateFlow(key) as StateFlow<String>, 425 payload = 426 ColorOptionIconViewModel( 427 lightThemeColor0 = lightThemeColors[0], 428 lightThemeColor1 = lightThemeColors[1], 429 lightThemeColor2 = lightThemeColors[2], 430 lightThemeColor3 = lightThemeColors[3], 431 darkThemeColor0 = darkThemeColors[0], 432 darkThemeColor1 = darkThemeColors[1], 433 darkThemeColor2 = darkThemeColors[2], 434 darkThemeColor3 = darkThemeColors[3], 435 ), 436 text = Text.Loaded(context.getString(R.string.default_theme_title)), 437 isTextUserVisible = true, 438 isSelected = isSelectedFlow, 439 onClicked = 440 isSelectedFlow.map { isSelected -> 441 if (isSelected) { 442 null 443 } else { 444 { 445 overridingClockColorId.value = DEFAULT_CLOCK_COLOR_ID 446 overridingSliderProgress.value = 447 ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS 448 } 449 } 450 }, 451 ) 452 } 453 454 private val isEdited = 455 combine( 456 isClockEdited, 457 isClockAxisStyleEdited, 458 isClockSizeEdited, 459 isClockColorIdEdited, 460 isSliderProgressEdited, 461 ) { 462 isClockEdited, 463 isClockAxisStyleEdited, 464 isClockSizeEdited, 465 isClockColorEdited, 466 isSliderProgressEdited -> 467 isClockEdited || 468 isClockAxisStyleEdited || 469 isClockSizeEdited || 470 isClockColorEdited || 471 isSliderProgressEdited 472 } 473 private val onApplyClicked: MutableStateFlow<Boolean> = MutableStateFlow(false) 474 val onApply: Flow<(suspend () -> Unit)?> = 475 combine( 476 onApplyClicked, 477 isEdited, 478 previewingClock, 479 previewingClockSize, 480 previewingClockColorId, 481 previewingSliderProgress, 482 previewingClockPresetIndexedStyle, 483 ) { array -> 484 val onApplyClicked: Boolean = array[0] as Boolean 485 val isEdited: Boolean = array[1] as Boolean 486 val clock: ClockMetadataModel = array[2] as ClockMetadataModel 487 val size: ClockSize = array[3] as ClockSize 488 val previewingColorId: String = array[4] as String 489 val previewProgress: Int = array[5] as Int 490 val clockAxisStyle: ClockAxisStyle = 491 (array[6] as? IndexedStyle)?.style ?: ClockAxisStyle() 492 if (isEdited && !onApplyClicked) { 493 { 494 this.onApplyClicked.value = true 495 clockPickerInteractor.applyClock( 496 clockId = clock.clockId, 497 size = size, 498 selectedColorId = previewingColorId, 499 colorToneProgress = previewProgress, 500 seedColor = 501 colorMap[previewingColorId]?.let { 502 blendColorWithTone( 503 color = it.color, 504 colorTone = it.getColorTone(previewProgress), 505 ) 506 }, 507 axisSettings = clockAxisStyle, 508 ) 509 } 510 } else { 511 null 512 } 513 } 514 515 fun resetPreview() { 516 overridingClock.value = null 517 overridingClockSize.value = null 518 overridingClockColorId.value = null 519 overridingSliderProgress.value = null 520 overridingClockPresetIndexedStyle.value = null 521 _selectedTab.value = Tab.STYLE 522 onApplyClicked.value = false 523 } 524 525 companion object { 526 private const val DEFAULT_CLOCK_COLOR_ID = "DEFAULT" 527 private val helperColorLab: DoubleArray by lazy { DoubleArray(3) } 528 529 fun blendColorWithTone(color: Int, colorTone: Double): Int { 530 ColorUtils.colorToLAB(color, helperColorLab) 531 return ColorUtils.LABToColor(colorTone, helperColorLab[1], helperColorLab[2]) 532 } 533 534 const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100 535 const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100 536 } 537 538 @ViewModelScoped 539 @AssistedFactory 540 interface Factory { 541 fun create(viewModelScope: CoroutineScope): ClockPickerViewModel 542 } 543 } 544