| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package com.android.systemui.keyguard.data.quickaffordance |
| |
| import android.content.Context |
| import android.content.IntentFilter |
| import android.content.SharedPreferences |
| import com.android.systemui.R |
| import com.android.systemui.backup.BackupHelper |
| import com.android.systemui.broadcast.BroadcastDispatcher |
| import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging |
| import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow |
| import com.android.systemui.dagger.SysUISingleton |
| import com.android.systemui.dagger.qualifiers.Application |
| import com.android.systemui.settings.UserFileManager |
| import com.android.systemui.settings.UserTracker |
| import javax.inject.Inject |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.channels.awaitClose |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.combine |
| import kotlinx.coroutines.flow.flatMapLatest |
| import kotlinx.coroutines.flow.onStart |
| |
| /** |
| * Manages and provides access to the current "selections" of keyguard quick affordances, answering |
| * the question "which affordances should the keyguard show?" for the user associated with the |
| * System UI process. |
| */ |
| @OptIn(ExperimentalCoroutinesApi::class) |
| @SysUISingleton |
| class KeyguardQuickAffordanceLocalUserSelectionManager |
| @Inject |
| constructor( |
| @Application context: Context, |
| private val userFileManager: UserFileManager, |
| private val userTracker: UserTracker, |
| broadcastDispatcher: BroadcastDispatcher, |
| ) : KeyguardQuickAffordanceSelectionManager { |
| |
| private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() |
| |
| private val userId: Flow<Int> = conflatedCallbackFlow { |
| val callback = |
| object : UserTracker.Callback { |
| override fun onUserChanged(newUser: Int, userContext: Context) { |
| trySendWithFailureLogging(newUser, TAG) |
| } |
| } |
| |
| userTracker.addCallback(callback) { it.run() } |
| trySendWithFailureLogging(userTracker.userId, TAG) |
| |
| awaitClose { userTracker.removeCallback(callback) } |
| } |
| |
| private val defaults: Map<String, List<String>> by lazy { |
| context.resources |
| .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) |
| .associate { item -> |
| val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) |
| check(splitUp.size == 2) |
| val slotId = splitUp[0] |
| val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) |
| slotId to affordanceIds |
| } |
| } |
| |
| /** |
| * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an |
| * initial value. |
| */ |
| private val backupRestorationEvents: Flow<Unit> = |
| broadcastDispatcher.broadcastFlow( |
| filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), |
| flags = Context.RECEIVER_NOT_EXPORTED, |
| permission = BackupHelper.PERMISSION_SELF, |
| ) |
| |
| override val selections: Flow<Map<String, List<String>>> = |
| combine( |
| userId, |
| backupRestorationEvents.onStart { |
| // We emit an initial event to make sure that the combine emits at least once, |
| // even if we never get a Backup & Restore restoration event (which is the most |
| // common case anyway as restoration really only happens on initial device |
| // setup). |
| emit(Unit) |
| } |
| ) { _, _ -> |
| } |
| .flatMapLatest { |
| conflatedCallbackFlow { |
| // We want to instantiate a new SharedPreferences instance each time either the |
| // user ID changes or we have a backup & restore restoration event. The reason |
| // is that our sharedPrefs instance needs to be replaced with a new one as it |
| // depends on the user ID and when the B&R job completes, the backing file is |
| // replaced but the existing instance still has a stale in-memory cache. |
| sharedPrefs = instantiateSharedPrefs() |
| |
| val listener = |
| SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> |
| trySend(getSelections()) |
| } |
| |
| sharedPrefs.registerOnSharedPreferenceChangeListener(listener) |
| send(getSelections()) |
| |
| awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } |
| } |
| } |
| |
| override fun getSelections(): Map<String, List<String>> { |
| val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } |
| val result = |
| slotKeys |
| .associate { key -> |
| val slotId = key.substring(KEY_PREFIX_SLOT.length) |
| val value = sharedPrefs.getString(key, null) |
| val affordanceIds = |
| if (!value.isNullOrEmpty()) { |
| value.split(AFFORDANCE_DELIMITER) |
| } else { |
| emptyList() |
| } |
| slotId to affordanceIds |
| } |
| .toMutableMap() |
| |
| // If the result map is missing keys, it means that the system has never set anything for |
| // those slots. This is where we need examine our defaults and see if there should be a |
| // default value for the affordances in the slot IDs that are missing from the result. |
| // |
| // Once the user makes any selection for a slot, even when they select "None", this class |
| // will persist a key for that slot ID. In the case of "None", it will have a value of the |
| // empty string. This is why this system works. |
| defaults.forEach { (slotId, affordanceIds) -> |
| if (!result.containsKey(slotId)) { |
| result[slotId] = affordanceIds |
| } |
| } |
| |
| return result |
| } |
| |
| override fun setSelections( |
| slotId: String, |
| affordanceIds: List<String>, |
| ) { |
| val key = "$KEY_PREFIX_SLOT$slotId" |
| val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) |
| sharedPrefs.edit().putString(key, value).apply() |
| } |
| |
| private fun instantiateSharedPrefs(): SharedPreferences { |
| return userFileManager.getSharedPreferences( |
| FILE_NAME, |
| Context.MODE_PRIVATE, |
| userTracker.userId, |
| ) |
| } |
| |
| companion object { |
| private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" |
| const val FILE_NAME = "quick_affordance_selections" |
| private const val KEY_PREFIX_SLOT = "slot_" |
| private const val SLOT_AFFORDANCES_DELIMITER = ":" |
| private const val AFFORDANCE_DELIMITER = "," |
| } |
| } |