1 /*
2  * Copyright (C) 2021 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
18 
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.database.ContentObserver
24 import android.net.Uri
25 import android.os.Handler
26 import android.os.UserHandle
27 import android.provider.Settings
28 import android.text.TextUtils
29 import android.util.ArraySet
30 import android.util.Log
31 import androidx.annotation.GuardedBy
32 import androidx.annotation.VisibleForTesting
33 import com.android.systemui.Dumpable
34 import com.android.systemui.broadcast.BroadcastDispatcher
35 import com.android.systemui.dagger.SysUISingleton
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.dump.DumpManager
39 import com.android.systemui.util.UserAwareController
40 import com.android.systemui.util.settings.SecureSettings
41 import java.io.PrintWriter
42 import java.util.concurrent.Executor
43 import javax.inject.Inject
44 
45 private const val TAG = "AutoAddTracker"
46 private const val DELIMITER = ","
47 
48 /**
49  * Class to track tiles that have been auto-added
50  *
51  * The list is backed by [Settings.Secure.QS_AUTO_ADDED_TILES].
52  *
53  * It also handles restore gracefully.
54  */
55 class AutoAddTracker @VisibleForTesting constructor(
56     private val secureSettings: SecureSettings,
57     private val broadcastDispatcher: BroadcastDispatcher,
58     private val qsHost: QSHost,
59     private val dumpManager: DumpManager,
60     private val mainHandler: Handler?,
61     private val backgroundExecutor: Executor,
62     private var userId: Int
63 ) : UserAwareController, Dumpable {
64 
65     companion object {
66         private val FILTER = IntentFilter(Intent.ACTION_SETTING_RESTORED)
67     }
68 
69     @GuardedBy("autoAdded")
70     private val autoAdded = ArraySet<String>()
71     private var restoredTiles: Map<String, AutoTile>? = null
72 
73     override val currentUserId: Int
74         get() = userId
75 
76     private val contentObserver = object : ContentObserver(mainHandler) {
77         override fun onChange(
78             selfChange: Boolean,
79             uris: Collection<Uri>,
80             flags: Int,
81             _userId: Int
82         ) {
83             if (_userId != userId) {
84                 // Ignore changes outside of our user. We'll load the correct value on user change
85                 return
86             }
87             loadTiles()
88         }
89     }
90 
91     private val restoreReceiver = object : BroadcastReceiver() {
92         override fun onReceive(context: Context, intent: Intent) {
93             if (intent.action != Intent.ACTION_SETTING_RESTORED) return
94             processRestoreIntent(intent)
95         }
96     }
97 
98     private fun processRestoreIntent(intent: Intent) {
99         when (intent.getStringExtra(Intent.EXTRA_SETTING_NAME)) {
100             Settings.Secure.QS_TILES -> {
101                 restoredTiles = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
102                         ?.split(DELIMITER)
103                         ?.mapIndexed(::AutoTile)
104                         ?.associateBy(AutoTile::tileType)
105                         ?: run {
106                             Log.w(TAG, "Null restored tiles for user $userId")
107                             emptyMap()
108                         }
109             }
110             Settings.Secure.QS_AUTO_ADDED_TILES -> {
111                 restoredTiles?.let { restoredTiles ->
112                     val restoredAutoAdded = intent
113                             .getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
114                             ?.split(DELIMITER)
115                             ?: emptyList()
116                     val autoAddedBeforeRestore = intent
117                             .getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE)
118                             ?.split(DELIMITER)
119                             ?: emptyList()
120 
121                     val tilesToRemove = restoredAutoAdded.filter { it !in restoredTiles }
122                     if (tilesToRemove.isNotEmpty()) {
123                         Log.d(TAG, "Removing tiles: $tilesToRemove")
124                         qsHost.removeTiles(tilesToRemove)
125                     }
126                     val tiles = synchronized(autoAdded) {
127                         autoAdded.clear()
128                         autoAdded.addAll(restoredAutoAdded + autoAddedBeforeRestore)
129                         getTilesFromListLocked()
130                     }
131                     saveTiles(tiles)
132                 } ?: run {
133                     Log.w(TAG, "${Settings.Secure.QS_AUTO_ADDED_TILES} restored before " +
134                             "${Settings.Secure.QS_TILES} for user $userId")
135                 }
136             }
137             else -> {} // Do nothing for other Settings
138         }
139     }
140 
141     /**
142      * Init method must be called after construction to start listening
143      */
144     fun initialize() {
145         dumpManager.registerDumpable(TAG, this)
146         loadTiles()
147         secureSettings.registerContentObserverForUser(
148                 secureSettings.getUriFor(Settings.Secure.QS_AUTO_ADDED_TILES),
149                 contentObserver,
150                 UserHandle.USER_ALL
151         )
152         registerBroadcastReceiver()
153     }
154 
155     /**
156      * Unregister listeners, receivers and observers
157      */
158     fun destroy() {
159         dumpManager.unregisterDumpable(TAG)
160         secureSettings.unregisterContentObserver(contentObserver)
161         unregisterBroadcastReceiver()
162     }
163 
164     private fun registerBroadcastReceiver() {
165         broadcastDispatcher.registerReceiver(
166                 restoreReceiver,
167                 FILTER,
168                 backgroundExecutor,
169                 UserHandle.of(userId)
170         )
171     }
172 
173     private fun unregisterBroadcastReceiver() {
174         broadcastDispatcher.unregisterReceiver(restoreReceiver)
175     }
176 
177     override fun changeUser(newUser: UserHandle) {
178         if (newUser.identifier == userId) return
179         unregisterBroadcastReceiver()
180         userId = newUser.identifier
181         restoredTiles = null
182         loadTiles()
183         registerBroadcastReceiver()
184     }
185 
186     fun getRestoredTilePosition(tile: String): Int =
187         restoredTiles?.get(tile)?.index ?: QSHost.POSITION_AT_END
188 
189     /**
190      * Returns `true` if the tile has been auto-added before
191      */
192     fun isAdded(tile: String): Boolean {
193         return synchronized(autoAdded) {
194             tile in autoAdded
195         }
196     }
197 
198     /**
199      * Sets a tile as auto-added.
200      *
201      * From here on, [isAdded] will return true for that tile.
202      */
203     fun setTileAdded(tile: String) {
204         val tiles = synchronized(autoAdded) {
205             if (autoAdded.add(tile)) {
206                 getTilesFromListLocked()
207             } else {
208                 null
209             }
210         }
211         tiles?.let { saveTiles(it) }
212     }
213 
214     /**
215      * Removes a tile from the list of auto-added.
216      *
217      * This allows for this tile to be auto-added again in the future.
218      */
219     fun setTileRemoved(tile: String) {
220         val tiles = synchronized(autoAdded) {
221             if (autoAdded.remove(tile)) {
222                 getTilesFromListLocked()
223             } else {
224                 null
225             }
226         }
227         tiles?.let { saveTiles(it) }
228     }
229 
230     private fun getTilesFromListLocked(): String {
231         return TextUtils.join(DELIMITER, autoAdded)
232     }
233 
234     private fun saveTiles(tiles: String) {
235         secureSettings.putStringForUser(
236                 Settings.Secure.QS_AUTO_ADDED_TILES,
237                 tiles,
238                 /* tag */ null,
239                 /* makeDefault */ false,
240                 userId,
241                 /* overrideableByRestore */ true
242         )
243     }
244 
245     private fun loadTiles() {
246         synchronized(autoAdded) {
247             autoAdded.clear()
248             autoAdded.addAll(getAdded())
249         }
250     }
251 
252     private fun getAdded(): Collection<String> {
253         val current = secureSettings.getStringForUser(Settings.Secure.QS_AUTO_ADDED_TILES, userId)
254         return current?.split(DELIMITER) ?: emptySet()
255     }
256 
257     override fun dump(pw: PrintWriter, args: Array<out String>) {
258         pw.println("Current user: $userId")
259         pw.println("Restored tiles: $restoredTiles")
260         pw.println("Added tiles: $autoAdded")
261     }
262 
263     @SysUISingleton
264     class Builder @Inject constructor(
265         private val secureSettings: SecureSettings,
266         private val broadcastDispatcher: BroadcastDispatcher,
267         private val qsHost: QSHost,
268         private val dumpManager: DumpManager,
269         @Main private val handler: Handler,
270         @Background private val executor: Executor
271     ) {
272         private var userId: Int = 0
273 
274         fun setUserId(_userId: Int): Builder {
275             userId = _userId
276             return this
277         }
278 
279         fun build(): AutoAddTracker {
280             return AutoAddTracker(
281                     secureSettings,
282                     broadcastDispatcher,
283                     qsHost,
284                     dumpManager,
285                     handler,
286                     executor,
287                     userId
288             )
289         }
290     }
291 
292     private data class AutoTile(val index: Int, val tileType: String)
293 }