1 /* 2 * 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.content.Context 21 import android.content.IntentFilter 22 import android.content.SharedPreferences 23 import com.android.systemui.R 24 import com.android.systemui.backup.BackupHelper 25 import com.android.systemui.broadcast.BroadcastDispatcher 26 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 27 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.settings.UserFileManager 31 import com.android.systemui.settings.UserTracker 32 import javax.inject.Inject 33 import kotlinx.coroutines.ExperimentalCoroutinesApi 34 import kotlinx.coroutines.channels.awaitClose 35 import kotlinx.coroutines.flow.Flow 36 import kotlinx.coroutines.flow.combine 37 import kotlinx.coroutines.flow.flatMapLatest 38 import kotlinx.coroutines.flow.onStart 39 40 /** 41 * Manages and provides access to the current "selections" of keyguard quick affordances, answering 42 * the question "which affordances should the keyguard show?" for the user associated with the 43 * System UI process. 44 */ 45 @OptIn(ExperimentalCoroutinesApi::class) 46 @SysUISingleton 47 class KeyguardQuickAffordanceLocalUserSelectionManager 48 @Inject 49 constructor( 50 @Application private val context: Context, 51 private val userFileManager: UserFileManager, 52 private val userTracker: UserTracker, 53 broadcastDispatcher: BroadcastDispatcher, 54 ) : KeyguardQuickAffordanceSelectionManager { 55 56 private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() 57 58 private val userId: Flow<Int> = conflatedCallbackFlow { 59 val callback = 60 object : UserTracker.Callback { 61 override fun onUserChanged(newUser: Int, userContext: Context) { 62 trySendWithFailureLogging(newUser, TAG) 63 } 64 } 65 66 userTracker.addCallback(callback) { it.run() } 67 trySendWithFailureLogging(userTracker.userId, TAG) 68 69 awaitClose { userTracker.removeCallback(callback) } 70 } 71 72 private val defaults: Map<String, List<String>> by lazy { 73 context.resources 74 .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) 75 .associate { item -> 76 val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) 77 check(splitUp.size == 2) 78 val slotId = splitUp[0] 79 val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) 80 slotId to affordanceIds 81 } 82 } 83 84 /** 85 * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an 86 * initial value. 87 */ 88 private val backupRestorationEvents: Flow<Unit> = 89 broadcastDispatcher.broadcastFlow( 90 filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), 91 flags = Context.RECEIVER_NOT_EXPORTED, 92 permission = BackupHelper.PERMISSION_SELF, 93 ) 94 95 override val selections: Flow<Map<String, List<String>>> = 96 combine( 97 userId, 98 backupRestorationEvents.onStart { 99 // We emit an initial event to make sure that the combine emits at least once, 100 // even if we never get a Backup & Restore restoration event (which is the most 101 // common case anyway as restoration really only happens on initial device 102 // setup). 103 emit(Unit) 104 } 105 ) { _, _ -> 106 } 107 .flatMapLatest { 108 conflatedCallbackFlow { 109 // We want to instantiate a new SharedPreferences instance each time either the 110 // user ID changes or we have a backup & restore restoration event. The reason 111 // is that our sharedPrefs instance needs to be replaced with a new one as it 112 // depends on the user ID and when the B&R job completes, the backing file is 113 // replaced but the existing instance still has a stale in-memory cache. 114 sharedPrefs = instantiateSharedPrefs() 115 116 val listener = 117 SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> 118 trySend(getSelections()) 119 } 120 121 sharedPrefs.registerOnSharedPreferenceChangeListener(listener) 122 send(getSelections()) 123 124 awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } 125 } 126 } 127 128 override fun getSelections(): Map<String, List<String>> { 129 // If the custom shortcuts feature is not enabled, ignore prior selections and use defaults 130 if (!context.resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled)) { 131 return defaults 132 } 133 134 val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } 135 val result = 136 slotKeys 137 .associate { key -> 138 val slotId = key.substring(KEY_PREFIX_SLOT.length) 139 val value = sharedPrefs.getString(key, null) 140 val affordanceIds = 141 if (!value.isNullOrEmpty()) { 142 value.split(AFFORDANCE_DELIMITER) 143 } else { 144 emptyList() 145 } 146 slotId to affordanceIds 147 } 148 .toMutableMap() 149 150 // If the result map is missing keys, it means that the system has never set anything for 151 // those slots. This is where we need examine our defaults and see if there should be a 152 // default value for the affordances in the slot IDs that are missing from the result. 153 // 154 // Once the user makes any selection for a slot, even when they select "None", this class 155 // will persist a key for that slot ID. In the case of "None", it will have a value of the 156 // empty string. This is why this system works. 157 defaults.forEach { (slotId, affordanceIds) -> 158 if (!result.containsKey(slotId)) { 159 result[slotId] = affordanceIds 160 } 161 } 162 163 return result 164 } 165 166 override fun setSelections( 167 slotId: String, 168 affordanceIds: List<String>, 169 ) { 170 val key = "$KEY_PREFIX_SLOT$slotId" 171 val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) 172 sharedPrefs.edit().putString(key, value).apply() 173 } 174 175 private fun instantiateSharedPrefs(): SharedPreferences { 176 return userFileManager.getSharedPreferences( 177 FILE_NAME, 178 Context.MODE_PRIVATE, 179 userTracker.userId, 180 ) 181 } 182 183 companion object { 184 private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" 185 const val FILE_NAME = "quick_affordance_selections" 186 private const val KEY_PREFIX_SLOT = "slot_" 187 private const val SLOT_AFFORDANCES_DELIMITER = ":" 188 private const val AFFORDANCE_DELIMITER = "," 189 } 190 } 191