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