From 74df4ef2f069530a8cc6e42a134d0344aad18fe0 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 28 Nov 2022 15:35:14 -0800 Subject: [PATCH] Multi-user customizable quick affordances. This CL adds support for multi-users to the customizable lock screen quick affordance system. The process that creates windows and renders System UI is always the primary user's system UI process. The process that owns the content provider accessed by Wallpaper Picker is bound to the currently-selected user on the system, which may be different than the primary user. What this means is that, when switching to a secondary user, what the Wallpaper Picker sees is fed to it from a secondary system UI process which is not the same system UI process that is rendering the affordances on the lock screen. Therefore, it didn't work. This CL adds a "remote user" selection manager which the repository can switch to when it's running on the primary user process but needs to query the state of the selected affordances for a secondary user. The "remote user" selection manager queries the content provider associated with the system UI process linked to the secondary user. This way, we can display the correct affordances on the screen, even when switching to a secondary user. Fix: 260251307 Test: included new and expanded unit tests. Manually verified that the selection of quick affordances for the primary and secondary user are correctly reflected on the lock screen when switching users and is retained even after switching away and back. Change-Id: I281577ed6efb987c23b19c2078e77c91e45ce9f2 --- .../KeyguardDataQuickAffordanceModule.kt | 42 ++-- ...guardQuickAffordanceLegacySettingSyncer.kt | 2 +- ...uickAffordanceLocalUserSelectionManager.kt | 184 +++++++++++++++ ...ardQuickAffordanceProviderClientFactory.kt | 43 ++++ ...ickAffordanceRemoteUserSelectionManager.kt | 129 +++++++++++ ...KeyguardQuickAffordanceSelectionManager.kt | 159 +------------ .../KeyguardQuickAffordanceRepository.kt | 65 +++++- .../KeyguardQuickAffordanceInteractor.kt | 4 - .../com/android/systemui/user/UserModule.java | 19 ++ .../KeyguardQuickAffordanceProviderTest.kt | 23 +- ...dQuickAffordanceLegacySettingSyncerTest.kt | 4 +- ...ffordanceLocalUserSelectionManagerTest.kt} | 6 +- ...ffordanceRemoteUserSelectionManagerTest.kt | 219 ++++++++++++++++++ .../KeyguardQuickAffordanceRepositoryTest.kt | 104 ++++++++- ...ckAffordanceInteractorParameterizedTest.kt | 23 +- .../KeyguardQuickAffordanceInteractorTest.kt | 23 +- .../KeyguardBottomAreaViewModelTest.kt | 23 +- ...ardQuickAffordanceProviderClientFactory.kt | 34 +++ .../systemui/settings/FakeUserTracker.kt | 3 +- 19 files changed, 891 insertions(+), 218 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt rename packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/{KeyguardQuickAffordanceSelectionManagerTest.kt => KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt} (98%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index 3013227c21c03..072cfb140e623 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -17,27 +17,35 @@ package com.android.systemui.keyguard.data.quickaffordance +import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @Module -object KeyguardDataQuickAffordanceModule { - @Provides - @ElementsIntoSet - fun quickAffordanceConfigs( - flashlight: FlashlightQuickAffordanceConfig, - home: HomeControlsKeyguardQuickAffordanceConfig, - quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, - qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, - camera: CameraQuickAffordanceConfig, - ): Set { - return setOf( - camera, - flashlight, - home, - quickAccessWallet, - qrCodeScanner, - ) +interface KeyguardDataQuickAffordanceModule { + @Binds + fun providerClientFactory( + impl: KeyguardQuickAffordanceProviderClientFactoryImpl, + ): KeyguardQuickAffordanceProviderClientFactory + + companion object { + @Provides + @ElementsIntoSet + fun quickAffordanceConfigs( + flashlight: FlashlightQuickAffordanceConfig, + home: HomeControlsKeyguardQuickAffordanceConfig, + quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, + qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, + camera: CameraQuickAffordanceConfig, + ): Set { + return setOf( + camera, + flashlight, + home, + quickAccessWallet, + qrCodeScanner, + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt index 766096f1fa2bc..72747f68bbbd4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt @@ -67,7 +67,7 @@ constructor( @Application private val scope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val secureSettings: SecureSettings, - private val selectionsManager: KeyguardQuickAffordanceSelectionManager, + private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager, ) { companion object { private val BINDINGS = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt new file mode 100644 index 0000000000000..006678546de8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt @@ -0,0 +1,184 @@ +/* + * 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 = 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> 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 = + broadcastDispatcher.broadcastFlow( + filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), + flags = Context.RECEIVER_NOT_EXPORTED, + permission = BackupHelper.PERMISSION_SELF, + ) + + override val selections: Flow>> = + 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> { + 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, + ) { + 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 = "," + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt new file mode 100644 index 0000000000000..727a81391dc2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt @@ -0,0 +1,43 @@ +/* + * 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 com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher + +interface KeyguardQuickAffordanceProviderClientFactory { + fun create(): KeyguardQuickAffordanceProviderClient +} + +class KeyguardQuickAffordanceProviderClientFactoryImpl +@Inject +constructor( + private val userTracker: UserTracker, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : KeyguardQuickAffordanceProviderClientFactory { + override fun create(): KeyguardQuickAffordanceProviderClient { + return KeyguardQuickAffordanceProviderClientImpl( + context = userTracker.userContext, + backgroundDispatcher = backgroundDispatcher, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt new file mode 100644 index 0000000000000..8ffef25d3aaea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt @@ -0,0 +1,129 @@ +/* + * 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.os.UserHandle +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.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * Manages and provides access to the current "selections" of keyguard quick affordances, answering + * the question "which affordances should the keyguard show?" for users associated with other System + * UI processes. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class KeyguardQuickAffordanceRemoteUserSelectionManager +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val userTracker: UserTracker, + private val clientFactory: KeyguardQuickAffordanceProviderClientFactory, + private val userHandle: UserHandle, +) : KeyguardQuickAffordanceSelectionManager { + + private val userId: Flow = 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 clientOrNull: StateFlow = + userId + .distinctUntilChanged() + .map { selectedUserId -> + if (userHandle.isSystem && userHandle.identifier != selectedUserId) { + clientFactory.create() + } else { + null + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + private val _selections: StateFlow>> = + clientOrNull + .flatMapLatest { client -> + client?.observeSelections()?.map { selections -> + buildMap> { + selections.forEach { selection -> + val slotId = selection.slotId + val affordanceIds = (get(slotId) ?: emptyList()).toMutableList() + affordanceIds.add(selection.affordanceId) + put(slotId, affordanceIds) + } + } + } + ?: emptyFlow() + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) + + override val selections: Flow>> = _selections + + override fun getSelections(): Map> { + return _selections.value + } + + override fun setSelections(slotId: String, affordanceIds: List) { + clientOrNull.value?.let { client -> + scope.launch { + client.deleteAllSelections(slotId = slotId) + affordanceIds.forEach { affordanceId -> + client.insertSelection(slotId = slotId, affordanceId = affordanceId) + } + } + } + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceMultiUserSelectionManager" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index 4f37e5f389ee0..21fffede5f97d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -17,153 +17,22 @@ 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?". + * Defines interface for classes that manage and provide access to the current "selections" of + * keyguard quick affordances, answering the question "which affordances should the keyguard show?". */ -@OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton -class KeyguardQuickAffordanceSelectionManager -@Inject -constructor( - @Application context: Context, - private val userFileManager: UserFileManager, - private val userTracker: UserTracker, - broadcastDispatcher: BroadcastDispatcher, -) { - - private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() - - private val userId: Flow = 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> 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 = - broadcastDispatcher.broadcastFlow( - filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), - flags = Context.RECEIVER_NOT_EXPORTED, - permission = BackupHelper.PERMISSION_SELF, - ) +interface KeyguardQuickAffordanceSelectionManager { /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ - val selections: Flow>> = - 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) } - } - } + val selections: Flow>> /** * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in * descending priority order. */ - fun getSelections(): Map> { - 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 - } + fun getSelections(): Map> /** * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance @@ -172,25 +41,9 @@ constructor( fun setSelections( slotId: String, affordanceIds: List, - ) { - 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 = "KeyguardQuickAffordanceSelectionManager" 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 = "," } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index d95a1a726bf50..e3f5e90b23006 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -18,45 +18,93 @@ package com.android.systemui.keyguard.data.repository import android.content.Context +import android.os.UserHandle import com.android.systemui.Dumpable import com.android.systemui.R +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.settings.UserTracker import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Abstracts access to application state related to keyguard quick affordances. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceRepository @Inject constructor( @Application private val appContext: Context, @Application private val scope: CoroutineScope, - private val selectionManager: KeyguardQuickAffordanceSelectionManager, + private val localUserSelectionManager: KeyguardQuickAffordanceLocalUserSelectionManager, + private val remoteUserSelectionManager: KeyguardQuickAffordanceRemoteUserSelectionManager, + private val userTracker: UserTracker, legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer, private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, dumpManager: DumpManager, + userHandle: UserHandle, ) { + private val userId: Flow = + ConflatedCallbackFlow.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 selectionManager: StateFlow = + userId + .distinctUntilChanged() + .map { selectedUserId -> + if (userHandle.identifier == selectedUserId) { + localUserSelectionManager + } else { + remoteUserSelectionManager + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = localUserSelectionManager, + ) + /** * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the * given ID. The configs are sorted in descending priority order. */ val selections: StateFlow>> = - selectionManager.selections - .map { selectionsBySlotId -> - selectionsBySlotId.mapValues { (_, selections) -> - configs.filter { selections.contains(it.key) } + selectionManager + .flatMapLatest { selectionManager -> + selectionManager.selections.map { selectionsBySlotId -> + selectionsBySlotId.mapValues { (_, selections) -> + configs.filter { selections.contains(it.key) } + } } } .stateIn( @@ -99,7 +147,7 @@ constructor( * slot with the given ID. The configs are sorted in descending priority order. */ fun getSelections(slotId: String): List { - val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) + val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList()) return configs.filter { selections.contains(it.key) } } @@ -108,7 +156,7 @@ constructor( * are sorted in descending priority order. */ fun getSelections(): Map> { - return selectionManager.getSelections() + return selectionManager.value.getSelections() } /** @@ -119,7 +167,7 @@ constructor( slotId: String, affordanceIds: List, ) { - selectionManager.setSelections( + selectionManager.value.setSelections( slotId = slotId, affordanceIds = affordanceIds, ) @@ -188,6 +236,7 @@ constructor( } companion object { + private const val TAG = "KeyguardQuickAffordanceRepository" private const val SLOT_CONFIG_DELIMITER = ":" } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index ee7154ff72195..748c6e8b75b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -190,8 +190,6 @@ constructor( /** Returns affordance IDs indexed by slot ID, for all known slots. */ suspend fun getSelections(): Map> { - check(isUsingRepository) - val slots = repository.get().getSlotPickerRepresentations() val selections = repository.get().getSelections() val affordanceById = @@ -312,8 +310,6 @@ constructor( suspend fun getAffordancePickerRepresentations(): List { - check(isUsingRepository) - return repository.get().getAffordancePickerRepresentations() } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 0c72b78a3c46c..2b29885db6822 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -17,6 +17,7 @@ package com.android.systemui.user; import android.app.Activity; +import android.os.UserHandle; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; @@ -51,4 +52,22 @@ public abstract class UserModule { @IntoMap @ClassKey(UserSwitcherActivity.class) public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); + + /** + * Provides the {@link UserHandle} for the user associated with this System UI process. + * + *

