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 */ 17 package com.android.wallpaper.customization.ui.viewmodel 18 19 import android.content.Context 20 import com.android.customization.model.color.ColorOption 21 import com.android.customization.model.color.ColorOptionImpl 22 import com.android.customization.module.logging.ThemesUserEventLogger 23 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor2 24 import com.android.customization.picker.color.shared.model.ColorType 25 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel 26 import com.android.themepicker.R 27 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon 28 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text 29 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel 30 import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel 31 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2 32 import dagger.assisted.Assisted 33 import dagger.assisted.AssistedFactory 34 import dagger.assisted.AssistedInject 35 import dagger.hilt.android.qualifiers.ApplicationContext 36 import dagger.hilt.android.scopes.ViewModelScoped 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.coroutineScope 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.MutableStateFlow 41 import kotlinx.coroutines.flow.StateFlow 42 import kotlinx.coroutines.flow.asStateFlow 43 import kotlinx.coroutines.flow.combine 44 import kotlinx.coroutines.flow.map 45 import kotlinx.coroutines.flow.stateIn 46 import kotlinx.coroutines.flow.take 47 import kotlinx.coroutines.launch 48 49 /** Models UI state for a color picker experience. */ 50 class ColorPickerViewModel2 51 @AssistedInject 52 constructor( 53 @ApplicationContext context: Context, 54 private val colorUpdateViewModel: ColorUpdateViewModel, 55 private val interactor: ColorPickerInteractor2, 56 private val logger: ThemesUserEventLogger, 57 @Assisted private val viewModelScope: CoroutineScope, 58 ) { 59 val selectedColorOption = interactor.selectedColorOption 60 61 private val overridingColorOption = MutableStateFlow<ColorOption?>(null) 62 val previewingColorOption = overridingColorOption.asStateFlow() 63 64 private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null) 65 66 /** View-models for each color tab. */ 67 val colorTypeTabs: Flow<List<FloatingToolbarTabViewModel>> = 68 combine(interactor.colorOptions, selectedColorTypeTabId) { 69 colorOptions, 70 selectedColorTypeIdOrNull -> 71 colorOptions.keys.mapIndexed { index, colorType -> 72 val isSelected = 73 (selectedColorTypeIdOrNull == null && index == 0) || 74 selectedColorTypeIdOrNull == colorType 75 76 val name = 77 when (colorType) { 78 ColorType.WALLPAPER_COLOR -> 79 context.resources.getString(R.string.wallpaper_color_tab) 80 ColorType.PRESET_COLOR -> 81 context.resources.getString(R.string.preset_color_tab_2) 82 } 83 84 FloatingToolbarTabViewModel( 85 Icon.Resource( 86 res = 87 when (colorType) { 88 ColorType.WALLPAPER_COLOR -> 89 com.android.wallpaper.R.drawable.ic_baseline_wallpaper_24 90 ColorType.PRESET_COLOR -> R.drawable.ic_colors 91 }, 92 contentDescription = Text.Loaded(name), 93 ), 94 name, 95 isSelected, 96 ) { 97 if (!isSelected) { 98 this.selectedColorTypeTabId.value = colorType 99 } 100 } 101 } 102 } 103 104 /** View-models for each color tab subheader */ 105 val colorTypeTabSubheader: Flow<String> = 106 selectedColorTypeTabId.map { selectedColorTypeIdOrNull -> 107 when (selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR) { 108 ColorType.WALLPAPER_COLOR -> 109 context.resources.getString(R.string.wallpaper_color_subheader) 110 ColorType.PRESET_COLOR -> 111 context.resources.getString(R.string.preset_color_subheader) 112 } 113 } 114 115 /** The list of all color options mapped by their color type */ 116 private val allColorOptions: 117 Flow<Map<ColorType, List<OptionItemViewModel2<ColorOptionIconViewModel>>>> = 118 interactor.colorOptions.map { colorOptions -> 119 colorOptions 120 .map { colorOptionEntry -> 121 colorOptionEntry.key to 122 colorOptionEntry.value.map { colorOption -> 123 colorOption as ColorOptionImpl 124 val isSelectedFlow: StateFlow<Boolean> = 125 combine(previewingColorOption, selectedColorOption) { 126 previewing, 127 selected -> 128 previewing?.isEquivalent(colorOption) 129 ?: selected?.isEquivalent(colorOption) 130 ?: false 131 } 132 .stateIn(viewModelScope) 133 val key = 134 "${colorOption.type}::${colorOption.style}::${colorOption.serializedPackages}" 135 OptionItemViewModel2<ColorOptionIconViewModel>( 136 key = MutableStateFlow(key) as StateFlow<String>, 137 payload = ColorOptionIconViewModel.fromColorOption(colorOption), 138 text = 139 Text.Loaded( 140 colorOption.getContentDescription(context).toString() 141 ), 142 isTextUserVisible = false, 143 isSelected = isSelectedFlow, 144 onClicked = 145 isSelectedFlow.map { isSelected -> 146 if (isSelected) { 147 null 148 } else { 149 { 150 viewModelScope.launch { 151 overridingColorOption.value = colorOption 152 } 153 } 154 } 155 }, 156 ) 157 } 158 } 159 .toMap() 160 } 161 162 /** 163 * This function suspends until onApplyComplete is called to accommodate for configuration 164 * change updates, which are applied with a latency. 165 */ 166 val onApply: Flow<(suspend () -> Unit)?> = 167 combine(previewingColorOption, selectedColorOption) { previewing, selected -> 168 previewing?.let { 169 if (previewing.isEquivalent(selected)) { 170 null 171 } else { 172 { 173 coroutineScope { 174 launch { interactor.select(it) } 175 // Suspend until first color update 176 colorUpdateViewModel.systemColorsUpdatedNoReplay.take(1).collect { 177 return@collect 178 } 179 logger.logThemeColorApplied( 180 it.sourceForLogging, 181 it.styleForLogging, 182 it.seedColor, 183 ) 184 } 185 } 186 } 187 } 188 } 189 190 fun resetPreview() { 191 overridingColorOption.value = null 192 } 193 194 /** The list of all available color options for the selected Color Type. */ 195 val colorOptions: Flow<List<OptionItemViewModel2<ColorOptionIconViewModel>>> = 196 combine(allColorOptions, selectedColorTypeTabId) { 197 allColorOptions: Map<ColorType, List<OptionItemViewModel2<ColorOptionIconViewModel>>>, 198 selectedColorTypeIdOrNull -> 199 val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR 200 allColorOptions[selectedColorTypeId]!! 201 } 202 203 @ViewModelScoped 204 @AssistedFactory 205 interface Factory { 206 fun create(viewModelScope: CoroutineScope): ColorPickerViewModel2 207 } 208 } 209