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.systemui.qs.panels.ui.viewmodel
18 
19 import com.android.systemui.dagger.SysUISingleton
20 import com.android.systemui.dagger.qualifiers.Application
21 import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
22 import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor
23 import com.android.systemui.qs.panels.domain.interactor.TilesAvailabilityInteractor
24 import com.android.systemui.qs.panels.shared.model.GridLayoutType
25 import com.android.systemui.qs.panels.ui.compose.GridLayout
26 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
27 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
28 import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
29 import com.android.systemui.qs.pipeline.shared.TileSpec
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.ExperimentalCoroutinesApi
32 import kotlinx.coroutines.flow.MutableStateFlow
33 import kotlinx.coroutines.flow.SharingStarted
34 import kotlinx.coroutines.flow.StateFlow
35 import kotlinx.coroutines.flow.asStateFlow
36 import kotlinx.coroutines.flow.emptyFlow
37 import kotlinx.coroutines.flow.flatMapLatest
38 import kotlinx.coroutines.flow.map
39 import kotlinx.coroutines.flow.stateIn
40 import javax.inject.Inject
41 import javax.inject.Named
42 
43 @SysUISingleton
44 @OptIn(ExperimentalCoroutinesApi::class)
45 class EditModeViewModel
46 @Inject
47 constructor(
48         private val editTilesListInteractor: EditTilesListInteractor,
49         private val currentTilesInteractor: CurrentTilesInteractor,
50         private val tilesAvailabilityInteractor: TilesAvailabilityInteractor,
51         private val minTilesInteractor: MinimumTilesInteractor,
52         @Named("Default") private val defaultGridLayout: GridLayout,
53         @Application private val applicationScope: CoroutineScope,
54         gridLayoutTypeInteractor: GridLayoutTypeInteractor,
55         gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>,
56 ) {
57     private val _isEditing = MutableStateFlow(false)
58 
59     /**
60      * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this
61      */
62     val isEditing = _isEditing.asStateFlow()
63     private val minimumTiles: Int
64         get() = minTilesInteractor.minNumberOfTiles
65 
66     val gridLayout: StateFlow<GridLayout> =
67         gridLayoutTypeInteractor.layout
68             .map { gridLayoutMap[it] ?: defaultGridLayout }
69             .stateIn(
70                 applicationScope,
71                 SharingStarted.WhileSubscribed(),
72                 defaultGridLayout,
73             )
74 
75     /**
76      * Flow of view models for each tile that should be visible in edit mode (or empty flow when not
77      * editing).
78      *
79      * Guarantees of the data:
80      * * The data for the tiles is fetched once whenever [isEditing] goes from `false` to `true`.
81      *   This prevents icons/labels changing while in edit mode.
82      * * It tracks the current tiles as they are added/removed/moved by the user.
83      * * The tiles that are current will be in the same relative order as the user sees them in
84      *   Quick Settings.
85      * * The tiles that are not current will preserve their relative order even when the current
86      *   tiles change.
87      * * Tiles that are not available will be filtered out. None of them can be current (as they
88      *   cannot be created), and they won't be able to be added.
89      */
90     val tiles =
91         isEditing.flatMapLatest {
92             if (it) {
93                 val editTilesData = editTilesListInteractor.getTilesToEdit()
94                 // Query only the non current platform tiles, as any current tile is clearly
95                 // available
96                 val unavailable = tilesAvailabilityInteractor.getUnavailableTiles(
97                         editTilesData.stockTiles.map { it.tileSpec }
98                                 .minus(currentTilesInteractor.currentTilesSpecs.toSet())
99                 )
100                 currentTilesInteractor.currentTiles.map { tiles ->
101                     val currentSpecs = tiles.map { it.spec }
102                     val canRemoveTiles = currentSpecs.size > minimumTiles
103                     val allTiles = editTilesData.stockTiles + editTilesData.customTiles
104                     val allTilesMap = allTiles.associate { it.tileSpec to it }
105                     val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
106                     val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
107 
108                     (currentTiles + nonCurrentTiles)
109                             .filterNot { it.tileSpec in unavailable }
110                             .map {
111                                 val current = it.tileSpec in currentSpecs
112                                 val availableActions = buildSet {
113                                     if (current) {
114                                         add(AvailableEditActions.MOVE)
115                                         if (canRemoveTiles) {
116                                             add(AvailableEditActions.REMOVE)
117                                         }
118                                     } else {
119                                         add(AvailableEditActions.ADD)
120                                     }
121                                 }
122                                 EditTileViewModel(
123                                         it.tileSpec,
124                                         it.icon,
125                                         it.label,
126                                         it.appName,
127                                         current,
128                                         availableActions
129                                 )
130                             }
131                 }
132             } else {
133                 emptyFlow()
134             }
135         }
136 
137     /** @see isEditing */
138     fun startEditing() {
139         _isEditing.value = true
140     }
141 
142     /** @see isEditing */
143     fun stopEditing() {
144         _isEditing.value = false
145     }
146 
147     /** Immediately moves [tileSpec] to [position]. */
148     fun moveTile(tileSpec: TileSpec, position: Int) {
149         throw NotImplementedError("This is not supported yet")
150     }
151 
152     /** Immediately adds [tileSpec] to the current tiles at [position]. */
153     fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
154         currentTilesInteractor.addTile(tileSpec, position)
155     }
156 
157     /** Immediately removes [tileSpec] from the current tiles. */
158     fun removeTile(tileSpec: TileSpec) {
159         currentTilesInteractor.removeTiles(listOf(tileSpec))
160     }
161 
162     /** Immediately resets the current tiles to the default list. */
163     fun resetCurrentTilesToDefault() {
164         throw NotImplementedError("This is not supported yet")
165     }
166 }
167