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.domain.interactor 18 19 import android.content.ComponentName 20 import android.content.Context 21 import android.content.Intent 22 import android.os.UserHandle 23 import com.android.systemui.Dumpable 24 import com.android.systemui.ProtoDumpable 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Application 27 import com.android.systemui.dagger.qualifiers.Background 28 import com.android.systemui.dagger.qualifiers.Main 29 import com.android.systemui.dump.nano.SystemUIProtoDump 30 import com.android.systemui.flags.FeatureFlags 31 import com.android.systemui.flags.Flags 32 import com.android.systemui.plugins.qs.QSFactory 33 import com.android.systemui.plugins.qs.QSTile 34 import com.android.systemui.qs.external.CustomTile 35 import com.android.systemui.qs.external.CustomTileStatePersister 36 import com.android.systemui.qs.external.TileLifecycleManager 37 import com.android.systemui.qs.external.TileServiceKey 38 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository 39 import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository 40 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository 41 import com.android.systemui.qs.pipeline.domain.model.TileModel 42 import com.android.systemui.qs.pipeline.shared.TileSpec 43 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger 44 import com.android.systemui.qs.toProto 45 import com.android.systemui.settings.UserTracker 46 import com.android.systemui.user.data.repository.UserRepository 47 import com.android.systemui.util.kotlin.pairwise 48 import java.io.PrintWriter 49 import javax.inject.Inject 50 import kotlinx.coroutines.CoroutineDispatcher 51 import kotlinx.coroutines.CoroutineScope 52 import kotlinx.coroutines.ExperimentalCoroutinesApi 53 import kotlinx.coroutines.flow.MutableStateFlow 54 import kotlinx.coroutines.flow.StateFlow 55 import kotlinx.coroutines.flow.asStateFlow 56 import kotlinx.coroutines.flow.collectLatest 57 import kotlinx.coroutines.flow.combine 58 import kotlinx.coroutines.flow.distinctUntilChanged 59 import kotlinx.coroutines.flow.flatMapLatest 60 import kotlinx.coroutines.flow.flowOn 61 import kotlinx.coroutines.flow.map 62 import kotlinx.coroutines.launch 63 import kotlinx.coroutines.withContext 64 65 /** 66 * Interactor for retrieving the list of current QS tiles, as well as making changes to this list 67 * 68 * It is [ProtoDumpable] as it needs to be able to dump state for CTS tests. 69 */ 70 interface CurrentTilesInteractor : ProtoDumpable { 71 /** Current list of tiles with their corresponding spec. */ 72 val currentTiles: StateFlow<List<TileModel>> 73 74 /** User for the [currentTiles]. */ 75 val userId: StateFlow<Int> 76 77 /** [Context] corresponding to [userId] */ 78 val userContext: StateFlow<Context> 79 80 /** List of specs corresponding to the last value of [currentTiles] */ 81 val currentTilesSpecs: List<TileSpec> 82 get() = currentTiles.value.map(TileModel::spec) 83 84 /** List of tiles corresponding to the last value of [currentTiles] */ 85 val currentQSTiles: List<QSTile> 86 get() = currentTiles.value.map(TileModel::tile) 87 88 /** 89 * Requests that a tile be added in the list of tiles for the current user. 90 * 91 * @see TileSpecRepository.addTile 92 */ 93 fun addTile(spec: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) 94 95 /** 96 * Requests that tiles be removed from the list of tiles for the current user 97 * 98 * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and 99 * marked as removed. 100 * 101 * @see TileSpecRepository.removeTiles 102 */ 103 fun removeTiles(specs: Collection<TileSpec>) 104 105 /** 106 * Requests that the list of tiles for the current user is changed to [specs]. 107 * 108 * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and 109 * marked as removed. 110 * 111 * @see TileSpecRepository.setTiles 112 */ 113 fun setTiles(specs: List<TileSpec>) 114 } 115 116 /** 117 * This implementation of [CurrentTilesInteractor] will try to re-use existing [QSTile] objects when 118 * possible, in particular: 119 * * It will only destroy tiles when they are not part of the list of tiles anymore 120 * * Platform tiles will be kept between users, with a call to [QSTile.userSwitch] 121 * * [CustomTile]s will only be destroyed if the user changes. 122 */ 123 @OptIn(ExperimentalCoroutinesApi::class) 124 @SysUISingleton 125 class CurrentTilesInteractorImpl 126 @Inject 127 constructor( 128 private val tileSpecRepository: TileSpecRepository, 129 private val installedTilesComponentRepository: InstalledTilesComponentRepository, 130 private val userRepository: UserRepository, 131 private val customTileStatePersister: CustomTileStatePersister, 132 private val tileFactory: QSFactory, 133 private val customTileAddedRepository: CustomTileAddedRepository, 134 private val tileLifecycleManagerFactory: TileLifecycleManager.Factory, 135 private val userTracker: UserTracker, 136 @Main private val mainDispatcher: CoroutineDispatcher, 137 @Background private val backgroundDispatcher: CoroutineDispatcher, 138 @Application private val scope: CoroutineScope, 139 private val logger: QSPipelineLogger, 140 featureFlags: FeatureFlags, 141 ) : CurrentTilesInteractor { 142 143 private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> = 144 MutableStateFlow(emptyList()) 145 146 override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow() 147 148 // This variable should only be accessed inside the collect of `startTileCollection`. 149 private val specsToTiles = mutableMapOf<TileSpec, TileOrNotInstalled>() 150 151 private val currentUser = MutableStateFlow(userTracker.userId) 152 override val userId = currentUser.asStateFlow() 153 154 private val _userContext = MutableStateFlow(userTracker.userContext) 155 override val userContext = _userContext.asStateFlow() 156 157 private val userAndTiles = 158 currentUser 159 .flatMapLatest { userId -> 160 tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) } 161 } 162 .distinctUntilChanged() 163 .pairwise(UserAndTiles(-1, emptyList())) 164 .flowOn(backgroundDispatcher) 165 166 private val installedPackagesWithTiles = 167 currentUser.flatMapLatest { 168 installedTilesComponentRepository.getInstalledTilesComponents(it) 169 } 170 171 init { 172 if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { 173 startTileCollection() 174 } 175 } 176 177 @OptIn(ExperimentalCoroutinesApi::class) 178 private fun startTileCollection() { 179 scope.launch { 180 launch { 181 userRepository.selectedUserInfo.collect { user -> 182 currentUser.value = user.id 183 _userContext.value = userTracker.userContext 184 } 185 } 186 187 launch(backgroundDispatcher) { 188 userAndTiles 189 .combine(installedPackagesWithTiles) { usersAndTiles, packages -> 190 Data( 191 usersAndTiles.previousValue, 192 usersAndTiles.newValue, 193 packages, 194 ) 195 } 196 .collectLatest { 197 val newTileList = it.newData.tiles 198 val userChanged = it.oldData.userId != it.newData.userId 199 val newUser = it.newData.userId 200 val components = it.installedComponents 201 202 // Destroy all tiles that are not in the new set 203 specsToTiles 204 .filter { 205 it.key !in newTileList && it.value is TileOrNotInstalled.Tile 206 } 207 .forEach { entry -> 208 logger.logTileDestroyed( 209 entry.key, 210 if (userChanged) { 211 QSPipelineLogger.TileDestroyedReason 212 .TILE_NOT_PRESENT_IN_NEW_USER 213 } else { 214 QSPipelineLogger.TileDestroyedReason.TILE_REMOVED 215 } 216 ) 217 (entry.value as TileOrNotInstalled.Tile).tile.destroy() 218 } 219 // MutableMap will keep the insertion order 220 val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>() 221 222 newTileList.forEach { tileSpec -> 223 if (tileSpec !in newTileMap) { 224 if ( 225 tileSpec is TileSpec.CustomTileSpec && 226 tileSpec.componentName !in components 227 ) { 228 newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled 229 } else { 230 // Create tile here will never try to create a CustomTile that 231 // is not installed 232 val newTile = 233 if (tileSpec in specsToTiles) { 234 processExistingTile( 235 tileSpec, 236 specsToTiles.getValue(tileSpec), 237 userChanged, 238 newUser 239 ) 240 ?: createTile(tileSpec) 241 } else { 242 createTile(tileSpec) 243 } 244 if (newTile != null) { 245 newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile) 246 } 247 } 248 } 249 } 250 251 val resolvedSpecs = newTileMap.keys.toList() 252 specsToTiles.clear() 253 specsToTiles.putAll(newTileMap) 254 _currentSpecsAndTiles.value = 255 newTileMap 256 .filter { it.value is TileOrNotInstalled.Tile } 257 .map { 258 TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) 259 } 260 logger.logTilesNotInstalled( 261 newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, 262 newUser 263 ) 264 if (resolvedSpecs != newTileList) { 265 // There were some tiles that couldn't be created. Change the value in 266 // the 267 // repository 268 launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } 269 } 270 } 271 } 272 } 273 } 274 275 override fun addTile(spec: TileSpec, position: Int) { 276 scope.launch { 277 tileSpecRepository.addTile(userRepository.getSelectedUserInfo().id, spec, position) 278 } 279 } 280 281 override fun removeTiles(specs: Collection<TileSpec>) { 282 val currentSpecsCopy = currentTilesSpecs.toSet() 283 val user = currentUser.value 284 // intersect: tiles that are there and are being removed 285 val toFree = currentSpecsCopy.intersect(specs).filterIsInstance<TileSpec.CustomTileSpec>() 286 toFree.forEach { onCustomTileRemoved(it.componentName, user) } 287 if (currentSpecsCopy.intersect(specs).isNotEmpty()) { 288 // We don't want to do the call to set in case getCurrentTileSpecs is not the most 289 // up to date for this user. 290 scope.launch { tileSpecRepository.removeTiles(user, specs) } 291 } 292 } 293 294 override fun setTiles(specs: List<TileSpec>) { 295 val currentSpecsCopy = currentTilesSpecs 296 val user = currentUser.value 297 if (currentSpecsCopy != specs) { 298 // minus: tiles that were there but are not there anymore 299 val toFree = currentSpecsCopy.minus(specs).filterIsInstance<TileSpec.CustomTileSpec>() 300 toFree.forEach { onCustomTileRemoved(it.componentName, user) } 301 scope.launch { tileSpecRepository.setTiles(user, specs) } 302 } 303 } 304 305 override fun dump(pw: PrintWriter, args: Array<out String>) { 306 pw.println("CurrentTileInteractorImpl:") 307 pw.println("User: ${userId.value}") 308 currentTiles.value 309 .map { it.tile } 310 .filterIsInstance<Dumpable>() 311 .forEach { it.dump(pw, args) } 312 } 313 314 override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) { 315 val data = 316 currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray() 317 systemUIProtoDump.tiles = data 318 } 319 320 private fun onCustomTileRemoved(componentName: ComponentName, userId: Int) { 321 val intent = Intent().setComponent(componentName) 322 val lifecycleManager = tileLifecycleManagerFactory.create(intent, UserHandle.of(userId)) 323 lifecycleManager.onStopListening() 324 lifecycleManager.onTileRemoved() 325 customTileStatePersister.removeState(TileServiceKey(componentName, userId)) 326 customTileAddedRepository.setTileAdded(componentName, userId, false) 327 lifecycleManager.flushMessagesAndUnbind() 328 } 329 330 private suspend fun createTile(spec: TileSpec): QSTile? { 331 val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) } 332 if (tile == null) { 333 logger.logTileNotFoundInFactory(spec) 334 return null 335 } else { 336 tile.tileSpec = spec.spec 337 return if (!tile.isAvailable) { 338 logger.logTileDestroyed( 339 spec, 340 QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE, 341 ) 342 tile.destroy() 343 null 344 } else { 345 logger.logTileCreated(spec) 346 tile 347 } 348 } 349 } 350 351 private fun processExistingTile( 352 tileSpec: TileSpec, 353 tileOrNotInstalled: TileOrNotInstalled, 354 userChanged: Boolean, 355 user: Int, 356 ): QSTile? { 357 return when (tileOrNotInstalled) { 358 is TileOrNotInstalled.NotInstalled -> null 359 is TileOrNotInstalled.Tile -> { 360 val qsTile = tileOrNotInstalled.tile 361 when { 362 !qsTile.isAvailable -> { 363 logger.logTileDestroyed( 364 tileSpec, 365 QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE 366 ) 367 qsTile.destroy() 368 null 369 } 370 // Tile is in the current list of tiles and available. 371 // We have a handful of different cases 372 qsTile !is CustomTile -> { 373 // The tile is not a custom tile. Make sure they are reset to the correct 374 // user 375 if (userChanged) { 376 qsTile.userSwitch(user) 377 logger.logTileUserChanged(tileSpec, user) 378 } 379 qsTile 380 } 381 qsTile.user == user -> { 382 // The tile is a custom tile for the same user, just return it 383 qsTile 384 } 385 else -> { 386 // The tile is a custom tile and the user has changed. Destroy it 387 qsTile.destroy() 388 logger.logTileDestroyed( 389 tileSpec, 390 QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED 391 ) 392 null 393 } 394 } 395 } 396 } 397 } 398 399 private sealed interface TileOrNotInstalled { 400 object NotInstalled : TileOrNotInstalled 401 402 @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled 403 } 404 405 private data class UserAndTiles( 406 val userId: Int, 407 val tiles: List<TileSpec>, 408 ) 409 410 private data class Data( 411 val oldData: UserAndTiles, 412 val newData: UserAndTiles, 413 val installedComponents: Set<ComponentName>, 414 ) 415 } 416