Merge "Multi-user customizable quick affordances." into tm-qpr-dev

This commit is contained in:
Ale Nijamkin
2022-11-30 17:51:53 +00:00
committed by Android (Google) Code Review
19 changed files with 891 additions and 218 deletions

View File

@@ -17,12 +17,19 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.multibindings.ElementsIntoSet import dagger.multibindings.ElementsIntoSet
@Module @Module
object KeyguardDataQuickAffordanceModule { interface KeyguardDataQuickAffordanceModule {
@Binds
fun providerClientFactory(
impl: KeyguardQuickAffordanceProviderClientFactoryImpl,
): KeyguardQuickAffordanceProviderClientFactory
companion object {
@Provides @Provides
@ElementsIntoSet @ElementsIntoSet
fun quickAffordanceConfigs( fun quickAffordanceConfigs(
@@ -40,4 +47,5 @@ object KeyguardDataQuickAffordanceModule {
qrCodeScanner, qrCodeScanner,
) )
} }
}
} }

View File

@@ -67,7 +67,7 @@ constructor(
@Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher,
private val secureSettings: SecureSettings, private val secureSettings: SecureSettings,
private val selectionsManager: KeyguardQuickAffordanceSelectionManager, private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager,
) { ) {
companion object { companion object {
private val BINDINGS = private val BINDINGS =

View File

@@ -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<Int> = conflatedCallbackFlow {
val callback =
object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
trySendWithFailureLogging(newUser, TAG)
}
}
userTracker.addCallback(callback) { it.run() }
trySendWithFailureLogging(userTracker.userId, TAG)
awaitClose { userTracker.removeCallback(callback) }
}
private val defaults: Map<String, List<String>> by lazy {
context.resources
.getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
.associate { item ->
val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER)
check(splitUp.size == 2)
val slotId = splitUp[0]
val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER)
slotId to affordanceIds
}
}
/**
* Emits an event each time a Backup & Restore restoration job is completed. Does not emit an
* initial value.
*/
private val backupRestorationEvents: Flow<Unit> =
broadcastDispatcher.broadcastFlow(
filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
flags = Context.RECEIVER_NOT_EXPORTED,
permission = BackupHelper.PERMISSION_SELF,
)
override val selections: Flow<Map<String, List<String>>> =
combine(
userId,
backupRestorationEvents.onStart {
// We emit an initial event to make sure that the combine emits at least once,
// even if we never get a Backup & Restore restoration event (which is the most
// common case anyway as restoration really only happens on initial device
// setup).
emit(Unit)
}
) { _, _ -> }
.flatMapLatest {
conflatedCallbackFlow {
// We want to instantiate a new SharedPreferences instance each time either the
// user ID changes or we have a backup & restore restoration event. The reason
// is that our sharedPrefs instance needs to be replaced with a new one as it
// depends on the user ID and when the B&R job completes, the backing file is
// replaced but the existing instance still has a stale in-memory cache.
sharedPrefs = instantiateSharedPrefs()
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
trySend(getSelections())
}
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
send(getSelections())
awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
}
}
override fun getSelections(): Map<String, List<String>> {
val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
val result =
slotKeys
.associate { key ->
val slotId = key.substring(KEY_PREFIX_SLOT.length)
val value = sharedPrefs.getString(key, null)
val affordanceIds =
if (!value.isNullOrEmpty()) {
value.split(AFFORDANCE_DELIMITER)
} else {
emptyList()
}
slotId to affordanceIds
}
.toMutableMap()
// If the result map is missing keys, it means that the system has never set anything for
// those slots. This is where we need examine our defaults and see if there should be a
// default value for the affordances in the slot IDs that are missing from the result.
//
// Once the user makes any selection for a slot, even when they select "None", this class
// will persist a key for that slot ID. In the case of "None", it will have a value of the
// empty string. This is why this system works.
defaults.forEach { (slotId, affordanceIds) ->
if (!result.containsKey(slotId)) {
result[slotId] = affordanceIds
}
}
return result
}
override fun setSelections(
slotId: String,
affordanceIds: List<String>,
) {
val key = "$KEY_PREFIX_SLOT$slotId"
val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
sharedPrefs.edit().putString(key, value).apply()
}
private fun instantiateSharedPrefs(): SharedPreferences {
return userFileManager.getSharedPreferences(
FILE_NAME,
Context.MODE_PRIVATE,
userTracker.userId,
)
}
companion object {
private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager"
const val FILE_NAME = "quick_affordance_selections"
private const val KEY_PREFIX_SLOT = "slot_"
private const val SLOT_AFFORDANCES_DELIMITER = ":"
private const val AFFORDANCE_DELIMITER = ","
}
}

