• 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 android.content.res.Resources
21 import android.graphics.drawable.Drawable
22 import com.android.customization.model.ResourceConstants
23 import com.android.customization.model.grid.GridOptionModel
24 import com.android.customization.model.grid.ShapeOptionModel
25 import com.android.customization.picker.grid.domain.interactor.ShapeGridInteractor
26 import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel
27 import com.android.customization.widget.GridTileDrawable
28 import com.android.themepicker.R
29 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
30 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
31 import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel
32 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2
33 import dagger.assisted.Assisted
34 import dagger.assisted.AssistedFactory
35 import dagger.assisted.AssistedInject
36 import dagger.hilt.android.qualifiers.ApplicationContext
37 import dagger.hilt.android.scopes.ViewModelScoped
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.MutableStateFlow
41 import kotlinx.coroutines.flow.SharingStarted
42 import kotlinx.coroutines.flow.StateFlow
43 import kotlinx.coroutines.flow.asStateFlow
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.filterNotNull
46 import kotlinx.coroutines.flow.map
47 import kotlinx.coroutines.flow.shareIn
48 import kotlinx.coroutines.flow.stateIn
49 
50 class ShapeGridPickerViewModel
51 @AssistedInject
52 constructor(
53     @ApplicationContext private val context: Context,
54     private val interactor: ShapeGridInteractor,
55     @Assisted private val viewModelScope: CoroutineScope,
56 ) {
57 
58     enum class Tab {
59         SHAPE,
60         GRID,
61     }
62 
63     //// Tabs
64     private val _selectedTab = MutableStateFlow(Tab.SHAPE)
65     val selectedTab: StateFlow<Tab> = _selectedTab.asStateFlow()
66     val tabs: Flow<List<FloatingToolbarTabViewModel>> =
67         _selectedTab.map {
68             listOf(
69                 FloatingToolbarTabViewModel(
70                     Icon.Resource(
71                         res = R.drawable.ic_category_filled_24px,
72                         contentDescription = Text.Resource(R.string.preview_name_shape),
73                     ),
74                     context.getString(R.string.preview_name_shape),
75                     it == Tab.SHAPE,
76                 ) {
77                     _selectedTab.value = Tab.SHAPE
78                 },
79                 FloatingToolbarTabViewModel(
80                     Icon.Resource(
81                         res = R.drawable.ic_apps_filled_24px,
82                         contentDescription = Text.Resource(R.string.grid_layout),
83                     ),
84                     context.getString(R.string.grid_layout),
85                     it == Tab.GRID,
86                 ) {
87                     _selectedTab.value = Tab.GRID
88                 },
89             )
90         }
91 
92     //// Shape
93 
94     // The currently-set system shape option
95     val selectedShapeKey =
96         interactor.selectedShapeOption
97             .filterNotNull()
98             .map { it.key }
99             .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
100     private val overridingShapeKey = MutableStateFlow<String?>(null)
101     // If the overriding key is null, use the currently-set system shape option
102     val previewingShapeKey =
103         combine(overridingShapeKey, selectedShapeKey) { overridingShapeOptionKey, selectedShapeKey
104             ->
105             overridingShapeOptionKey ?: selectedShapeKey
106         }
107 
108     val shapeOptions: Flow<List<OptionItemViewModel2<ShapeIconViewModel>>> =
109         interactor.shapeOptions
110             .filterNotNull()
111             .map { shapeOptions -> shapeOptions.map { toShapeOptionItemViewModel(it) } }
112             .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
113 
114     //// Grid
115 
116     // The currently-set system grid option
117     val selectedGridOption =
118         interactor.selectedGridOption
119             .filterNotNull()
120             .map { toGridOptionItemViewModel(it) }
121             .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
122     private val overridingGridKey = MutableStateFlow<String?>(null)
123     // If the overriding key is null, use the currently-set system grid option
124     val previewingGridKey =
125         combine(overridingGridKey, selectedGridOption) { overridingGridOptionKey, selectedGridOption
126             ->
127             overridingGridOptionKey ?: selectedGridOption.key.value
128         }
129 
130     val gridOptions: Flow<List<OptionItemViewModel2<Drawable>>> =
131         interactor.gridOptions
132             .filterNotNull()
133             .map { gridOptions -> gridOptions.map { toGridOptionItemViewModel(it) } }
134             .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
135 
136     val onApply: Flow<(suspend () -> Unit)?> =
137         combine(overridingGridKey, selectedGridOption, overridingShapeKey, selectedShapeKey) {
138             overridingGridKey,
139             selectedGridOption,
140             overridingShapeKey,
141             selectedShapeKey ->
142             if (
143                 (overridingGridKey != null && overridingGridKey != selectedGridOption.key.value) ||
144                     (overridingShapeKey != null && overridingShapeKey != selectedShapeKey)
145             ) {
146                 {
147                     interactor.applySelectedOption(
148                         overridingShapeKey ?: selectedShapeKey,
149                         overridingGridKey ?: selectedGridOption.key.value,
150                     )
151                 }
152             } else {
153                 null
154             }
155         }
156 
157     fun resetPreview() {
158         overridingShapeKey.value = null
159         overridingGridKey.value = null
160         _selectedTab.value = Tab.SHAPE
161     }
162 
163     private fun toShapeOptionItemViewModel(
164         option: ShapeOptionModel
165     ): OptionItemViewModel2<ShapeIconViewModel> {
166         val isSelected =
167             previewingShapeKey
168                 .map { it == option.key }
169                 .stateIn(
170                     scope = viewModelScope,
171                     started = SharingStarted.Lazily,
172                     initialValue = false,
173                 )
174 
175         return OptionItemViewModel2(
176             key = MutableStateFlow(option.key),
177             payload = ShapeIconViewModel(option.key, option.path),
178             text = Text.Loaded(option.title),
179             isSelected = isSelected,
180             onClicked =
181                 isSelected.map {
182                     if (!it) {
183                         { overridingShapeKey.value = option.key }
184                     } else {
185                         null
186                     }
187                 },
188         )
189     }
190 
191     private fun toGridOptionItemViewModel(option: GridOptionModel): OptionItemViewModel2<Drawable> {
192         // Fallback to use GridTileDrawable when no resource found for the icon ID
193         val drawable =
194             interactor.getGridOptionDrawable(option.iconId)
195                 ?: GridTileDrawable(
196                     option.cols,
197                     option.rows,
198                     context.resources.getString(
199                         Resources.getSystem()
200                             .getIdentifier(
201                                 ResourceConstants.CONFIG_ICON_MASK,
202                                 "string",
203                                 ResourceConstants.ANDROID_PACKAGE,
204                             )
205                     ),
206                 )
207         val isSelected =
208             previewingGridKey
209                 .map { it == option.key }
210                 .stateIn(
211                     scope = viewModelScope,
212                     started = SharingStarted.Lazily,
213                     initialValue = false,
214                 )
215         return OptionItemViewModel2(
216             key = MutableStateFlow(option.key),
217             payload = drawable,
218             text = Text.Loaded(option.title),
219             isSelected = isSelected,
220             onClicked =
221                 isSelected.map {
222                     if (!it) {
223                         { overridingGridKey.value = option.key }
224                     } else {
225                         null
226                     }
227                 },
228         )
229     }
230 
231     @ViewModelScoped
232     @AssistedFactory
233     interface Factory {
234         fun create(viewModelScope: CoroutineScope): ShapeGridPickerViewModel
235     }
236 }
237