<lambda>null1 package com.android.systemui.qs.pipeline.data.repository
2 
3 import android.annotation.UserIdInt
4 import android.database.ContentObserver
5 import android.provider.Settings
6 import com.android.systemui.common.coroutine.ConflatedCallbackFlow
7 import com.android.systemui.dagger.qualifiers.Application
8 import com.android.systemui.dagger.qualifiers.Background
9 import com.android.systemui.qs.pipeline.data.model.RestoreData
10 import com.android.systemui.qs.pipeline.shared.TileSpec
11 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
12 import com.android.systemui.util.settings.SecureSettings
13 import dagger.assisted.Assisted
14 import dagger.assisted.AssistedFactory
15 import dagger.assisted.AssistedInject
16 import kotlinx.coroutines.CoroutineDispatcher
17 import kotlinx.coroutines.CoroutineScope
18 import kotlinx.coroutines.channels.awaitClose
19 import kotlinx.coroutines.flow.Flow
20 import kotlinx.coroutines.flow.MutableSharedFlow
21 import kotlinx.coroutines.flow.StateFlow
22 import kotlinx.coroutines.flow.flowOn
23 import kotlinx.coroutines.flow.map
24 import kotlinx.coroutines.flow.scan
25 import kotlinx.coroutines.flow.stateIn
26 import kotlinx.coroutines.launch
27 import kotlinx.coroutines.withContext
28 
29 /**
30  * Single user version of [TileSpecRepository]. It provides a similar interface as
31  * [TileSpecRepository], but focusing solely on the user it was created for.
32  *
33  * This is the source of truth for that user's tiles, after the user has been started. Persisting
34  * all the changes to [Settings]. Changes in [Settings] that disagree with this repository will be
35  * reverted
36  *
37  * All operations against [Settings] will be performed in a background thread.
38  */
39 class UserTileSpecRepository
40 @AssistedInject
41 constructor(
42     @Assisted private val userId: Int,
43     private val defaultTilesRepository: DefaultTilesRepository,
44     private val secureSettings: SecureSettings,
45     private val logger: QSPipelineLogger,
46     @Application private val applicationScope: CoroutineScope,
47     @Background private val backgroundDispatcher: CoroutineDispatcher,
48 ) {
49 
50     private val defaultTiles: List<TileSpec>
51         get() = defaultTilesRepository.defaultTiles
52 
53     private val changeEvents =
54         MutableSharedFlow<ChangeAction>(extraBufferCapacity = CHANGES_BUFFER_SIZE)
55 
56     private lateinit var _tiles: StateFlow<List<TileSpec>>
57 
58     suspend fun tiles(): Flow<List<TileSpec>> {
59         if (!::_tiles.isInitialized) {
60             _tiles =
61                 changeEvents
62                     .scan(loadTilesFromSettingsAndParse(userId)) { current, change ->
63                         change
64                             .apply(current)
65                             .also {
66                                 if (current != it) {
67                                     if (change is RestoreTiles) {
68                                         logger.logTilesRestoredAndReconciled(current, it, userId)
69                                     } else {
70                                         logger.logProcessTileChange(change, it, userId)
71                                     }
72                                 }
73                             }
74                             // Distinct preserves the order of the elements removing later
75                             // duplicates,
76                             // all tiles should be different
77                             .distinct()
78                     }
79                     .flowOn(backgroundDispatcher)
80                     .stateIn(applicationScope)
81                     .also { startFlowCollections(it) }
82         }
83         return _tiles
84     }
85 
86     private fun startFlowCollections(tiles: StateFlow<List<TileSpec>>) {
87         applicationScope.launch(backgroundDispatcher) {
88             launch { tiles.collect { storeTiles(userId, it) } }
89             launch {
90                 // As Settings is not the source of truth, once we started tracking tiles for a
91                 // user, we don't want anyone to change the underlying setting. Therefore, if there
92                 // are any changes that don't match with the source of truth (this class), we
93                 // overwrite them with the current value.
94                 ConflatedCallbackFlow.conflatedCallbackFlow {
95                         val observer =
96                             object : ContentObserver(null) {
97                                 override fun onChange(selfChange: Boolean) {
98                                     trySend(Unit)
99                                 }
100                             }
101                         secureSettings.registerContentObserverForUserSync(SETTING, observer, userId)
102                         awaitClose { secureSettings.unregisterContentObserverSync(observer) }
103                     }
104                     .map { loadTilesFromSettings(userId) }
105                     .flowOn(backgroundDispatcher)
106                     .collect { setting ->
107                         val current = tiles.value
108                         if (setting != current) {
109                             storeTiles(userId, current)
110                         }
111                     }
112             }
113         }
114     }
115 
116     private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
117         val toStore =
118             tiles
119                 .filter { it !is TileSpec.Invalid }
120                 .joinToString(DELIMITER, transform = TileSpec::spec)
121         withContext(backgroundDispatcher) {
122             secureSettings.putStringForUser(
123                 SETTING,
124                 toStore,
125                 null,
126                 false,
127                 forUser,
128                 true,
129             )
130         }
131     }
132 
133     suspend fun addTile(tile: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) {
134         if (tile is TileSpec.Invalid) {
135             return
136         }
137         changeEvents.emit(AddTile(tile, position))
138     }
139 
140     suspend fun removeTiles(tiles: Collection<TileSpec>) {
141         changeEvents.emit(RemoveTiles(tiles))
142     }
143 
144     suspend fun setTiles(tiles: List<TileSpec>) {
145         changeEvents.emit(ChangeTiles(tiles))
146     }
147 
148     private fun parseTileSpecs(fromSettings: List<TileSpec>, user: Int): List<TileSpec> {
149         return if (fromSettings.isNotEmpty()) {
150             fromSettings.also { logger.logParsedTiles(it, false, user) }
151         } else {
152             defaultTiles.also { logger.logParsedTiles(it, true, user) }
153         }
154     }
155 
156     private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> {
157         return parseTileSpecs(loadTilesFromSettings(userId), userId)
158     }
159 
160     private suspend fun loadTilesFromSettings(userId: Int): List<TileSpec> {
161         return withContext(backgroundDispatcher) {
162                 secureSettings.getStringForUser(SETTING, userId) ?: ""
163             }
164             .toTilesList()
165     }
166 
167     suspend fun reconcileRestore(restoreData: RestoreData, currentAutoAdded: Set<TileSpec>) {
168         changeEvents.emit(RestoreTiles(restoreData, currentAutoAdded))
169     }
170 
171     suspend fun prependDefault() {
172         changeEvents.emit(PrependDefault(defaultTiles))
173     }
174 
175     sealed interface ChangeAction {
176         fun apply(currentTiles: List<TileSpec>): List<TileSpec>
177     }
178 
179     private data class AddTile(
180         val tileSpec: TileSpec,
181         val position: Int = TileSpecRepository.POSITION_AT_END
182     ) : ChangeAction {
183         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
184             val tilesList = currentTiles.toMutableList()
185             if (tileSpec !in tilesList) {
186                 if (position < 0 || position >= tilesList.size) {
187                     tilesList.add(tileSpec)
188                 } else {
189                     tilesList.add(position, tileSpec)
190                 }
191             }
192             return tilesList
193         }
194     }
195 
196     private data class RemoveTiles(val tileSpecs: Collection<TileSpec>) : ChangeAction {
197         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
198             return currentTiles.toMutableList().apply { removeAll(tileSpecs) }
199         }
200     }
201 
202     private data class ChangeTiles(
203         val newTiles: List<TileSpec>,
204     ) : ChangeAction {
205         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
206             val new = newTiles.filter { it !is TileSpec.Invalid }
207             return if (new.isNotEmpty()) new else currentTiles
208         }
209     }
210 
211     private data class PrependDefault(val defaultTiles: List<TileSpec>) : ChangeAction {
212         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
213             return defaultTiles + currentTiles
214         }
215     }
216 
217     private data class RestoreTiles(
218         val restoreData: RestoreData,
219         val currentAutoAdded: Set<TileSpec>,
220     ) : ChangeAction {
221 
222         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
223             return reconcileTiles(currentTiles, currentAutoAdded, restoreData)
224         }
225     }
226 
227     companion object {
228         private const val SETTING = Settings.Secure.QS_TILES
229         private const val DELIMITER = TilesSettingConverter.DELIMITER
230         // We want a small buffer in case multiple changes come in at the same time (sometimes
231         // happens in first start. This should be enough to not lose changes.
232         private const val CHANGES_BUFFER_SIZE = 10
233 
234         private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
235 
236         fun reconcileTiles(
237             currentTiles: List<TileSpec>,
238             currentAutoAdded: Set<TileSpec>,
239             restoreData: RestoreData
240         ): List<TileSpec> {
241             val toRestore = restoreData.restoredTiles.toMutableList()
242             val freshlyAutoAdded =
243                 currentAutoAdded.filterNot { it in restoreData.restoredAutoAddedTiles }
244             freshlyAutoAdded
245                 .filter { it in currentTiles && it !in restoreData.restoredTiles }
246                 .map { it to currentTiles.indexOf(it) }
247                 .sortedBy { it.second }
248                 .forEachIndexed { iteration, (tile, position) ->
249                     val insertAt = position + iteration
250                     if (insertAt > toRestore.size) {
251                         toRestore.add(tile)
252                     } else {
253                         toRestore.add(insertAt, tile)
254                     }
255                 }
256 
257             return toRestore
258         }
259     }
260 
261     @AssistedFactory
262     interface Factory {
263         fun create(
264             userId: Int,
265         ): UserTileSpecRepository
266     }
267 }
268