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.graphics.Color 19 import androidx.lifecycle.ViewModel 20 import androidx.lifecycle.ViewModelProvider 21 import androidx.lifecycle.viewModelScope 22 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor 23 import com.android.customization.picker.clock.shared.ClockSize 24 import com.android.wallpaper.R 25 import kotlinx.coroutines.CoroutineDispatcher 26 import kotlinx.coroutines.ExperimentalCoroutinesApi 27 import kotlinx.coroutines.Job 28 import kotlinx.coroutines.delay 29 import kotlinx.coroutines.flow.Flow 30 import kotlinx.coroutines.flow.SharingStarted 31 import kotlinx.coroutines.flow.StateFlow 32 import kotlinx.coroutines.flow.flatMapLatest 33 import kotlinx.coroutines.flow.map 34 import kotlinx.coroutines.flow.mapLatest 35 import kotlinx.coroutines.flow.mapNotNull 36 import kotlinx.coroutines.flow.stateIn 37 import kotlinx.coroutines.launch 38 39 /** 40 * Clock carousel view model that provides data for the carousel of clock previews. When there is 41 * only one item, we should show a single clock preview instead of a carousel. 42 */ 43 class ClockCarouselViewModel( 44 private val interactor: ClockPickerInteractor, 45 private val backgroundDispatcher: CoroutineDispatcher, 46 ) : ViewModel() { 47 @OptIn(ExperimentalCoroutinesApi::class) 48 val allClocks: StateFlow<List<ClockCarouselItemViewModel>> = 49 interactor.allClocks 50 .mapLatest { allClocks -> 51 // Delay to avoid the case that the full list of clocks is not initiated. 52 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS) 53 allClocks.map { ClockCarouselItemViewModel(it.clockId, it.isSelected) } 54 } 55 .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) 56 57 val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize 58 59 val seedColor: Flow<Int?> = interactor.seedColor 60 61 fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> { 62 return interactor.seedColor.map { 63 it.let { seedColor -> 64 // if seedColor is null, default clock color is selected 65 if (seedColor == null) { 66 if (isDarkThemeEnabled) { 67 // In dark mode, use darkest surface container color 68 R.color.system_surface_container_high 69 } else { 70 // In light mode, use lightest surface container color 71 R.color.system_surface_bright 72 } 73 } else { 74 val luminance = Color.luminance(seedColor) 75 if (isDarkThemeEnabled) { 76 if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) { 77 R.color.system_surface_bright 78 } else { 79 R.color.system_surface_container_high 80 } 81 } else { 82 if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) { 83 R.color.system_surface_bright 84 } else { 85 R.color.system_surface_container_highest 86 } 87 } 88 } 89 } 90 } 91 } 92 93 @OptIn(ExperimentalCoroutinesApi::class) 94 val selectedIndex: Flow<Int> = 95 allClocks 96 .flatMapLatest { allClockIds -> 97 interactor.selectedClockId.map { selectedClockId -> 98 val index = allClockIds.indexOfFirst { it.clockId == selectedClockId } 99 /** Making sure there is no active [setSelectedClockJob] */ 100 val isSetClockIdJobActive = setSelectedClockJob?.isActive == true 101 if (index >= 0 && !isSetClockIdJobActive) { 102 index 103 } else { 104 null 105 } 106 } 107 } 108 .mapNotNull { it } 109 110 private var setSelectedClockJob: Job? = null 111 fun setSelectedClock(clockId: String) { 112 setSelectedClockJob?.cancel() 113 setSelectedClockJob = 114 viewModelScope.launch(backgroundDispatcher) { interactor.setSelectedClock(clockId) } 115 } 116 117 class Factory( 118 private val interactor: ClockPickerInteractor, 119 private val backgroundDispatcher: CoroutineDispatcher, 120 ) : ViewModelProvider.Factory { 121 override fun <T : ViewModel> create(modelClass: Class<T>): T { 122 @Suppress("UNCHECKED_CAST") 123 return ClockCarouselViewModel( 124 interactor = interactor, 125 backgroundDispatcher = backgroundDispatcher, 126 ) 127 as T 128 } 129 } 130 131 companion object { 132 const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100 133 const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f 134 const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f 135 } 136 } 137