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.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 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 val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } 130 val result = 131 slotKeys 132 .associate { key -> 133 val slotId = key.substring(KEY_PREFIX_SLOT.length) 134 val value = sharedPrefs.getString(key, null) 135 val affordanceIds = 136 if (!value.isNullOrEmpty()) { 137 value.split(AFFORDANCE_DELIMITER) 138 } else { 139 emptyList() 140 } 141 slotId to affordanceIds 142 } 143 .toMutableMap() 144 145 // If the result map is missing keys, it means that the system has never set anything for 146 // those slots. This is where we need examine our defaults and see if there should be a 147 // default value for the affordances in the slot IDs that are missing from the result. 148 // 149 // Once the user makes any selection for a slot, even when they select "None", this class 150 // will persist a key for that slot ID. In the case of "None", it will have a value of the 151 // empty string. This is why this system works. 152 defaults.forEach { (slotId, affordanceIds) -> 153 if (!result.containsKey(slotId)) { 154 result[slotId] = affordanceIds 155 } 156 } 157 158 return result 159 } 160 161 override fun setSelections( 162 slotId: String, 163 affordanceIds: List<String>, 164 ) { 165 val key = "$KEY_PREFIX_SLOT$slotId" 166 val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) 167 sharedPrefs.edit().putString(key, value).apply() 168 } 169 170 private fun instantiateSharedPrefs(): SharedPreferences { 171 return userFileManager.getSharedPreferences( 172 FILE_NAME, 173 Context.MODE_PRIVATE, 174 userTracker.userId, 175 ) 176 } 177 178 companion object { 179 private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" 180 const val FILE_NAME = "quick_affordance_selections" 181 private const val KEY_PREFIX_SLOT = "slot_" 182 private const val SLOT_AFFORDANCES_DELIMITER = ":" 183 private const val AFFORDANCE_DELIMITER = "," 184 } 185 } 186