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