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