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.module.logging.ThemesUserEventLogger
25 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
26 import com.android.customization.picker.color.shared.model.ColorType
27 import com.android.themepicker.R
28 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
29 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
30 import kotlin.math.max
31 import kotlin.math.min
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.MutableStateFlow
34 import kotlinx.coroutines.flow.StateFlow
35 import kotlinx.coroutines.flow.combine
36 import kotlinx.coroutines.flow.map
37 import kotlinx.coroutines.flow.stateIn
38 import kotlinx.coroutines.launch
39 
40 /** Models UI state for a color picker experience. */
41 class ColorPickerViewModel
42 private constructor(
43     context: Context,
44     private val interactor: ColorPickerInteractor,
45     private val logger: ThemesUserEventLogger,
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.map { colorOptions ->
97             colorOptions
98                 .map { colorOptionEntry ->
99                     colorOptionEntry.key to
100                         colorOptionEntry.value.map { colorOptionModel ->
101                             val colorOption: ColorOptionImpl =
102                                 colorOptionModel.colorOption as ColorOptionImpl
103                             val lightThemeColors =
104                                 colorOption.previewInfo.resolveColors(/* darkTheme= */ false)
105                             val darkThemeColors =
106                                 colorOption.previewInfo.resolveColors(/* darkTheme= */ true)
107                             val isSelectedFlow: StateFlow<Boolean> =
108                                 interactor.selectingColorOption
109                                     .map {
110                                         it?.colorOption?.isEquivalent(colorOptionModel.colorOption)
111                                             ?: colorOptionModel.isSelected
112                                     }
113                                     .stateIn(viewModelScope)
114                             OptionItemViewModel<ColorOptionIconViewModel>(
115                                 key = MutableStateFlow(colorOptionModel.key) as StateFlow<String>,
116                                 payload =
117                                     ColorOptionIconViewModel(
118                                         lightThemeColor0 = lightThemeColors[0],
119                                         lightThemeColor1 = lightThemeColors[1],
120                                         lightThemeColor2 = lightThemeColors[2],
121                                         lightThemeColor3 = lightThemeColors[3],
122                                         darkThemeColor0 = darkThemeColors[0],
123                                         darkThemeColor1 = darkThemeColors[1],
124                                         darkThemeColor2 = darkThemeColors[2],
125                                         darkThemeColor3 = darkThemeColors[3],
126                                     ),
127                                 text =
128                                     Text.Loaded(
129                                         colorOption.getContentDescription(context).toString()
130                                     ),
131                                 isTextUserVisible = false,
132                                 isSelected = isSelectedFlow,
133                                 onClicked =
134                                     isSelectedFlow.map { isSelected ->
135                                         if (isSelected) {
136                                             null
137                                         } else {
138                                             {
139                                                 viewModelScope.launch {
140                                                     interactor.select(colorOptionModel)
141                                                     logger.logThemeColorApplied(
142                                                         colorOptionModel.colorOption
143                                                             .sourceForLogging,
144                                                         colorOptionModel.colorOption
145                                                             .styleForLogging,
146                                                         colorOptionModel.colorOption
147                                                             .seedColorForLogging,
148                                                     )
149                                                 }
150                                             }
151                                         }
152                                     },
153                             )
154                         }
155                 }
156                 .toMap()
157         }
158 
159     /** The list of all available color options for the selected Color Type. */
160     val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
161         combine(allColorOptions, selectedColorTypeTabId) {
162             allColorOptions: Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>,
163             selectedColorTypeIdOrNull ->
164             val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR
165             allColorOptions[selectedColorTypeId]!!
166         }
167 
168     /** The list of color options for the color section */
169     val colorSectionOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
170         allColorOptions.map { allColorOptions ->
171             val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR]
172             val presetOptions = allColorOptions[ColorType.PRESET_COLOR]
173             val subOptions =
174                 wallpaperOptions!!.subList(0, min(COLOR_SECTION_OPTION_SIZE, wallpaperOptions.size))
175             // Add additional options based on preset colors if size of wallpaper color options is
176             // less than COLOR_SECTION_OPTION_SIZE
177             val additionalSubOptions =
178                 presetOptions!!.subList(
179                     0,
180                     min(
181                         max(0, COLOR_SECTION_OPTION_SIZE - wallpaperOptions.size),
182                         presetOptions.size,
183                     )
184                 )
185             subOptions + additionalSubOptions
186         }
187 
188     class Factory(
189         private val context: Context,
190         private val interactor: ColorPickerInteractor,
191         private val logger: ThemesUserEventLogger,
192     ) : ViewModelProvider.Factory {
193         override fun <T : ViewModel> create(modelClass: Class<T>): T {
194             @Suppress("UNCHECKED_CAST")
195             return ColorPickerViewModel(
196                 context = context,
197                 interactor = interactor,
198                 logger = logger,
199             )
200                 as T
201         }
202     }
203 
204     companion object {
205         private const val COLOR_SECTION_OPTION_SIZE = 5
206     }
207 }
208