<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