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.res.Resources 19 import android.graphics.Color 20 import androidx.lifecycle.ViewModel 21 import androidx.lifecycle.ViewModelProvider 22 import androidx.lifecycle.viewModelScope 23 import com.android.customization.module.logging.ThemesUserEventLogger 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.ui.view.ClockViewFactory 27 import com.android.themepicker.R 28 import kotlinx.coroutines.CoroutineDispatcher 29 import kotlinx.coroutines.ExperimentalCoroutinesApi 30 import kotlinx.coroutines.Job 31 import kotlinx.coroutines.delay 32 import kotlinx.coroutines.flow.Flow 33 import kotlinx.coroutines.flow.SharingStarted 34 import kotlinx.coroutines.flow.StateFlow 35 import kotlinx.coroutines.flow.flatMapLatest 36 import kotlinx.coroutines.flow.flowOn 37 import kotlinx.coroutines.flow.map 38 import kotlinx.coroutines.flow.mapLatest 39 import kotlinx.coroutines.flow.mapNotNull 40 import kotlinx.coroutines.flow.stateIn 41 import kotlinx.coroutines.launch 42 43 /** 44 * Clock carousel view model that provides data for the carousel of clock previews. When there is 45 * only one item, we should show a single clock preview instead of a carousel. 46 */ 47 class ClockCarouselViewModel( 48 private val interactor: ClockPickerInteractor, 49 private val backgroundDispatcher: CoroutineDispatcher, 50 private val clockViewFactory: ClockViewFactory, 51 private val resources: Resources, 52 private val logger: ThemesUserEventLogger, 53 ) : ViewModel() { 54 @OptIn(ExperimentalCoroutinesApi::class) 55 val allClocks: StateFlow<List<ClockCarouselItemViewModel>> = 56 interactor.allClocks 57 .mapLatest { allClocks -> 58 // Delay to avoid the case that the full list of clocks is not initiated. 59 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS) 60 allClocks.map { 61 val contentDescription = 62 resources.getString( 63 R.string.select_clock_action_description, 64 clockViewFactory.getController(it.clockId).config.description 65 ) 66 ClockCarouselItemViewModel(it.clockId, it.isSelected, contentDescription) 67 } 68 } 69 // makes sure that the operations above this statement are executed on I/O dispatcher 70 // while parallelism limits the number of threads this can run on which makes sure that 71 // the flows run sequentially 72 .flowOn(backgroundDispatcher.limitedParallelism(1)) 73 .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) 74 75 val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize 76 77 val seedColor: Flow<Int?> = interactor.seedColor 78 79 fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> { 80 return interactor.seedColor.map { 81 it.let { seedColor -> 82 // if seedColor is null, default clock color is selected 83 if (seedColor == null) { 84 if (isDarkThemeEnabled) { 85 // In dark mode, use darkest surface container color 86 com.android.wallpaper.R.color.system_surface_container_high 87 } else { 88 // In light mode, use lightest surface container color 89 com.android.wallpaper.R.color.system_surface_bright 90 } 91 } else { 92 val luminance = Color.luminance(seedColor) 93 if (isDarkThemeEnabled) { 94 if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) { 95 com.android.wallpaper.R.color.system_surface_bright 96 } else { 97 com.android.wallpaper.R.color.system_surface_container_high 98 } 99 } else { 100 if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) { 101 com.android.wallpaper.R.color.system_surface_bright 102 } else { 103 com.android.wallpaper.R.color.system_surface_container_highest 104 } 105 } 106 } 107 } 108 } 109 } 110 111 @OptIn(ExperimentalCoroutinesApi::class) 112 val selectedIndex: Flow<Int> = 113 allClocks 114 .flatMapLatest { allClockIds -> 115 interactor.selectedClockId.map { selectedClockId -> 116 val index = allClockIds.indexOfFirst { it.clockId == selectedClockId } 117 /** Making sure there is no active [setSelectedClockJob] */ 118 val isSetClockIdJobActive = setSelectedClockJob?.isActive == true 119 if (index >= 0 && !isSetClockIdJobActive) { 120 index 121 } else { 122 null 123 } 124 } 125 } 126 .mapNotNull { it } 127 128 private var setSelectedClockJob: Job? = null 129 fun setSelectedClock(clockId: String) { 130 setSelectedClockJob?.cancel() 131 setSelectedClockJob = 132 viewModelScope.launch(backgroundDispatcher) { 133 interactor.setSelectedClock(clockId) 134 logger.logClockApplied(clockId) 135 } 136 } 137 138 class Factory( 139 private val interactor: ClockPickerInteractor, 140 private val backgroundDispatcher: CoroutineDispatcher, 141 private val clockViewFactory: ClockViewFactory, 142 private val resources: Resources, 143 private val logger: ThemesUserEventLogger, 144 ) : ViewModelProvider.Factory { 145 override fun <T : ViewModel> create(modelClass: Class<T>): T { 146 @Suppress("UNCHECKED_CAST") 147 return ClockCarouselViewModel( 148 interactor = interactor, 149 backgroundDispatcher = backgroundDispatcher, 150 clockViewFactory = clockViewFactory, 151 resources = resources, 152 logger = logger, 153 ) 154 as T 155 } 156 } 157 158 companion object { 159 const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100 160 const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f 161 const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f 162 } 163 } 164