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(interactor.colorOptions, selectedColorTypeTabId) {
53             colorOptions,
54             selectedColorTypeIdOrNull ->
55             colorOptions.keys
56                 .mapIndexed { index, colorType ->
57                     val isSelected =
58                         (selectedColorTypeIdOrNull == null && index == 0) ||
59                             selectedColorTypeIdOrNull == colorType
60                     colorType to
61                         ColorTypeTabViewModel(
62                             name =
63                                 when (colorType) {
64                                     ColorType.WALLPAPER_COLOR ->
65                                         context.resources.getString(R.string.wallpaper_color_tab)
66                                     ColorType.PRESET_COLOR ->
67                                         context.resources.getString(R.string.preset_color_tab_2)
68                                 },
69                             isSelected = isSelected,
70                             onClick =
71                                 if (isSelected) {
72                                     null
73                                 } else {
74                                     { this.selectedColorTypeTabId.value = colorType }
75                                 },
76                         )
77                 }
78                 .toMap()
79         }
80 
81     /** View-models for each color tab subheader */
82     val colorTypeTabSubheader: Flow<String> =
83         selectedColorTypeTabId.map { selectedColorTypeIdOrNull ->
84             when (selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR) {
85                 ColorType.WALLPAPER_COLOR ->
86                     context.resources.getString(R.string.wallpaper_color_subheader)
87                 ColorType.PRESET_COLOR ->
88                     context.resources.getString(R.string.preset_color_subheader)
89             }
90         }
91 
92     /** The list of all color options mapped by their color type */
93     private val allColorOptions:
94         Flow<Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>> =
95         interactor.colorOptions.map { colorOptions ->
96             colorOptions
97                 .map { colorOptionEntry ->
98                     colorOptionEntry.key to
99                         colorOptionEntry.value.map { colorOptionModel ->
100                             val colorOption: ColorOptionImpl =
101                                 colorOptionModel.colorOption as ColorOptionImpl
102                             val lightThemeColors =
103                                 colorOption.previewInfo.resolveColors(/* darkTheme= */ false)
104                             val darkThemeColors =
105                                 colorOption.previewInfo.resolveColors(/* darkTheme= */ true)
106                             val isSelectedFlow: StateFlow<Boolean> =
107                                 interactor.selectingColorOption
108                                     .map {
109                                         it?.colorOption?.isEquivalent(colorOptionModel.colorOption)
110                                             ?: colorOptionModel.isSelected
111                                     }
112                                     .stateIn(viewModelScope)
113                             OptionItemViewModel<ColorOptionIconViewModel>(
114                                 key = MutableStateFlow(colorOptionModel.key) as StateFlow<String>,
115                                 payload =
116                                     ColorOptionIconViewModel(
117                                         lightThemeColor0 = lightThemeColors[0],
118                                         lightThemeColor1 = lightThemeColors[1],
119                                         lightThemeColor2 = lightThemeColors[2],
120                                         lightThemeColor3 = lightThemeColors[3],
121                                         darkThemeColor0 = darkThemeColors[0],
122                                         darkThemeColor1 = darkThemeColors[1],
123                                         darkThemeColor2 = darkThemeColors[2],
124                                         darkThemeColor3 = darkThemeColors[3],
125                                     ),
126                                 text =
127                                     Text.Loaded(
128                                         colorOption.getContentDescription(context).toString()
129                                     ),
130                                 isTextUserVisible = false,
131                                 isSelected = isSelectedFlow,
132                                 onClicked =
133                                     isSelectedFlow.map { isSelected ->
134                                         if (isSelected) {
135                                             null
136                                         } else {
137                                             {
138                                                 viewModelScope.launch {
139                                                     interactor.select(colorOptionModel)
140                                                     logger.logThemeColorApplied(
141                                                         colorOptionModel.colorOption
142                                                             .sourceForLogging,
143                                                         colorOptionModel.colorOption
144                                                             .styleForLogging,
145                                                         colorOptionModel.colorOption.seedColor,
146                                                     )
147                                                 }
148                                             }
149                                         }
150                                     },
151                             )
152                         }
153                 }
154                 .toMap()
155         }
156 
157     /** The list of all available color options for the selected Color Type. */
158     val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
159         combine(allColorOptions, selectedColorTypeTabId) {
160             allColorOptions: Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>,
161             selectedColorTypeIdOrNull ->
162             val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR
163             allColorOptions[selectedColorTypeId]!!
164         }
165 
166     /** The list of color options for the color section */
167     val colorSectionOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
168         allColorOptions.map { allColorOptions ->
169             val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR]
170             val presetOptions = allColorOptions[ColorType.PRESET_COLOR]
171             val subOptions =
172                 wallpaperOptions!!.subList(0, min(COLOR_SECTION_OPTION_SIZE, wallpaperOptions.size))
173             // Add additional options based on preset colors if size of wallpaper color options is
174             // less than COLOR_SECTION_OPTION_SIZE
175             val additionalSubOptions =
176                 presetOptions!!.subList(
177                     0,
178                     min(
179                         max(0, COLOR_SECTION_OPTION_SIZE - wallpaperOptions.size),
180                         presetOptions.size,
181                     ),
182                 )
183             subOptions + additionalSubOptions
184         }
185 
186     class Factory(
187         private val context: Context,
188         private val interactor: ColorPickerInteractor,
189         private val logger: ThemesUserEventLogger,
190     ) : ViewModelProvider.Factory {
191         override fun <T : ViewModel> create(modelClass: Class<T>): T {
192             @Suppress("UNCHECKED_CAST")
193             return ColorPickerViewModel(context = context, interactor = interactor, logger = logger)
194                 as T
195         }
196     }
197 
198     companion object {
199         private const val COLOR_SECTION_OPTION_SIZE = 6
200     }
201 }
202