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 */ 17 package com.android.customization.picker.color.ui.viewmodel 18 19 import android.content.Context 20 import androidx.lifecycle.ViewModel 21 import androidx.lifecycle.ViewModelProvider 22 import androidx.lifecycle.viewModelScope 23 import com.android.customization.model.color.ColorOptionImpl 24 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor 25 import com.android.customization.picker.color.shared.model.ColorType 26 import com.android.wallpaper.R 27 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text 28 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel 29 import kotlin.math.max 30 import kotlin.math.min 31 import kotlinx.coroutines.flow.Flow 32 import kotlinx.coroutines.flow.MutableStateFlow 33 import kotlinx.coroutines.flow.SharingStarted 34 import kotlinx.coroutines.flow.StateFlow 35 import kotlinx.coroutines.flow.combine 36 import kotlinx.coroutines.flow.map 37 import kotlinx.coroutines.flow.shareIn 38 import kotlinx.coroutines.flow.stateIn 39 import kotlinx.coroutines.launch 40 41 /** Models UI state for a color picker experience. */ 42 class ColorPickerViewModel 43 private constructor( 44 context: Context, 45 private val interactor: ColorPickerInteractor, 46 ) : ViewModel() { 47 48 private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null) 49 50 /** View-models for each color tab. */ 51 val colorTypeTabs: Flow<Map<ColorType, ColorTypeTabViewModel>> = 52 combine( 53 interactor.colorOptions, 54 selectedColorTypeTabId, 55 ) { colorOptions, selectedColorTypeIdOrNull -> 56 colorOptions.keys 57 .mapIndexed { index, colorType -> 58 val isSelected = 59 (selectedColorTypeIdOrNull == null && index == 0) || 60 selectedColorTypeIdOrNull == colorType 61 colorType to 62 ColorTypeTabViewModel( 63 name = 64 when (colorType) { 65 ColorType.WALLPAPER_COLOR -> 66 context.resources.getString(R.string.wallpaper_color_tab) 67 ColorType.PRESET_COLOR -> 68 context.resources.getString(R.string.preset_color_tab_2) 69 }, 70 isSelected = isSelected, 71 onClick = 72 if (isSelected) { 73 null 74 } else { 75 { this.selectedColorTypeTabId.value = colorType } 76 }, 77 ) 78 } 79 .toMap() 80 } 81 82 /** View-models for each color tab subheader */ 83 val colorTypeTabSubheader: Flow<String> = 84 selectedColorTypeTabId.map { selectedColorTypeIdOrNull -> 85 when (selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR) { 86 ColorType.WALLPAPER_COLOR -> 87 context.resources.getString(R.string.wallpaper_color_subheader) 88 ColorType.PRESET_COLOR -> 89 context.resources.getString(R.string.preset_color_subheader) 90 } 91 } 92 93 /** The list of all color options mapped by their color type */ 94 private val allColorOptions: 95 Flow<Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>> = 96 interactor.colorOptions 97 .map { colorOptions -> 98 colorOptions 99 .map { colorOptionEntry -> 100 colorOptionEntry.key to 101 colorOptionEntry.value.map { colorOptionModel -> 102 val colorOption: ColorOptionImpl = 103 colorOptionModel.colorOption as ColorOptionImpl 104 val lightThemeColors = 105 colorOption.previewInfo.resolveColors(/* darkTheme= */ false) 106 val darkThemeColors = 107 colorOption.previewInfo.resolveColors(/* darkTheme= */ true) 108 val isSelectedFlow: StateFlow<Boolean> = 109 interactor.activeColorOption 110 .map { 111 it?.colorOption?.isEquivalent( 112 colorOptionModel.colorOption 113 ) 114 ?: colorOptionModel.isSelected 115 } 116 .stateIn(viewModelScope) 117 OptionItemViewModel<ColorOptionIconViewModel>( 118 key = 119 MutableStateFlow(colorOptionModel.key) as StateFlow<String>, 120 payload = 121 ColorOptionIconViewModel( 122 lightThemeColor0 = lightThemeColors[0], 123 lightThemeColor1 = lightThemeColors[1], 124 lightThemeColor2 = lightThemeColors[2], 125 lightThemeColor3 = lightThemeColors[3], 126 darkThemeColor0 = darkThemeColors[0], 127 darkThemeColor1 = darkThemeColors[1], 128 darkThemeColor2 = darkThemeColors[2], 129 darkThemeColor3 = darkThemeColors[3], 130 ), 131 text = 132 Text.Loaded( 133 colorOption.getContentDescription(context).toString() 134 ), 135 isTextUserVisible = false, 136 isSelected = isSelectedFlow, 137 onClicked = 138 isSelectedFlow.map { isSelected -> 139 if (isSelected) { 140 null 141 } else { 142 { 143 viewModelScope.launch { 144 interactor.select(colorOptionModel) 145 } 146 } 147 } 148 }, 149 ) 150 } 151 } 152 .toMap() 153 } 154 .shareIn( 155 scope = viewModelScope, 156 started = SharingStarted.WhileSubscribed(), 157 replay = 1, 158 ) 159 160 /** The list of all available color options for the selected Color Type. */ 161 val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> = 162 combine(allColorOptions, selectedColorTypeTabId) { 163 allColorOptions: 164 Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>, 165 selectedColorTypeIdOrNull -> 166 val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR 167 allColorOptions[selectedColorTypeId]!! 168 } 169 .shareIn( 170 scope = viewModelScope, 171 started = SharingStarted.Eagerly, 172 replay = 1, 173 ) 174 175 /** The list of color options for the color section */ 176 val colorSectionOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> = 177 allColorOptions 178 .map { allColorOptions -> 179 val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR] 180 val presetOptions = allColorOptions[ColorType.PRESET_COLOR] 181 val subOptions = 182 wallpaperOptions!!.subList( 183 0, 184 min(COLOR_SECTION_OPTION_SIZE, wallpaperOptions.size) 185 ) 186 // Add additional options based on preset colors if size of wallpaper color options 187 // is 188 // less than COLOR_SECTION_OPTION_SIZE 189 val additionalSubOptions = 190 presetOptions!!.subList( 191 0, 192 min( 193 max(0, COLOR_SECTION_OPTION_SIZE - wallpaperOptions.size), 194 presetOptions.size, 195 ) 196 ) 197 subOptions + additionalSubOptions 198 } 199 .shareIn( 200 scope = viewModelScope, 201 started = SharingStarted.WhileSubscribed(), 202 replay = 1, 203 ) 204 205 class Factory( 206 private val context: Context, 207 private val interactor: ColorPickerInteractor, 208 ) : ViewModelProvider.Factory { 209 override fun <T : ViewModel> create(modelClass: Class<T>): T { 210 @Suppress("UNCHECKED_CAST") 211 return ColorPickerViewModel( 212 context = context, 213 interactor = interactor, 214 ) 215 as T 216 } 217 } 218 219 companion object { 220 private const val COLOR_SECTION_OPTION_SIZE = 5 221 } 222 } 223