• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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