View File

@@ -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,
)
}
}

View File

@@ -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<Int> = conflatedCallbackFlow {
val callback =
object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
trySendWithFailureLogging(newUser, TAG)
}
}
userTracker.addCallback(callback) { it.run() }
trySendWithFailureLogging(userTracker.userId, TAG)
awaitClose { userTracker.removeCallback(callback) }
}
private val clientOrNull: StateFlow<KeyguardQuickAffordanceProviderClient?> =
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<Map<String, List<String>>> =
clientOrNull
.flatMapLatest { client ->
client?.observeSelections()?.map { selections ->
buildMap<String, List<String>> {
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<Map<String, List<String>>> = _selections
override fun getSelections(): Map<String, List<String>> {
return _selections.value
}
override fun setSelections(slotId: String, affordanceIds: List<String>) {
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"
}
}

View File

@@ -17,153 +17,22 @@
package com.android.systemui.keyguard.data.quickaffordance 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.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 * Defines interface for classes that manage and provide access to the current "selections" of
* the question "which affordances should the keyguard show?". * keyguard quick affordances, answering the question "which affordances should the keyguard show?".
*/ */
@OptIn(ExperimentalCoroutinesApi::class) interface KeyguardQuickAffordanceSelectionManager {
@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<Int> = conflatedCallbackFlow {
val callback =
object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
trySendWithFailureLogging(newUser, TAG)
}
}
userTracker.addCallback(callback) { it.run() }
trySendWithFailureLogging(userTracker.userId, TAG)
awaitClose { userTracker.removeCallback(callback) }
}
private val defaults: Map<String, List<String>> by lazy {
context.resources
.getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
.associate { item ->
val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER)
check(splitUp.size == 2)
val slotId = splitUp[0]
val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER)
slotId to affordanceIds
}
}
/**
* Emits an event each time a Backup & Restore restoration job is completed. Does not emit an
* initial value.
*/
private val backupRestorationEvents: Flow<Unit> =
broadcastDispatcher.broadcastFlow(
filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
flags = Context.RECEIVER_NOT_EXPORTED,
permission = BackupHelper.PERMISSION_SELF,
)
/** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
val selections: Flow<Map<String, List<String>>> = val selections: Flow<Map<String, List<String>>>
combine(
userId,
backupRestorationEvents.onStart {
// We emit an initial event to make sure that the combine emits at least once,
// even
// if we never get a Backup & Restore restoration event (which is the most
// common
// case anyway as restoration really only happens on initial device setup).
emit(Unit)
}
) { _, _ ->
}
.flatMapLatest {
conflatedCallbackFlow {
// We want to instantiate a new SharedPreferences instance each time either the
// user
// ID changes or we have a backup & restore restoration event. The reason is
// that
// our sharedPrefs instance needs to be replaced with a new one as it depends on
// the
// user ID and when the B&R job completes, the backing file is replaced but the
// existing instance still has a stale in-memory cache.
sharedPrefs = instantiateSharedPrefs()
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
trySend(getSelections())
}
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
send(getSelections())
awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
}
}
/** /**
* Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
* descending priority order. * descending priority order.
*/ */
fun getSelections(): Map<String, List<String>> { fun getSelections(): Map<String, List<String>>
val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
val result =
slotKeys
.associate { key ->
val slotId = key.substring(KEY_PREFIX_SLOT.length)
val value = sharedPrefs.getString(key, null)
val affordanceIds =
if (!value.isNullOrEmpty()) {
value.split(AFFORDANCE_DELIMITER)
} else {
emptyList()
}
slotId to affordanceIds
}
.toMutableMap()
// If the result map is missing keys, it means that the system has never set anything for
// those slots. This is where we need examine our defaults and see if there should be a
// default value for the affordances in the slot IDs that are missing from the result.
//
// Once the user makes any selection for a slot, even when they select "None", this class
// will persist a key for that slot ID. In the case of "None", it will have a value of the
// empty string. This is why this system works.
defaults.forEach { (slotId, affordanceIds) ->
if (!result.containsKey(slotId)) {
result[slotId] = affordanceIds
}
}
return result
}
/** /**
* Updates the IDs of affordances to show at the slot with the given ID. The order of affordance * 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( fun setSelections(
slotId: String, slotId: String,
affordanceIds: List<String>, affordanceIds: List<String>,
) {
val key = "$KEY_PREFIX_SLOT$slotId"
val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
sharedPrefs.edit().putString(key, value).apply()
}
private fun instantiateSharedPrefs(): SharedPreferences {
return userFileManager.getSharedPreferences(
FILE_NAME,
Context.MODE_PRIVATE,
userTracker.userId,
) )
}
companion object { companion object {
private const val TAG = "KeyguardQuickAffordanceSelectionManager"
const val FILE_NAME = "quick_affordance_selections" 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 = ","
} }
} }

View File

@@ -18,47 +18,95 @@
package com.android.systemui.keyguard.data.repository package com.android.systemui.keyguard.data.repository
import android.content.Context import android.content.Context
import android.os.UserHandle
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.R 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer 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.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
import com.android.systemui.settings.UserTracker
import java.io.PrintWriter import java.io.PrintWriter
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope 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.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
/** Abstracts access to application state related to keyguard quick affordances. */ /** Abstracts access to application state related to keyguard quick affordances. */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton @SysUISingleton
class KeyguardQuickAffordanceRepository class KeyguardQuickAffordanceRepository
@Inject @Inject
constructor( constructor(
@Application private val appContext: Context, @Application private val appContext: Context,
@Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope,
private val selectionManager: KeyguardQuickAffordanceSelectionManager, private val localUserSelectionManager: KeyguardQuickAffordanceLocalUserSelectionManager,
private val remoteUserSelectionManager: KeyguardQuickAffordanceRemoteUserSelectionManager,
private val userTracker: UserTracker,
legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer, legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
dumpManager: DumpManager, dumpManager: DumpManager,
userHandle: UserHandle,
) { ) {
private val userId: Flow<Int> =
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<KeyguardQuickAffordanceSelectionManager> =
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 * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the
* given ID. The configs are sorted in descending priority order. * given ID. The configs are sorted in descending priority order.
*/ */
val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> = val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> =
selectionManager.selections selectionManager
.map { selectionsBySlotId -> .flatMapLatest { selectionManager ->
selectionManager.selections.map { selectionsBySlotId ->
selectionsBySlotId.mapValues { (_, selections) -> selectionsBySlotId.mapValues { (_, selections) ->
configs.filter { selections.contains(it.key) } configs.filter { selections.contains(it.key) }
} }
} }
}
.stateIn( .stateIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
@@ -99,7 +147,7 @@ constructor(
* slot with the given ID. The configs are sorted in descending priority order. * slot with the given ID. The configs are sorted in descending priority order.
*/ */
fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
return configs.filter { selections.contains(it.key) } return configs.filter { selections.contains(it.key) }
} }
@@ -108,7 +156,7 @@ constructor(
* are sorted in descending priority order. * are sorted in descending priority order.
*/ */
fun getSelections(): Map<String, List<String>> { fun getSelections(): Map<String, List<String>> {
return selectionManager.getSelections() return selectionManager.value.getSelections()
} }
/** /**
@@ -119,7 +167,7 @@ constructor(
slotId: String, slotId: String,
affordanceIds: List<String>, affordanceIds: List<String>,
) { ) {
selectionManager.setSelections( selectionManager.value.setSelections(
slotId = slotId, slotId = slotId,
affordanceIds = affordanceIds, affordanceIds = affordanceIds,
) )
@@ -188,6 +236,7 @@ constructor(
} }
companion object { companion object {
private const val TAG = "KeyguardQuickAffordanceRepository"
private const val SLOT_CONFIG_DELIMITER = ":" private const val SLOT_CONFIG_DELIMITER = ":"
} }
} }

View File

@@ -190,8 +190,6 @@ constructor(
/** Returns affordance IDs indexed by slot ID, for all known slots. */ /** Returns affordance IDs indexed by slot ID, for all known slots. */
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> { suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
check(isUsingRepository)
val slots = repository.get().getSlotPickerRepresentations() val slots = repository.get().getSlotPickerRepresentations()
val selections = repository.get().getSelections() val selections = repository.get().getSelections()
val affordanceById = val affordanceById =
@@ -312,8 +310,6 @@ constructor(
suspend fun getAffordancePickerRepresentations(): suspend fun getAffordancePickerRepresentations():
List<KeyguardQuickAffordancePickerRepresentation> { List<KeyguardQuickAffordancePickerRepresentation> {
check(isUsingRepository)
return repository.get().getAffordancePickerRepresentations() return repository.get().getAffordancePickerRepresentations()
} }

View File

@@ -17,6 +17,7 @@
package com.android.systemui.user; package com.android.systemui.user;
import android.app.Activity; import android.app.Activity;
import android.os.UserHandle;
import com.android.settingslib.users.EditUserInfoController; import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.data.repository.UserRepositoryModule;
@@ -51,4 +52,22 @@ public abstract class UserModule {
@IntoMap @IntoMap
@ClassKey(UserSwitcherActivity.class) @ClassKey(UserSwitcherActivity.class)
public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity);
/**
* Provides the {@link UserHandle} for the user associated with this System UI process.
*
* <p>Note that this is static and unchanging for the life-time of the process we are running
* in. It can be <i>different</i> from the user that is the currently-selected user, which may
* be associated with a different System UI process.
*
* <p>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());
}
} }

View File

@@ -20,6 +20,7 @@ package com.android.systemui.keyguard
import android.content.ContentValues import android.content.ContentValues
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ProviderInfo import android.content.pm.ProviderInfo
import android.os.UserHandle
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SystemUIAppComponentFactoryBase 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.FakeFeatureFlags
import com.android.systemui.flags.Flags import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig 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.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.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -74,8 +77,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
underTest = KeyguardQuickAffordanceProvider() underTest = KeyguardQuickAffordanceProvider()
val scope = CoroutineScope(IMMEDIATE) val scope = CoroutineScope(IMMEDIATE)
val selectionManager = val localUserSelectionManager =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock<UserFileManager>().apply { mock<UserFileManager>().apply {
@@ -91,11 +94,20 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
userTracker = userTracker, userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher, broadcastDispatcher = fakeBroadcastDispatcher,
) )
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
scope = scope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = scope, scope = scope,
selectionManager = selectionManager, localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
configs = configs =
setOf( setOf(
FakeKeyguardQuickAffordanceConfig( FakeKeyguardQuickAffordanceConfig(
@@ -114,9 +126,10 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
scope = scope, scope = scope,
backgroundDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(), secureSettings = FakeSettings(),
selectionsManager = selectionManager, selectionsManager = localUserSelectionManager,
), ),
dumpManager = mock(), dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
) )
underTest.interactor = underTest.interactor =
KeyguardQuickAffordanceInteractor( KeyguardQuickAffordanceInteractor(

View File

@@ -57,7 +57,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
private lateinit var testScope: TestScope private lateinit var testScope: TestScope
private lateinit var testDispatcher: TestDispatcher private lateinit var testDispatcher: TestDispatcher
private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager private lateinit var selectionManager: KeyguardQuickAffordanceLocalUserSelectionManager
private lateinit var settings: FakeSettings private lateinit var settings: FakeSettings
@Before @Before
@@ -75,7 +75,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
testDispatcher = UnconfinedTestDispatcher() testDispatcher = UnconfinedTestDispatcher()
testScope = TestScope(testDispatcher) testScope = TestScope(testDispatcher)
selectionManager = selectionManager =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock { mock {

View File

@@ -52,11 +52,11 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { class KeyguardQuickAffordanceLocalUserSelectionManagerTest : SysuiTestCase() {
@Mock private lateinit var userFileManager: UserFileManager @Mock private lateinit var userFileManager: UserFileManager
private lateinit var underTest: KeyguardQuickAffordanceSelectionManager private lateinit var underTest: KeyguardQuickAffordanceLocalUserSelectionManager
private lateinit var userTracker: FakeUserTracker private lateinit var userTracker: FakeUserTracker
private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences> private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences>
@@ -74,7 +74,7 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
underTest = underTest =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager, userFileManager = userFileManager,
userTracker = userTracker, userTracker = userTracker,

View File

@@ -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<Map<String, List<String>>>()
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<Map<String, List<String>>>()
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
}
}

View File

@@ -17,17 +17,23 @@
package com.android.systemui.keyguard.data.repository package com.android.systemui.keyguard.data.repository
import android.content.pm.UserInfo
import android.os.UserHandle
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer 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.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserFileManager 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.FakeSharedPreferences
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
@@ -39,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -55,14 +62,24 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
private lateinit var config1: FakeKeyguardQuickAffordanceConfig private lateinit var config1: FakeKeyguardQuickAffordanceConfig
private lateinit var config2: FakeKeyguardQuickAffordanceConfig private lateinit var config2: FakeKeyguardQuickAffordanceConfig
private lateinit var userTracker: FakeUserTracker
private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient
private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient
@Before @Before
fun setUp() { fun setUp() {
config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") config1 =
config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") FakeKeyguardQuickAffordanceConfig(
FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1
)
config2 =
FakeKeyguardQuickAffordanceConfig(
FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2
)
val scope = CoroutineScope(IMMEDIATE) val scope = CoroutineScope(IMMEDIATE)
val selectionManager = userTracker = FakeUserTracker()
KeyguardQuickAffordanceSelectionManager( val localUserSelectionManager =
KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock<UserFileManager>().apply { mock<UserFileManager>().apply {
@@ -75,24 +92,45 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
) )
.thenReturn(FakeSharedPreferences()) .thenReturn(FakeSharedPreferences())
}, },
userTracker = FakeUserTracker(), userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher, 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 = underTest =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = scope, scope = scope,
selectionManager = selectionManager, localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
legacySettingSyncer = legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer( KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope, scope = scope,
backgroundDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(), secureSettings = FakeSettings(),
selectionsManager = selectionManager, selectionsManager = localUserSelectionManager,
), ),
configs = setOf(config1, config2), configs = setOf(config1, config2),
dumpManager = mock(), 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<Map<String, List<KeyguardQuickAffordanceConfig>>>()
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<String, List<KeyguardQuickAffordanceConfig>>?, observed: Map<String, List<KeyguardQuickAffordanceConfig>>?,
expected: Map<String, List<KeyguardQuickAffordanceConfig>>, expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
) { ) {
@@ -201,5 +285,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
companion object { companion object {
private val IMMEDIATE = Dispatchers.Main.immediate 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
} }
} }

View File

@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.domain.interactor package com.android.systemui.keyguard.domain.interactor
import android.content.Intent import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase 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.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer 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.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
@@ -237,8 +240,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
val qrCodeScanner = val qrCodeScanner =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
val scope = CoroutineScope(IMMEDIATE) val scope = CoroutineScope(IMMEDIATE)
val selectionManager = val localUserSelectionManager =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock<UserFileManager>().apply { mock<UserFileManager>().apply {
@@ -254,20 +257,30 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
userTracker = userTracker, userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher, broadcastDispatcher = fakeBroadcastDispatcher,
) )
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
scope = scope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = scope, scope = scope,
selectionManager = selectionManager, localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
legacySettingSyncer = legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer( KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope, scope = scope,
backgroundDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(), secureSettings = FakeSettings(),
selectionsManager = selectionManager, selectionsManager = localUserSelectionManager,
), ),
configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
dumpManager = mock(), dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
) )
underTest = underTest =
KeyguardQuickAffordanceInteractor( KeyguardQuickAffordanceInteractor(

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor package com.android.systemui.keyguard.domain.interactor
import android.os.UserHandle
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase 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.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer 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.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
@@ -98,8 +101,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
val scope = CoroutineScope(IMMEDIATE) val scope = CoroutineScope(IMMEDIATE)
val selectionManager = val localUserSelectionManager =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock<UserFileManager>().apply { mock<UserFileManager>().apply {
@@ -115,20 +118,30 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
userTracker = userTracker, userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher, broadcastDispatcher = fakeBroadcastDispatcher,
) )
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
scope = scope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = scope, scope = scope,
selectionManager = selectionManager, localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
legacySettingSyncer = legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer( KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope, scope = scope,
backgroundDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(), secureSettings = FakeSettings(),
selectionsManager = selectionManager, selectionsManager = localUserSelectionManager,
), ),
configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
dumpManager = mock(), dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
) )
featureFlags = featureFlags =
FakeFeatureFlags().apply { FakeFeatureFlags().apply {

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel package com.android.systemui.keyguard.ui.viewmodel
import android.content.Intent import android.content.Intent
import android.os.UserHandle
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase 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.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer 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.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
@@ -121,8 +124,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
val scope = CoroutineScope(IMMEDIATE) val scope = CoroutineScope(IMMEDIATE)
val selectionManager = val localUserSelectionManager =
KeyguardQuickAffordanceSelectionManager( KeyguardQuickAffordanceLocalUserSelectionManager(
context = context, context = context,
userFileManager = userFileManager =
mock<UserFileManager>().apply { mock<UserFileManager>().apply {
@@ -138,17 +141,26 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
userTracker = userTracker, userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher, broadcastDispatcher = fakeBroadcastDispatcher,
) )
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
scope = scope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = scope, scope = scope,
selectionManager = selectionManager, localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
legacySettingSyncer = legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer( KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope, scope = scope,
backgroundDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(), secureSettings = FakeSettings(),
selectionsManager = selectionManager, selectionsManager = localUserSelectionManager,
), ),
configs = configs =
setOf( setOf(
@@ -157,6 +169,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
qrCodeScannerAffordanceConfig, qrCodeScannerAffordanceConfig,
), ),
dumpManager = mock(), dumpManager = mock(),
userHandle = UserHandle.SYSTEM,
) )
underTest = underTest =
KeyguardBottomAreaViewModel( KeyguardBottomAreaViewModel(

View File

@@ -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)
}
}

View File

@@ -66,7 +66,8 @@ class FakeUserTracker(
_userId = _userInfo.id _userId = _userInfo.id
_userHandle = UserHandle.of(_userId) _userHandle = UserHandle.of(_userId)
callbacks.forEach { it.onUserChanged(_userId, userContext) } val copy = callbacks.toList()
copy.forEach { it.onUserChanged(_userId, userContext) }
} }
fun onProfileChanged() { fun onProfileChanged() {