1 /* 2 * Copyright (C) 2023 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.pipeline.data.repository 18 19 import android.annotation.UserIdInt 20 import android.content.res.Resources 21 import android.database.ContentObserver 22 import android.provider.Settings 23 import com.android.systemui.R 24 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Background 27 import com.android.systemui.dagger.qualifiers.Main 28 import com.android.systemui.qs.QSHost 29 import com.android.systemui.qs.pipeline.shared.TileSpec 30 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger 31 import com.android.systemui.retail.data.repository.RetailModeRepository 32 import com.android.systemui.util.settings.SecureSettings 33 import javax.inject.Inject 34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.ExperimentalCoroutinesApi 36 import kotlinx.coroutines.channels.awaitClose 37 import kotlinx.coroutines.flow.Flow 38 import kotlinx.coroutines.flow.distinctUntilChanged 39 import kotlinx.coroutines.flow.flatMapLatest 40 import kotlinx.coroutines.flow.flowOf 41 import kotlinx.coroutines.flow.flowOn 42 import kotlinx.coroutines.flow.map 43 import kotlinx.coroutines.flow.onEach 44 import kotlinx.coroutines.flow.onStart 45 import kotlinx.coroutines.sync.Mutex 46 import kotlinx.coroutines.sync.withLock 47 import kotlinx.coroutines.withContext 48 49 /** Repository that tracks the current tiles. */ 50 interface TileSpecRepository { 51 52 /** 53 * Returns a flow of the current list of [TileSpec] for a given [userId]. 54 * 55 * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty. 56 */ 57 fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>> 58 59 /** 60 * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile 61 * at the end of the list. 62 * 63 * Passing [TileSpec.Invalid] is a noop. 64 * 65 * Trying to add a tile beyond the end of the list will add it at the end. 66 */ 67 suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END) 68 69 /** 70 * Removes a [tile] for a given [userId]. 71 * 72 * Passing [TileSpec.Invalid] or a non present tile is a noop. 73 */ 74 suspend fun removeTiles(@UserIdInt userId: Int, tiles: Collection<TileSpec>) 75 76 /** 77 * Sets the list of current [tiles] for a given [userId]. 78 * 79 * [TileSpec.Invalid] will be ignored, and an effectively empty list will not be stored. 80 */ 81 suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>) 82 83 companion object { 84 /** Position to indicate the end of the list */ 85 const val POSITION_AT_END = -1 86 } 87 } 88 89 /** 90 * Implementation of [TileSpecRepository] that persist the values of tiles in 91 * [Settings.Secure.QS_TILES]. 92 * 93 * All operations against [Settings] will be performed in a background thread. 94 * 95 * If the device is in retail mode, the tiles are fixed to the value of 96 * [R.string.quick_settings_tiles_retail_mode]. 97 */ 98 @SysUISingleton 99 class TileSpecSettingsRepository 100 @Inject 101 constructor( 102 private val secureSettings: SecureSettings, 103 @Main private val resources: Resources, 104 private val logger: QSPipelineLogger, 105 private val retailModeRepository: RetailModeRepository, 106 @Background private val backgroundDispatcher: CoroutineDispatcher, 107 ) : TileSpecRepository { 108 109 private val mutex = Mutex() 110 111 private val retailModeTiles by lazy { 112 resources 113 .getString(R.string.quick_settings_tiles_retail_mode) 114 .split(DELIMITER) 115 .map(TileSpec::create) 116 .filter { it !is TileSpec.Invalid } 117 } 118 119 @OptIn(ExperimentalCoroutinesApi::class) 120 override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { 121 return retailModeRepository.retailMode.flatMapLatest { inRetailMode -> 122 if (inRetailMode) { 123 logger.logUsingRetailTiles() 124 flowOf(retailModeTiles) 125 } else { 126 settingsTiles(userId) 127 } 128 } 129 } 130 131 private fun settingsTiles(userId: Int): Flow<List<TileSpec>> { 132 return conflatedCallbackFlow { 133 val observer = 134 object : ContentObserver(null) { 135 override fun onChange(selfChange: Boolean) { 136 trySend(Unit) 137 } 138 } 139 140 secureSettings.registerContentObserverForUser(SETTING, observer, userId) 141 142 awaitClose { secureSettings.unregisterContentObserver(observer) } 143 } 144 .onStart { emit(Unit) } 145 .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } 146 .distinctUntilChanged() 147 .onEach { logger.logTilesChangedInSettings(it, userId) } 148 .map { parseTileSpecs(it, userId) } 149 .flowOn(backgroundDispatcher) 150 } 151 152 override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) = 153 mutex.withLock { 154 if (tile == TileSpec.Invalid) { 155 return 156 } 157 val tilesList = loadTiles(userId).toMutableList() 158 if (tile !in tilesList) { 159 if (position < 0 || position >= tilesList.size) { 160 tilesList.add(tile) 161 } else { 162 tilesList.add(position, tile) 163 } 164 storeTiles(userId, tilesList) 165 } 166 } 167 168 override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) = 169 mutex.withLock { 170 if (tiles.all { it == TileSpec.Invalid }) { 171 return 172 } 173 val tilesList = loadTiles(userId).toMutableList() 174 if (tilesList.removeAll(tiles)) { 175 storeTiles(userId, tilesList.toList()) 176 } 177 } 178 179 override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) = 180 mutex.withLock { 181 val filtered = tiles.filter { it != TileSpec.Invalid } 182 if (filtered.isNotEmpty()) { 183 storeTiles(userId, filtered) 184 } 185 } 186 187 private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> { 188 return withContext(backgroundDispatcher) { 189 (secureSettings.getStringForUser(SETTING, forUser) ?: "") 190 .split(DELIMITER) 191 .map(TileSpec::create) 192 .filter { it !is TileSpec.Invalid } 193 } 194 } 195 196 private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) { 197 if (retailModeRepository.inRetailMode) { 198 // No storing tiles when in retail mode 199 return 200 } 201 val toStore = 202 tiles 203 .filter { it !is TileSpec.Invalid } 204 .joinToString(DELIMITER, transform = TileSpec::spec) 205 withContext(backgroundDispatcher) { 206 secureSettings.putStringForUser( 207 SETTING, 208 toStore, 209 null, 210 false, 211 forUser, 212 true, 213 ) 214 } 215 } 216 217 private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> { 218 val fromSettings = 219 tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter { 220 it != TileSpec.Invalid 221 } 222 return if (fromSettings.isNotEmpty()) { 223 fromSettings.also { logger.logParsedTiles(it, false, user) } 224 } else { 225 QSHost.getDefaultSpecs(resources) 226 .map(TileSpec::create) 227 .filter { it != TileSpec.Invalid } 228 .also { logger.logParsedTiles(it, true, user) } 229 } 230 } 231 232 companion object { 233 private const val SETTING = Settings.Secure.QS_TILES 234 private const val DELIMITER = "," 235 } 236 } 237