1 /*
<lambda>null2  * Copyright (C) 2022 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 
18 package com.android.systemui.keyguard.data.quickaffordance
19 
20 import android.os.UserHandle
21 import android.provider.Settings
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Application
24 import com.android.systemui.dagger.qualifiers.Background
25 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer.Companion.BINDINGS
26 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
27 import com.android.systemui.util.settings.SecureSettings
28 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
29 import javax.inject.Inject
30 import kotlinx.coroutines.CoroutineDispatcher
31 import kotlinx.coroutines.CoroutineScope
32 import kotlinx.coroutines.Job
33 import kotlinx.coroutines.flow.distinctUntilChanged
34 import kotlinx.coroutines.flow.flowOn
35 import kotlinx.coroutines.flow.launchIn
36 import kotlinx.coroutines.flow.map
37 import kotlinx.coroutines.flow.onEach
38 import kotlinx.coroutines.launch
39 import kotlinx.coroutines.withContext
40 
41 /**
42  * Keeps quick affordance selections and legacy user settings in sync.
43  *
44  * "Legacy user settings" are user settings like: Settings > Display > Lock screen > "Show device
45  * controls" Settings > Display > Lock screen > "Show wallet"
46  *
47  * Quick affordance selections are the ones available through the new custom lock screen experience
48  * from Settings > Wallpaper & Style.
49  *
50  * This class keeps these in sync, mostly for backwards compatibility purposes and in order to not
51  * "forget" an existing legacy user setting when the device gets updated with a version of System UI
52  * that has the new customizable lock screen feature.
53  *
54  * The way it works is that, when [startSyncing] is called, the syncer starts coroutines to listen
55  * for changes in both legacy user settings and their respective affordance selections. Whenever one
56  * of each pair is changed, the other member of that pair is also updated to match. For example, if
57  * the user turns on "Show device controls", we automatically select the home controls affordance
58  * for the preferred slot. Conversely, when the home controls affordance is unselected by the user,
59  * we set the "Show device controls" setting to "off".
60  *
61  * The class can be configured by updating its list of triplets in the code under [BINDINGS].
62  */
63 @SysUISingleton
64 class KeyguardQuickAffordanceLegacySettingSyncer
65 @Inject
66 constructor(
67     @Application private val scope: CoroutineScope,
68     @Background private val backgroundDispatcher: CoroutineDispatcher,
69     private val secureSettings: SecureSettings,
70     private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager,
71 ) {
72     companion object {
73         private val BINDINGS =
74             listOf(
75                 Binding(
76                     settingsKey = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
77                     slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
78                     affordanceId = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
79                 ),
80                 Binding(
81                     settingsKey = Settings.Secure.LOCKSCREEN_SHOW_WALLET,
82                     slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
83                     affordanceId = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET,
84                 ),
85                 Binding(
86                     settingsKey = Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER,
87                     slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
88                     affordanceId = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER,
89                 ),
90             )
91     }
92 
93     fun startSyncing(
94         bindings: List<Binding> = BINDINGS,
95     ): Job {
96         return scope.launch { bindings.forEach { binding -> startSyncing(this, binding) } }
97     }
98 
99     private fun startSyncing(
100         scope: CoroutineScope,
101         binding: Binding,
102     ) {
103         secureSettings
104             .observerFlow(
105                 names = arrayOf(binding.settingsKey),
106                 userId = UserHandle.USER_ALL,
107             )
108             .map {
109                 isSet(
110                     settingsKey = binding.settingsKey,
111                 )
112             }
113             .distinctUntilChanged()
114             .onEach { isSet ->
115                 if (isSelected(binding.affordanceId) != isSet) {
116                     if (isSet) {
117                         select(
118                             slotId = binding.slotId,
119                             affordanceId = binding.affordanceId,
120                         )
121                     } else {
122                         unselect(
123                             affordanceId = binding.affordanceId,
124                         )
125                     }
126                 }
127             }
128             .flowOn(backgroundDispatcher)
129             .launchIn(scope)
130 
131         selectionsManager.selections
132             .map { it.values.flatten().toSet() }
133             .map { it.contains(binding.affordanceId) }
134             .distinctUntilChanged()
135             .onEach { isSelected ->
136                 if (isSet(binding.settingsKey) != isSelected) {
137                     set(binding.settingsKey, isSelected)
138                 }
139             }
140             .flowOn(backgroundDispatcher)
141             .launchIn(scope)
142     }
143 
144     private fun isSelected(
145         affordanceId: String,
146     ): Boolean {
147         return selectionsManager
148             .getSelections() // Map<String, List<String>>
149             .values // Collection<List<String>>
150             .flatten() // List<String>
151             .toSet() // Set<String>
152             .contains(affordanceId)
153     }
154 
155     private fun select(
156         slotId: String,
157         affordanceId: String,
158     ) {
159         val affordanceIdsAtSlotId = selectionsManager.getSelections()[slotId] ?: emptyList()
160         selectionsManager.setSelections(
161             slotId = slotId,
162             affordanceIds = affordanceIdsAtSlotId + listOf(affordanceId),
163         )
164     }
165 
166     private fun unselect(
167         affordanceId: String,
168     ) {
169         val currentSelections = selectionsManager.getSelections()
170         val slotIdsContainingAffordanceId =
171             currentSelections
172                 .filter { (_, affordanceIds) -> affordanceIds.contains(affordanceId) }
173                 .map { (slotId, _) -> slotId }
174 
175         slotIdsContainingAffordanceId.forEach { slotId ->
176             val currentAffordanceIds = currentSelections[slotId] ?: emptyList()
177             val affordanceIdsAfterUnselecting =
178                 currentAffordanceIds.toMutableList().apply { remove(affordanceId) }
179 
180             selectionsManager.setSelections(
181                 slotId = slotId,
182                 affordanceIds = affordanceIdsAfterUnselecting,
183             )
184         }
185     }
186 
187     private fun isSet(
188         settingsKey: String,
189     ): Boolean {
190         return secureSettings.getIntForUser(
191             settingsKey,
192             0,
193             UserHandle.USER_CURRENT,
194         ) != 0
195     }
196 
197     private suspend fun set(
198         settingsKey: String,
199         isSet: Boolean,
200     ) {
201         withContext(backgroundDispatcher) {
202             secureSettings.putInt(
203                 settingsKey,
204                 if (isSet) 1 else 0,
205             )
206         }
207     }
208 
209     data class Binding(
210         val settingsKey: String,
211         val slotId: String,
212         val affordanceId: String,
213     )
214 }
215