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.data.repository
18 
19 import android.annotation.UserIdInt
20 import android.content.res.Resources
21 import android.database.ContentObserver
22 import android.provider.Settings
23 import com.android.systemui.R
24 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.dagger.qualifiers.Main
28 import com.android.systemui.qs.QSHost
29 import com.android.systemui.qs.pipeline.shared.TileSpec
30 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
31 import com.android.systemui.retail.data.repository.RetailModeRepository
32 import com.android.systemui.util.settings.SecureSettings
33 import javax.inject.Inject
34 import kotlinx.coroutines.CoroutineDispatcher
35 import kotlinx.coroutines.ExperimentalCoroutinesApi
36 import kotlinx.coroutines.channels.awaitClose
37 import kotlinx.coroutines.flow.Flow
38 import kotlinx.coroutines.flow.distinctUntilChanged
39 import kotlinx.coroutines.flow.flatMapLatest
40 import kotlinx.coroutines.flow.flowOf
41 import kotlinx.coroutines.flow.flowOn
42 import kotlinx.coroutines.flow.map
43 import kotlinx.coroutines.flow.onEach
44 import kotlinx.coroutines.flow.onStart
45 import kotlinx.coroutines.sync.Mutex
46 import kotlinx.coroutines.sync.withLock
47 import kotlinx.coroutines.withContext
48 
49 /** Repository that tracks the current tiles. */
50 interface TileSpecRepository {
51 
52     /**
53      * Returns a flow of the current list of [TileSpec] for a given [userId].
54      *
55      * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty.
56      */
57     fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>
58 
59     /**
60      * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile
61      * at the end of the list.
62      *
63      * Passing [TileSpec.Invalid] is a noop.
64      *
65      * Trying to add a tile beyond the end of the list will add it at the end.
66      */
67     suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END)
68 
69     /**
70      * Removes a [tile] for a given [userId].
71      *
72      * Passing [TileSpec.Invalid] or a non present tile is a noop.
73      */
74     suspend fun removeTiles(@UserIdInt userId: Int, tiles: Collection<TileSpec>)
75 
76     /**
77      * Sets the list of current [tiles] for a given [userId].
78      *
79      * [TileSpec.Invalid] will be ignored, and an effectively empty list will not be stored.
80      */
81     suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>)
82 
83     companion object {
84         /** Position to indicate the end of the list */
85         const val POSITION_AT_END = -1
86     }
87 }
88 
89 /**
90  * Implementation of [TileSpecRepository] that persist the values of tiles in
91  * [Settings.Secure.QS_TILES].
92  *
93  * All operations against [Settings] will be performed in a background thread.
94  *
95  * If the device is in retail mode, the tiles are fixed to the value of
96  * [R.string.quick_settings_tiles_retail_mode].
97  */
98 @SysUISingleton
99 class TileSpecSettingsRepository
100 @Inject
101 constructor(
102     private val secureSettings: SecureSettings,
103     @Main private val resources: Resources,
104     private val logger: QSPipelineLogger,
105     private val retailModeRepository: RetailModeRepository,
106     @Background private val backgroundDispatcher: CoroutineDispatcher,
107 ) : TileSpecRepository {
108 
109     private val mutex = Mutex()
110 
111     private val retailModeTiles by lazy {
112         resources
113             .getString(R.string.quick_settings_tiles_retail_mode)
114             .split(DELIMITER)
115             .map(TileSpec::create)
116             .filter { it !is TileSpec.Invalid }
117     }
118 
119     @OptIn(ExperimentalCoroutinesApi::class)
120     override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
121         return retailModeRepository.retailMode.flatMapLatest { inRetailMode ->
122             if (inRetailMode) {
123                 logger.logUsingRetailTiles()
124                 flowOf(retailModeTiles)
125             } else {
126                 settingsTiles(userId)
127             }
128         }
129     }
130 
131     private fun settingsTiles(userId: Int): Flow<List<TileSpec>> {
132         return conflatedCallbackFlow {
133                 val observer =
134                     object : ContentObserver(null) {
135                         override fun onChange(selfChange: Boolean) {
136                             trySend(Unit)
137                         }
138                     }
139 
140                 secureSettings.registerContentObserverForUser(SETTING, observer, userId)
141 
142                 awaitClose { secureSettings.unregisterContentObserver(observer) }
143             }
144             .onStart { emit(Unit) }
145             .map { secureSettings.getStringForUser(SETTING, userId) ?: "" }
146             .distinctUntilChanged()
147             .onEach { logger.logTilesChangedInSettings(it, userId) }
148             .map { parseTileSpecs(it, userId) }
149             .flowOn(backgroundDispatcher)
150     }
151 
152     override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) =
153         mutex.withLock {
154             if (tile == TileSpec.Invalid) {
155                 return
156             }
157             val tilesList = loadTiles(userId).toMutableList()
158             if (tile !in tilesList) {
159                 if (position < 0 || position >= tilesList.size) {
160                     tilesList.add(tile)
161                 } else {
162                     tilesList.add(position, tile)
163                 }
164                 storeTiles(userId, tilesList)
165             }
166         }
167 
168     override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) =
169         mutex.withLock {
170             if (tiles.all { it == TileSpec.Invalid }) {
171                 return
172             }
173             val tilesList = loadTiles(userId).toMutableList()
174             if (tilesList.removeAll(tiles)) {
175                 storeTiles(userId, tilesList.toList())
176             }
177         }
178 
179     override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) =
180         mutex.withLock {
181             val filtered = tiles.filter { it != TileSpec.Invalid }
182             if (filtered.isNotEmpty()) {
183                 storeTiles(userId, filtered)
184             }
185         }
186 
187     private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> {
188         return withContext(backgroundDispatcher) {
189             (secureSettings.getStringForUser(SETTING, forUser) ?: "")
190                 .split(DELIMITER)
191                 .map(TileSpec::create)
192                 .filter { it !is TileSpec.Invalid }
193         }
194     }
195 
196     private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
197         if (retailModeRepository.inRetailMode) {
198             // No storing tiles when in retail mode
199             return
200         }
201         val toStore =
202             tiles
203                 .filter { it !is TileSpec.Invalid }
204                 .joinToString(DELIMITER, transform = TileSpec::spec)
205         withContext(backgroundDispatcher) {
206             secureSettings.putStringForUser(
207                 SETTING,
208                 toStore,
209                 null,
210                 false,
211                 forUser,
212                 true,
213             )
214         }
215     }
216 
217     private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> {
218         val fromSettings =
219             tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter {
220                 it != TileSpec.Invalid
221             }
222         return if (fromSettings.isNotEmpty()) {
223             fromSettings.also { logger.logParsedTiles(it, false, user) }
224         } else {
225             QSHost.getDefaultSpecs(resources)
226                 .map(TileSpec::create)
227                 .filter { it != TileSpec.Invalid }
228                 .also { logger.logParsedTiles(it, true, user) }
229         }
230     }
231 
232     companion object {
233         private const val SETTING = Settings.Secure.QS_TILES
234         private const val DELIMITER = ","
235     }
236 }
237