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.database.ContentObserver
20 import android.provider.Settings
21 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Background
24 import com.android.systemui.qs.pipeline.shared.TileSpec
25 import com.android.systemui.util.settings.SecureSettings
26 import javax.inject.Inject
27 import kotlinx.coroutines.CoroutineDispatcher
28 import kotlinx.coroutines.channels.awaitClose
29 import kotlinx.coroutines.flow.Flow
30 import kotlinx.coroutines.flow.distinctUntilChanged
31 import kotlinx.coroutines.flow.flowOn
32 import kotlinx.coroutines.flow.map
33 import kotlinx.coroutines.flow.onStart
34 import kotlinx.coroutines.withContext
35 
36 /** Repository to track what QS tiles have been auto-added */
37 interface AutoAddRepository {
38 
39     /** Flow of tiles that have been auto-added */
40     fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>>
41 
42     /** Mark a tile as having been auto-added */
43     suspend fun markTileAdded(userId: Int, spec: TileSpec)
44 
45     /**
46      * Unmark a tile as having been auto-added. This is used for tiles that can be auto-added
47      * multiple times.
48      */
49     suspend fun unmarkTileAdded(userId: Int, spec: TileSpec)
50 }
51 
52 /**
53  * Implementation that tracks the auto-added tiles stored in [Settings.Secure.QS_AUTO_ADDED_TILES].
54  */
55 @SysUISingleton
56 class AutoAddSettingRepository
57 @Inject
58 constructor(
59     private val secureSettings: SecureSettings,
60     @Background private val bgDispatcher: CoroutineDispatcher,
61 ) : AutoAddRepository {
62     override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
63         return conflatedCallbackFlow {
64                 val observer =
65                     object : ContentObserver(null) {
66                         override fun onChange(selfChange: Boolean) {
67                             trySend(Unit)
68                         }
69                     }
70 
71                 secureSettings.registerContentObserverForUser(SETTING, observer, userId)
72 
73                 awaitClose { secureSettings.unregisterContentObserver(observer) }
74             }
75             .onStart { emit(Unit) }
76             .map { secureSettings.getStringForUser(SETTING, userId) ?: "" }
77             .distinctUntilChanged()
78             .map {
79                 it.split(DELIMITER).map(TileSpec::create).filter { it !is TileSpec.Invalid }.toSet()
80             }
81             .flowOn(bgDispatcher)
82     }
83 
84     override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
85         if (spec is TileSpec.Invalid) {
86             return
87         }
88         val added = load(userId).toMutableSet()
89         if (added.add(spec)) {
90             store(userId, added)
91         }
92     }
93 
94     override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
95         if (spec is TileSpec.Invalid) {
96             return
97         }
98         val added = load(userId).toMutableSet()
99         if (added.remove(spec)) {
100             store(userId, added)
101         }
102     }
103 
104     private suspend fun store(userId: Int, tiles: Set<TileSpec>) {
105         val toStore =
106             tiles
107                 .filter { it !is TileSpec.Invalid }
108                 .joinToString(DELIMITER, transform = TileSpec::spec)
109         withContext(bgDispatcher) {
110             secureSettings.putStringForUser(
111                 SETTING,
112                 toStore,
113                 null,
114                 false,
115                 userId,
116                 true,
117             )
118         }
119     }
120 
121     private suspend fun load(userId: Int): Set<TileSpec> {
122         return withContext(bgDispatcher) {
123             (secureSettings.getStringForUser(SETTING, userId) ?: "")
124                 .split(",")
125                 .map(TileSpec::create)
126                 .filter { it !is TileSpec.Invalid }
127                 .toSet()
128         }
129     }
130 
131     companion object {
132         private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
133         private const val DELIMITER = ","
134     }
135 }
136