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