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