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() {