Note that this is static and unchanging for the life-time of the process we are running + * in. It can be different from the user that is the currently-selected user, which may + * be associated with a different System UI process. + * + *

For example, the System UI process which creates all the windows and renders UI is always + * the one associated with the primary user on the device. However, if the user is switched to + * another, non-primary user (for example user "X"), then a secondary System UI process will be + * spawned. While the original primary user process continues to be the only one rendering UI, + * the new system UI process may be used for things like file or content access. + */ + @Provides + public static UserHandle provideUserHandle() { + return new UserHandle(UserHandle.myUserId()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index 32c5b3f99d419..cef452b8ec220 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard import android.content.ContentValues import android.content.pm.PackageManager import android.content.pm.ProviderInfo +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SystemUIAppComponentFactoryBase @@ -27,8 +28,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -74,8 +77,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceProvider() val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock().apply { @@ -91,11 +94,20 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { userTracker = userTracker, broadcastDispatcher = fakeBroadcastDispatcher, ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, configs = setOf( FakeKeyguardQuickAffordanceConfig( @@ -114,9 +126,10 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest.interactor = KeyguardQuickAffordanceInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt index 552b8cb965254..3b0169d77063b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -57,7 +57,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { private lateinit var testScope: TestScope private lateinit var testDispatcher: TestDispatcher - private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager + private lateinit var selectionManager: KeyguardQuickAffordanceLocalUserSelectionManager private lateinit var settings: FakeSettings @Before @@ -75,7 +75,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { testDispatcher = UnconfinedTestDispatcher() testScope = TestScope(testDispatcher) selectionManager = - KeyguardQuickAffordanceSelectionManager( + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt similarity index 98% rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt index 6a2376b5bc4e7..67091a9f40c76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt @@ -52,11 +52,11 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) -class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { +class KeyguardQuickAffordanceLocalUserSelectionManagerTest : SysuiTestCase() { @Mock private lateinit var userFileManager: UserFileManager - private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + private lateinit var underTest: KeyguardQuickAffordanceLocalUserSelectionManager private lateinit var userTracker: FakeUserTracker private lateinit var sharedPrefs: MutableMap @@ -74,7 +74,7 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { Dispatchers.setMain(dispatcher) underTest = - KeyguardQuickAffordanceSelectionManager( + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = userFileManager, userTracker = userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt new file mode 100644 index 0000000000000..d7e9cf144f887 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt @@ -0,0 +1,219 @@ +/* + * 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.pm.UserInfo +import android.os.UserHandle +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceRemoteUserSelectionManagerTest : SysuiTestCase() { + + @Mock private lateinit var userHandle: UserHandle + + private lateinit var underTest: KeyguardQuickAffordanceRemoteUserSelectionManager + + private lateinit var clientFactory: FakeKeyguardQuickAffordanceProviderClientFactory + private lateinit var testScope: TestScope + private lateinit var testDispatcher: TestDispatcher + private lateinit var userTracker: FakeUserTracker + private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient + private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(userHandle.identifier).thenReturn(UserHandle.USER_SYSTEM) + whenever(userHandle.isSystem).thenReturn(true) + client1 = FakeKeyguardQuickAffordanceProviderClient() + client2 = FakeKeyguardQuickAffordanceProviderClient() + + userTracker = FakeUserTracker() + userTracker.set( + userInfos = + listOf( + UserInfo( + UserHandle.USER_SYSTEM, + "Primary", + /* flags= */ 0, + ), + UserInfo( + OTHER_USER_ID_1, + "Secondary 1", + /* flags= */ 0, + ), + UserInfo( + OTHER_USER_ID_2, + "Secondary 2", + /* flags= */ 0, + ), + ), + selectedUserIndex = 0, + ) + + clientFactory = + FakeKeyguardQuickAffordanceProviderClientFactory( + userTracker, + ) { selectedUserId -> + when (selectedUserId) { + OTHER_USER_ID_1 -> client1 + OTHER_USER_ID_2 -> client2 + else -> error("No client set-up for user $selectedUserId!") + } + } + + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + underTest = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = testScope.backgroundScope, + userTracker = userTracker, + clientFactory = clientFactory, + userHandle = userHandle, + ) + } + + @Test + fun `selections - primary user process`() = + testScope.runTest { + val values = mutableListOf>>() + val job = launch { underTest.selections.toList(values) } + + runCurrent() + assertThat(values.last()).isEmpty() + + client1.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ) + client2.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ) + + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + assertThat(values.last()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ), + ) + ) + + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 2, + ) + runCurrent() + assertThat(values.last()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ), + ) + ) + + job.cancel() + } + + @Test + fun `selections - secondary user process - always empty`() = + testScope.runTest { + whenever(userHandle.isSystem).thenReturn(false) + val values = mutableListOf>>() + val job = launch { underTest.selections.toList(values) } + + runCurrent() + assertThat(values.last()).isEmpty() + + client1.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ) + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + assertThat(values.last()).isEmpty() + + job.cancel() + } + + @Test + fun setSelections() = + testScope.runTest { + userTracker.set( + userInfos = userTracker.userProfiles, + selectedUserIndex = 1, + ) + runCurrent() + + underTest.setSelections( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceIds = listOf(FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1), + ) + runCurrent() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1, + ), + ) + ) + } + + companion object { + private const val OTHER_USER_ID_1 = UserHandle.MIN_SECONDARY_USER_ID + 1 + private const val OTHER_USER_ID_2 = UserHandle.MIN_SECONDARY_USER_ID + 2 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index 652fae9687447..c40488adf0298 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -17,17 +17,23 @@ package com.android.systemui.keyguard.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -39,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,14 +62,24 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { private lateinit var config1: FakeKeyguardQuickAffordanceConfig private lateinit var config2: FakeKeyguardQuickAffordanceConfig + private lateinit var userTracker: FakeUserTracker + private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient + private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient @Before fun setUp() { - config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") - config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + config1 = + FakeKeyguardQuickAffordanceConfig( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1 + ) + config2 = + FakeKeyguardQuickAffordanceConfig( + FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2 + ) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + userTracker = FakeUserTracker() + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock().apply { @@ -75,24 +92,45 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { ) .thenReturn(FakeSharedPreferences()) }, - userTracker = FakeUserTracker(), + userTracker = userTracker, broadcastDispatcher = fakeBroadcastDispatcher, ) + client1 = FakeKeyguardQuickAffordanceProviderClient() + client2 = FakeKeyguardQuickAffordanceProviderClient() + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = + FakeKeyguardQuickAffordanceProviderClientFactory( + userTracker, + ) { selectedUserId -> + when (selectedUserId) { + SECONDARY_USER_1 -> client1 + SECONDARY_USER_2 -> client2 + else -> error("No set-up client for user $selectedUserId!") + } + }, + userHandle = UserHandle.SYSTEM, + ) underTest = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(config1, config2), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) } @@ -187,7 +225,53 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { ) } - private suspend fun assertSelections( + @Test + fun `selections for secondary user`() = + runBlocking(IMMEDIATE) { + userTracker.set( + userInfos = + listOf( + UserInfo( + UserHandle.USER_SYSTEM, + "Primary", + /* flags= */ 0, + ), + UserInfo( + SECONDARY_USER_1, + "Secondary 1", + /* flags= */ 0, + ), + UserInfo( + SECONDARY_USER_2, + "Secondary 2", + /* flags= */ 0, + ), + ), + selectedUserIndex = 2, + ) + client2.insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2, + ) + val observed = mutableListOf>>() + val job = underTest.selections.onEach { observed.add(it) }.launchIn(this) + yield() + + assertSelections( + observed = observed.last(), + expected = + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf( + config2, + ), + ) + ) + + job.cancel() + } + + private fun assertSelections( observed: Map>?, expected: Map>, ) { @@ -201,5 +285,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private const val SECONDARY_USER_1 = UserHandle.MIN_SECONDARY_USER_ID + 1 + private const val SECONDARY_USER_2 = UserHandle.MIN_SECONDARY_USER_ID + 2 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index ba7c40b6b3812..1c1f0399bb069 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -29,9 +30,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry @@ -237,8 +240,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { val qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock().apply { @@ -254,20 +257,30 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { userTracker = userTracker, broadcastDispatcher = fakeBroadcastDispatcher, ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest = KeyguardQuickAffordanceInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 8d0c4ef4b3da8..11fe905b1d1f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -26,9 +27,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel @@ -98,8 +101,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock().apply { @@ -115,20 +118,30 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { userTracker = userTracker, broadcastDispatcher = fakeBroadcastDispatcher, ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) featureFlags = FakeFeatureFlags().apply { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 32849cdce02e4..83a5d0e90c842 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Intent +import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase @@ -27,9 +28,11 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer -import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor @@ -121,8 +124,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) val scope = CoroutineScope(IMMEDIATE) - val selectionManager = - KeyguardQuickAffordanceSelectionManager( + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( context = context, userFileManager = mock().apply { @@ -138,17 +141,26 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { userTracker = userTracker, broadcastDispatcher = fakeBroadcastDispatcher, ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, scope = scope, - selectionManager = selectionManager, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( scope = scope, backgroundDispatcher = IMMEDIATE, secureSettings = FakeSettings(), - selectionsManager = selectionManager, + selectionsManager = localUserSelectionManager, ), configs = setOf( @@ -157,6 +169,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { qrCodeScannerAffordanceConfig, ), dumpManager = mock(), + userHandle = UserHandle.SYSTEM, ) underTest = KeyguardBottomAreaViewModel( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt new file mode 100644 index 0000000000000..d85dd2e7bc8a5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt @@ -0,0 +1,34 @@ +/* + * 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 com.android.systemui.settings.UserTracker +import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient + +class FakeKeyguardQuickAffordanceProviderClientFactory( + private val userTracker: UserTracker, + private val callback: (Int) -> KeyguardQuickAffordanceProviderClient = { + FakeKeyguardQuickAffordanceProviderClient() + }, +) : KeyguardQuickAffordanceProviderClientFactory { + + override fun create(): KeyguardQuickAffordanceProviderClient { + return callback(userTracker.userId) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index a7eadba60ddc2..0dd1fc758a274 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -66,7 +66,8 @@ class FakeUserTracker( _userId = _userInfo.id _userHandle = UserHandle.of(_userId) - callbacks.forEach { it.onUserChanged(_userId, userContext) } + val copy = callbacks.toList() + copy.forEach { it.onUserChanged(_userId, userContext) } } fun onProfileChanged() {