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