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