Merge changes I2ea2baa4,Icd8ff155,I63484a92,If95af61c into tm-qpr-dev

* changes:
  Exposes feature flag to wallpaper picker.
  Legacy setting syncer.
  Defines config for default affordances.
  Persistence layer for quick affordances.
This commit is contained in:
Ale Nijamkin
2022-11-17 04:11:25 +00:00
committed by Android (Google) Code Review
15 changed files with 1071 additions and 124 deletions

View File

@@ -817,4 +817,13 @@
<item>bottom_end:1</item> <item>bottom_end:1</item>
</string-array> </string-array>
<!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
default is displayed by System UI as long as the user hasn't made a different choice for that
slot. If the user did make a choice, even if the choice is the "None" option, the default is
ignored. -->
<string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
</string-array>
</resources> </resources>

View File

@@ -108,4 +108,30 @@ object KeyguardQuickAffordanceProviderContract {
const val AFFORDANCE_ID = "affordance_id" const val AFFORDANCE_ID = "affordance_id"
} }
} }
/**
* Table for flags.
*
* Flags are key-value pairs.
*
* Supported operations:
* - Query - to know the values of flags, query the [FlagsTable.URI] [Uri]. The result set will
* contain rows, each of which with the columns from [FlagsTable.Columns].
*/
object FlagsTable {
const val TABLE_NAME = "flags"
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
/**
* Flag denoting whether the customizable lock screen quick affordances feature is enabled.
*/
const val FLAG_NAME_FEATURE_ENABLED = "is_feature_enabled"
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
/** Int. Value of the flag. `1` means `true` and `0` means `false`. */
const val VALUE = "value"
}
}
} }

View File

@@ -31,7 +31,6 @@ import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCall
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.runBlocking
class KeyguardQuickAffordanceProvider : class KeyguardQuickAffordanceProvider :
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
@@ -57,6 +56,11 @@ class KeyguardQuickAffordanceProvider :
Contract.SelectionTable.TABLE_NAME, Contract.SelectionTable.TABLE_NAME,
MATCH_CODE_ALL_SELECTIONS, MATCH_CODE_ALL_SELECTIONS,
) )
addURI(
Contract.AUTHORITY,
Contract.FlagsTable.TABLE_NAME,
MATCH_CODE_ALL_FLAGS,
)
} }
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
@@ -77,6 +81,7 @@ class KeyguardQuickAffordanceProvider :
when (uriMatcher.match(uri)) { when (uriMatcher.match(uri)) {
MATCH_CODE_ALL_SLOTS, MATCH_CODE_ALL_SLOTS,
MATCH_CODE_ALL_AFFORDANCES, MATCH_CODE_ALL_AFFORDANCES,
MATCH_CODE_ALL_FLAGS,
MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd." MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
else -> null else -> null
} }
@@ -86,6 +91,7 @@ class KeyguardQuickAffordanceProvider :
MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME
else -> null else -> null
} }
@@ -115,6 +121,7 @@ class KeyguardQuickAffordanceProvider :
MATCH_CODE_ALL_AFFORDANCES -> queryAffordances() MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
MATCH_CODE_ALL_SLOTS -> querySlots() MATCH_CODE_ALL_SLOTS -> querySlots()
MATCH_CODE_ALL_SELECTIONS -> querySelections() MATCH_CODE_ALL_SELECTIONS -> querySelections()
MATCH_CODE_ALL_FLAGS -> queryFlags()
else -> null else -> null
} }
} }
@@ -171,12 +178,11 @@ class KeyguardQuickAffordanceProvider :
throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!") throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
} }
val success = runBlocking { val success =
interactor.select( interactor.select(
slotId = slotId, slotId = slotId,
affordanceId = affordanceId, affordanceId = affordanceId,
) )
}
return if (success) { return if (success) {
Log.d(TAG, "Successfully selected $affordanceId for slot $slotId") Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
@@ -196,7 +202,7 @@ class KeyguardQuickAffordanceProvider :
) )
) )
.apply { .apply {
val affordanceIdsBySlotId = runBlocking { interactor.getSelections() } val affordanceIdsBySlotId = interactor.getSelections()
affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) -> affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
affordanceIds.forEach { affordanceId -> affordanceIds.forEach { affordanceId ->
addRow( addRow(
@@ -250,6 +256,29 @@ class KeyguardQuickAffordanceProvider :
} }
} }
private fun queryFlags(): Cursor {
return MatrixCursor(
arrayOf(
Contract.FlagsTable.Columns.NAME,
Contract.FlagsTable.Columns.VALUE,
)
)
.apply {
interactor.getPickerFlags().forEach { flag ->
addRow(
arrayOf(
flag.name,
if (flag.value) {
1
} else {
0
},
)
)
}
}
}
private fun deleteSelection( private fun deleteSelection(
uri: Uri, uri: Uri,
selectionArgs: Array<out String>?, selectionArgs: Array<out String>?,
@@ -271,12 +300,11 @@ class KeyguardQuickAffordanceProvider :
) )
} }
val deleted = runBlocking { val deleted =
interactor.unselect( interactor.unselect(
slotId = slotId, slotId = slotId,
affordanceId = affordanceId, affordanceId = affordanceId,
) )
}
return if (deleted) { return if (deleted) {
Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId") Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
@@ -293,5 +321,6 @@ class KeyguardQuickAffordanceProvider :
private const val MATCH_CODE_ALL_SLOTS = 1 private const val MATCH_CODE_ALL_SLOTS = 1
private const val MATCH_CODE_ALL_AFFORDANCES = 2 private const val MATCH_CODE_ALL_AFFORDANCES = 2
private const val MATCH_CODE_ALL_SELECTIONS = 3 private const val MATCH_CODE_ALL_SELECTIONS = 3
private const val MATCH_CODE_ALL_FLAGS = 4
} }
} }

View File

@@ -0,0 +1,214 @@
/*
* 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.os.UserHandle
import android.provider.Settings
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer.Companion.BINDINGS
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Keeps quick affordance selections and legacy user settings in sync.
*
* "Legacy user settings" are user settings like: Settings > Display > Lock screen > "Show device
* controls" Settings > Display > Lock screen > "Show wallet"
*
* Quick affordance selections are the ones available through the new custom lock screen experience
* from Settings > Wallpaper & Style.
*
* This class keeps these in sync, mostly for backwards compatibility purposes and in order to not
* "forget" an existing legacy user setting when the device gets updated with a version of System UI
* that has the new customizable lock screen feature.
*
* The way it works is that, when [startSyncing] is called, the syncer starts coroutines to listen
* for changes in both legacy user settings and their respective affordance selections. Whenever one
* of each pair is changed, the other member of that pair is also updated to match. For example, if
* the user turns on "Show device controls", we automatically select the home controls affordance
* for the preferred slot. Conversely, when the home controls affordance is unselected by the user,
* we set the "Show device controls" setting to "off".
*
* The class can be configured by updating its list of triplets in the code under [BINDINGS].
*/
@SysUISingleton
class KeyguardQuickAffordanceLegacySettingSyncer
@Inject
constructor(
@Application private val scope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val secureSettings: SecureSettings,
private val selectionsManager: KeyguardQuickAffordanceSelectionManager,
) {
companion object {
private val BINDINGS =
listOf(
Binding(
settingsKey = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
affordanceId = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
),
Binding(
settingsKey = Settings.Secure.LOCKSCREEN_SHOW_WALLET,
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
affordanceId = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET,
),
Binding(
settingsKey = Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER,
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
affordanceId = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER,
),
)
}
fun startSyncing(
bindings: List<Binding> = BINDINGS,
): Job {
return scope.launch { bindings.forEach { binding -> startSyncing(this, binding) } }
}
private fun startSyncing(
scope: CoroutineScope,
binding: Binding,
) {
secureSettings
.observerFlow(
names = arrayOf(binding.settingsKey),
userId = UserHandle.USER_ALL,
)
.map {
isSet(
settingsKey = binding.settingsKey,
)
}
.distinctUntilChanged()
.onEach { isSet ->
if (isSelected(binding.affordanceId) != isSet) {
if (isSet) {
select(
slotId = binding.slotId,
affordanceId = binding.affordanceId,
)
} else {
unselect(
affordanceId = binding.affordanceId,
)
}
}
}
.flowOn(backgroundDispatcher)
.launchIn(scope)
selectionsManager.selections
.map { it.values.flatten().toSet() }
.map { it.contains(binding.affordanceId) }
.distinctUntilChanged()
.onEach { isSelected ->
if (isSet(binding.settingsKey) != isSelected) {
set(binding.settingsKey, isSelected)
}
}
.flowOn(backgroundDispatcher)
.launchIn(scope)
}
private fun isSelected(
affordanceId: String,
): Boolean {
return selectionsManager
.getSelections() // Map<String, List<String>>
.values // Collection<List<String>>
.flatten() // List<String>
.toSet() // Set<String>
.contains(affordanceId)
}
private fun select(
slotId: String,
affordanceId: String,
) {
val affordanceIdsAtSlotId = selectionsManager.getSelections()[slotId] ?: emptyList()
selectionsManager.setSelections(
slotId = slotId,
affordanceIds = affordanceIdsAtSlotId + listOf(affordanceId),
)
}
private fun unselect(
affordanceId: String,
) {
val currentSelections = selectionsManager.getSelections()
val slotIdsContainingAffordanceId =
currentSelections
.filter { (_, affordanceIds) -> affordanceIds.contains(affordanceId) }
.map { (slotId, _) -> slotId }
slotIdsContainingAffordanceId.forEach { slotId ->
val currentAffordanceIds = currentSelections[slotId] ?: emptyList()
val affordanceIdsAfterUnselecting =
currentAffordanceIds.toMutableList().apply { remove(affordanceId) }
selectionsManager.setSelections(
slotId = slotId,
affordanceIds = affordanceIdsAfterUnselecting,
)
}
}
private fun isSet(
settingsKey: String,
): Boolean {
return secureSettings.getIntForUser(
settingsKey,
0,
UserHandle.USER_CURRENT,
) != 0
}
private suspend fun set(
settingsKey: String,
isSet: Boolean,
) {
withContext(backgroundDispatcher) {
secureSettings.putInt(
settingsKey,
if (isSet) 1 else 0,
)
}
}
data class Binding(
val settingsKey: String,
val slotId: String,
val affordanceId: String,
)
}

View File

@@ -17,46 +17,138 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
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.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 javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.asStateFlow
/** /**
* Manages and provides access to the current "selections" of keyguard quick affordances, answering * Manages and provides access to the current "selections" of keyguard quick affordances, answering
* the question "which affordances should the keyguard show?". * the question "which affordances should the keyguard show?".
*/ */
@SysUISingleton @SysUISingleton
class KeyguardQuickAffordanceSelectionManager @Inject constructor() { class KeyguardQuickAffordanceSelectionManager
@Inject
constructor(
@Application context: Context,
private val userFileManager: UserFileManager,
private val userTracker: UserTracker,
) {
// TODO(b/254858695): implement a persistence layer (database). private val sharedPrefs: SharedPreferences
private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) get() =
userFileManager.getSharedPreferences(
FILE_NAME,
Context.MODE_PRIVATE,
userTracker.userId,
)
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
}
}
/** 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>>> = _selections.asStateFlow() val selections: Flow<Map<String, List<String>>> =
userId.flatMapLatest {
conflatedCallbackFlow {
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.
*/ */
suspend fun getSelections(): Map<String, List<String>> { fun getSelections(): Map<String, List<String>> {
return _selections.value 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
* IDs should be descending priority order. * IDs should be descending priority order.
*/ */
suspend fun setSelections( fun setSelections(
slotId: String, slotId: String,
affordanceIds: List<String>, affordanceIds: List<String>,
) { ) {
// Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit val key = "$KEY_PREFIX_SLOT$slotId"
// when we set its value to the same instance of the original map, even if we change the val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
// map by updating the value of one of its keys. sharedPrefs.edit().putString(key, value).apply()
val copy = _selections.value.toMutableMap() }
copy[slotId] = affordanceIds
_selections.value = copy companion object {
private const val TAG = "KeyguardQuickAffordanceSelectionManager"
@VisibleForTesting 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

@@ -21,19 +21,17 @@ import android.content.Context
import com.android.systemui.R import com.android.systemui.R
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.dagger.qualifiers.Background
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.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 javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Abstracts access to application state related to keyguard quick affordances. */ /** Abstracts access to application state related to keyguard quick affordances. */
@SysUISingleton @SysUISingleton
@@ -42,8 +40,8 @@ class KeyguardQuickAffordanceRepository
constructor( constructor(
@Application private val appContext: Context, @Application private val appContext: Context,
@Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val selectionManager: KeyguardQuickAffordanceSelectionManager, private val selectionManager: KeyguardQuickAffordanceSelectionManager,
legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
) { ) {
/** /**
@@ -87,11 +85,15 @@ constructor(
} }
} }
init {
legacySettingSyncer.startSyncing()
}
/** /**
* Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
* 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.
*/ */
suspend fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList())
return configs.filter { selections.contains(it.key) } return configs.filter { selections.contains(it.key) }
} }
@@ -100,7 +102,7 @@ constructor(
* Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
* are sorted in descending priority order. * are sorted in descending priority order.
*/ */
suspend fun getSelections(): Map<String, List<String>> { fun getSelections(): Map<String, List<String>> {
return selectionManager.getSelections() return selectionManager.getSelections()
} }
@@ -112,12 +114,10 @@ constructor(
slotId: String, slotId: String,
affordanceIds: List<String>, affordanceIds: List<String>,
) { ) {
scope.launch(backgroundDispatcher) { selectionManager.setSelections(
selectionManager.setSelections( slotId = slotId,
slotId = slotId, affordanceIds = affordanceIds,
affordanceIds = affordanceIds, )
)
}
} }
/** /**

View File

@@ -28,11 +28,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
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
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag
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.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import dagger.Lazy import dagger.Lazy
@@ -117,7 +119,7 @@ constructor(
* *
* @return `true` if the affordance was selected successfully; `false` otherwise. * @return `true` if the affordance was selected successfully; `false` otherwise.
*/ */
suspend fun select(slotId: String, affordanceId: String): Boolean { fun select(slotId: String, affordanceId: String): Boolean {
check(isUsingRepository) check(isUsingRepository)
val slots = repository.get().getSlotPickerRepresentations() val slots = repository.get().getSlotPickerRepresentations()
@@ -152,7 +154,7 @@ constructor(
* @return `true` if the affordance was successfully removed; `false` otherwise (for example, if * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
* the affordance was not on the slot to begin with). * the affordance was not on the slot to begin with).
*/ */
suspend fun unselect(slotId: String, affordanceId: String?): Boolean { fun unselect(slotId: String, affordanceId: String?): Boolean {
check(isUsingRepository) check(isUsingRepository)
val slots = repository.get().getSlotPickerRepresentations() val slots = repository.get().getSlotPickerRepresentations()
@@ -187,7 +189,7 @@ 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<String>> { fun getSelections(): Map<String, List<String>> {
check(isUsingRepository) check(isUsingRepository)
val selections = repository.get().getSelections() val selections = repository.get().getSelections()
@@ -314,6 +316,15 @@ constructor(
return repository.get().getSlotPickerRepresentations() return repository.get().getSlotPickerRepresentations()
} }
fun getPickerFlags(): List<KeyguardPickerFlag> {
return listOf(
KeyguardPickerFlag(
name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED,
value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
)
)
}
companion object { companion object {
private const val TAG = "KeyguardQuickAffordanceInteractor" private const val TAG = "KeyguardQuickAffordanceInteractor"
private const val DELIMITER = "::" private const val DELIMITER = "::"

View File

@@ -0,0 +1,24 @@
/*
* 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.shared.model
/** Represents a flag that's consumed by the settings or wallpaper picker app. */
data class KeyguardPickerFlag(
val name: String,
val value: Boolean,
)

View File

@@ -27,17 +27,22 @@ 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.KeyguardQuickAffordanceLegacySettingSyncer
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
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
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
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.settings.FakeSettings
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -46,6 +51,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@@ -66,12 +73,28 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
underTest = KeyguardQuickAffordanceProvider() underTest = KeyguardQuickAffordanceProvider()
val scope = CoroutineScope(IMMEDIATE)
val selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = CoroutineScope(IMMEDIATE), scope = scope,
backgroundDispatcher = IMMEDIATE, selectionManager = selectionManager,
selectionManager = KeyguardQuickAffordanceSelectionManager(),
configs = configs =
setOf( setOf(
FakeKeyguardQuickAffordanceConfig( FakeKeyguardQuickAffordanceConfig(
@@ -83,6 +106,13 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
pickerIconResourceId = 2, pickerIconResourceId = 2,
), ),
), ),
legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope,
backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(),
selectionsManager = selectionManager,
),
) )
underTest.interactor = underTest.interactor =
KeyguardQuickAffordanceInteractor( KeyguardQuickAffordanceInteractor(

View File

@@ -0,0 +1,191 @@
/*
* 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.res.Resources
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
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.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
@Mock private lateinit var sharedPrefs: FakeSharedPreferences
private lateinit var underTest: KeyguardQuickAffordanceLegacySettingSyncer
private lateinit var testScope: TestScope
private lateinit var testDispatcher: TestDispatcher
private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager
private lateinit var settings: FakeSettings
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val context: Context = mock()
sharedPrefs = FakeSharedPreferences()
whenever(context.getSharedPreferences(anyString(), any())).thenReturn(sharedPrefs)
val resources: Resources = mock()
whenever(resources.getStringArray(R.array.config_keyguardQuickAffordanceDefaults))
.thenReturn(emptyArray())
whenever(context.resources).thenReturn(resources)
testDispatcher = UnconfinedTestDispatcher()
testScope = TestScope(testDispatcher)
selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = FakeUserTracker(),
)
settings = FakeSettings()
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET, 0)
settings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0)
underTest =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = testScope,
backgroundDispatcher = testDispatcher,
secureSettings = settings,
selectionsManager = selectionManager,
)
}
@Test
fun `Setting a setting selects the affordance`() =
testScope.runTest {
val job = underTest.startSyncing()
settings.putInt(
Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
1,
)
assertThat(
selectionManager
.getSelections()
.getOrDefault(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
emptyList()
)
)
.contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
job.cancel()
}
@Test
fun `Clearing a setting selects the affordance`() =
testScope.runTest {
val job = underTest.startSyncing()
settings.putInt(
Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
1,
)
settings.putInt(
Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
0,
)
assertThat(
selectionManager
.getSelections()
.getOrDefault(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
emptyList()
)
)
.doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
job.cancel()
}
@Test
fun `Selecting an affordance sets its setting`() =
testScope.runTest {
val job = underTest.startSyncing()
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
)
advanceUntilIdle()
assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(1)
job.cancel()
}
@Test
fun `Unselecting an affordance clears its setting`() =
testScope.runTest {
val job = underTest.startSyncing()
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
)
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
emptyList()
)
assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0)
job.cancel()
}
}

View File

@@ -17,111 +17,312 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.content.SharedPreferences
import android.content.pm.UserInfo
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserFileManager
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest
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
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
@Mock private lateinit var userFileManager: UserFileManager
private lateinit var underTest: KeyguardQuickAffordanceSelectionManager private lateinit var underTest: KeyguardQuickAffordanceSelectionManager
private lateinit var userTracker: FakeUserTracker
private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences>
@Before @Before
fun setUp() { fun setUp() {
underTest = KeyguardQuickAffordanceSelectionManager() MockitoAnnotations.initMocks(this)
sharedPrefs = mutableMapOf()
whenever(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())).thenAnswer {
val userId = it.arguments[2] as Int
sharedPrefs.getOrPut(userId) { FakeSharedPreferences() }
}
userTracker = FakeUserTracker()
underTest =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager = userFileManager,
userTracker = userTracker,
)
} }
@Test @Test
fun setSelections() = fun setSelections() = runTest {
runBlocking(IMMEDIATE) { overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
var affordanceIdsBySlotId: Map<String, List<String>>? = null val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this) val job =
val slotId1 = "slot1" launch(UnconfinedTestDispatcher()) {
val slotId2 = "slot2" underTest.selections.toList(affordanceIdsBySlotId)
val affordanceId1 = "affordance1" }
val affordanceId2 = "affordance2" val slotId1 = "slot1"
val affordanceId3 = "affordance3" val slotId2 = "slot2"
val affordanceId1 = "affordance1"
val affordanceId2 = "affordance2"
val affordanceId3 = "affordance3"
underTest.setSelections( underTest.setSelections(
slotId = slotId1, slotId = slotId1,
affordanceIds = listOf(affordanceId1), affordanceIds = listOf(affordanceId1),
)
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId1),
),
)
underTest.setSelections(
slotId = slotId2,
affordanceIds = listOf(affordanceId2),
)
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId1),
slotId2 to listOf(affordanceId2),
) )
assertSelections( )
affordanceIdsBySlotId,
underTest.setSelections(
slotId = slotId1,
affordanceIds = listOf(affordanceId1, affordanceId3),
)
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId1, affordanceId3),
slotId2 to listOf(affordanceId2),
)
)
underTest.setSelections(
slotId = slotId1,
affordanceIds = listOf(affordanceId3),
)
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId3),
slotId2 to listOf(affordanceId2),
)
)
underTest.setSelections(
slotId = slotId2,
affordanceIds = listOf(),
)
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId3),
slotId2 to listOf(),
)
)
job.cancel()
}
@Test
fun `remembers selections by user`() = runTest {
val slot1 = "slot_1"
val slot2 = "slot_2"
val affordance1 = "affordance_1"
val affordance2 = "affordance_2"
val affordance3 = "affordance_3"
val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
val job =
launch(UnconfinedTestDispatcher()) {
underTest.selections.toList(affordanceIdsBySlotId)
}
val userInfos =
listOf(
UserInfo(/* id= */ 0, "zero", /* flags= */ 0),
UserInfo(/* id= */ 1, "one", /* flags= */ 0),
)
userTracker.set(
userInfos = userInfos,
selectedUserIndex = 0,
)
underTest.setSelections(
slotId = slot1,
affordanceIds = listOf(affordance1),
)
underTest.setSelections(
slotId = slot2,
affordanceIds = listOf(affordance2),
)
// Switch to user 1
userTracker.set(
userInfos = userInfos,
selectedUserIndex = 1,
)
// We never set selections on user 1, so it should be empty.
assertSelections(
observed = affordanceIdsBySlotId.last(),
expected = emptyMap(),
)
// Now, let's set selections on user 1.
underTest.setSelections(
slotId = slot1,
affordanceIds = listOf(affordance2),
)
underTest.setSelections(
slotId = slot2,
affordanceIds = listOf(affordance3),
)
assertSelections(
observed = affordanceIdsBySlotId.last(),
expected =
mapOf( mapOf(
slotId1 to listOf(affordanceId1), slot1 to listOf(affordance2),
slot2 to listOf(affordance3),
), ),
) )
underTest.setSelections( // Switch back to user 0.
slotId = slotId2, userTracker.set(
affordanceIds = listOf(affordanceId2), userInfos = userInfos,
) selectedUserIndex = 0,
assertSelections( )
affordanceIdsBySlotId, // Assert that we still remember the old selections for user 0.
assertSelections(
observed = affordanceIdsBySlotId.last(),
expected =
mapOf( mapOf(
slotId1 to listOf(affordanceId1), slot1 to listOf(affordance1),
slotId2 to listOf(affordanceId2), slot2 to listOf(affordance2),
) ),
) )
underTest.setSelections( job.cancel()
slotId = slotId1, }
affordanceIds = listOf(affordanceId1, affordanceId3),
)
assertSelections(
affordanceIdsBySlotId,
mapOf(
slotId1 to listOf(affordanceId1, affordanceId3),
slotId2 to listOf(affordanceId2),
)
)
underTest.setSelections( @Test
slotId = slotId1, fun `selections respects defaults`() = runTest {
affordanceIds = listOf(affordanceId3), val slotId1 = "slot1"
) val slotId2 = "slot2"
assertSelections( val affordanceId1 = "affordance1"
affordanceIdsBySlotId, val affordanceId2 = "affordance2"
mapOf( val affordanceId3 = "affordance3"
slotId1 to listOf(affordanceId3), overrideResource(
slotId2 to listOf(affordanceId2), R.array.config_keyguardQuickAffordanceDefaults,
) arrayOf(
) "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
"$slotId2:${listOf(affordanceId2).joinToString(",")}",
),
)
val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
val job =
launch(UnconfinedTestDispatcher()) {
underTest.selections.toList(affordanceIdsBySlotId)
}
underTest.setSelections( assertSelections(
slotId = slotId2, affordanceIdsBySlotId.last(),
affordanceIds = listOf(), mapOf(
) slotId1 to listOf(affordanceId1, affordanceId3),
assertSelections( slotId2 to listOf(affordanceId2),
affordanceIdsBySlotId, ),
mapOf( )
slotId1 to listOf(affordanceId3),
slotId2 to listOf(),
)
)
job.cancel() job.cancel()
} }
private suspend fun assertSelections( @Test
fun `selections ignores defaults after selecting an affordance`() = runTest {
val slotId1 = "slot1"
val slotId2 = "slot2"
val affordanceId1 = "affordance1"
val affordanceId2 = "affordance2"
val affordanceId3 = "affordance3"
overrideResource(
R.array.config_keyguardQuickAffordanceDefaults,
arrayOf(
"$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
"$slotId2:${listOf(affordanceId2).joinToString(",")}",
),
)
val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
val job =
launch(UnconfinedTestDispatcher()) {
underTest.selections.toList(affordanceIdsBySlotId)
}
underTest.setSelections(slotId1, listOf(affordanceId2))
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(affordanceId2),
slotId2 to listOf(affordanceId2),
),
)
job.cancel()
}
@Test
fun `selections ignores defaults after clearing a slot`() = runTest {
val slotId1 = "slot1"
val slotId2 = "slot2"
val affordanceId1 = "affordance1"
val affordanceId2 = "affordance2"
val affordanceId3 = "affordance3"
overrideResource(
R.array.config_keyguardQuickAffordanceDefaults,
arrayOf(
"$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
"$slotId2:${listOf(affordanceId2).joinToString(",")}",
),
)
val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
val job =
launch(UnconfinedTestDispatcher()) {
underTest.selections.toList(affordanceIdsBySlotId)
}
underTest.setSelections(slotId1, listOf())
assertSelections(
affordanceIdsBySlotId.last(),
mapOf(
slotId1 to listOf(),
slotId2 to listOf(affordanceId2),
),
)
job.cancel()
}
private fun assertSelections(
observed: Map<String, List<String>>?, observed: Map<String, List<String>>?,
expected: Map<String, List<String>>, expected: Map<String, List<String>>,
) { ) {
assertThat(underTest.getSelections()).isEqualTo(expected) assertThat(underTest.getSelections()).isEqualTo(expected)
assertThat(observed).isEqualTo(expected) assertThat(observed).isEqualTo(expected)
} }
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
} }

View File

@@ -22,9 +22,16 @@ 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.KeyguardQuickAffordanceConfig 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.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.FakeUserTracker
import com.android.systemui.settings.UserFileManager
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -36,6 +43,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
@@ -51,12 +60,36 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
fun setUp() { fun setUp() {
config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") config1 = FakeKeyguardQuickAffordanceConfig("built_in:1")
config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") config2 = FakeKeyguardQuickAffordanceConfig("built_in:2")
val scope = CoroutineScope(IMMEDIATE)
val selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = FakeUserTracker(),
)
underTest = underTest =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = CoroutineScope(IMMEDIATE), scope = scope,
backgroundDispatcher = IMMEDIATE, selectionManager = selectionManager,
selectionManager = KeyguardQuickAffordanceSelectionManager(), legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope,
backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(),
selectionsManager = selectionManager,
),
configs = setOf(config1, config2), configs = setOf(config1, config2),
) )
} }

View File

@@ -30,17 +30,22 @@ 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.KeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceSelectionManager
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
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
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
import com.android.systemui.util.settings.FakeSettings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
@@ -50,6 +55,8 @@ import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameter
import org.junit.runners.Parameterized.Parameters import org.junit.runners.Parameterized.Parameters
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.same import org.mockito.ArgumentMatchers.same
import org.mockito.Mock import org.mockito.Mock
@@ -201,7 +208,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
@Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
@Mock private lateinit var expandable: Expandable @Mock private lateinit var expandable: Expandable
@@ -214,12 +220,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
@JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false
@JvmField @Parameter(4) var startActivity: Boolean = false @JvmField @Parameter(4) var startActivity: Boolean = false
private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
private lateinit var userTracker: UserTracker
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(expandable.activityLaunchController()).thenReturn(animationController) whenever(expandable.activityLaunchController()).thenReturn(animationController)
userTracker = FakeUserTracker()
homeControls = homeControls =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
val quickAccessWallet = val quickAccessWallet =
@@ -228,12 +236,35 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
) )
val qrCodeScanner = val qrCodeScanner =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
val scope = CoroutineScope(IMMEDIATE)
val selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = CoroutineScope(IMMEDIATE), scope = scope,
backgroundDispatcher = IMMEDIATE, selectionManager = selectionManager,
selectionManager = KeyguardQuickAffordanceSelectionManager(), legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope,
backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(),
selectionsManager = selectionManager,
),
configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
) )
underTest = underTest =
@@ -319,7 +350,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
needStrongAuthAfterBoot: Boolean = true, needStrongAuthAfterBoot: Boolean = true,
keyguardIsUnlocked: Boolean = false, keyguardIsUnlocked: Boolean = false,
) { ) {
whenever(userTracker.userHandle).thenReturn(mock())
whenever(lockPatternUtils.getStrongAuthForUser(any())) whenever(lockPatternUtils.getStrongAuthForUser(any()))
.thenReturn( .thenReturn(
if (needStrongAuthAfterBoot) { if (needStrongAuthAfterBoot) {

View File

@@ -27,6 +27,7 @@ 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.KeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceSelectionManager
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
@@ -35,11 +36,14 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
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
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -53,6 +57,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@@ -89,13 +95,36 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
) )
qrCodeScanner = qrCodeScanner =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
val scope = CoroutineScope(IMMEDIATE)
val selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = CoroutineScope(IMMEDIATE), scope = scope,
backgroundDispatcher = IMMEDIATE, selectionManager = selectionManager,
selectionManager = KeyguardQuickAffordanceSelectionManager(), legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope,
backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(),
selectionsManager = selectionManager,
),
configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
) )
featureFlags = featureFlags =

View File

@@ -28,6 +28,7 @@ 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.KeyguardQuickAffordanceConfig 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.KeyguardQuickAffordanceSelectionManager
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
@@ -38,10 +39,13 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -56,6 +60,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.verifyZeroInteractions
@@ -115,12 +120,35 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
whenever(userTracker.userHandle).thenReturn(mock()) whenever(userTracker.userHandle).thenReturn(mock())
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 selectionManager =
KeyguardQuickAffordanceSelectionManager(
context = context,
userFileManager =
mock<UserFileManager>().apply {
whenever(
getSharedPreferences(
anyString(),
anyInt(),
anyInt(),
)
)
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
)
val quickAffordanceRepository = val quickAffordanceRepository =
KeyguardQuickAffordanceRepository( KeyguardQuickAffordanceRepository(
appContext = context, appContext = context,
scope = CoroutineScope(IMMEDIATE), scope = scope,
backgroundDispatcher = IMMEDIATE, selectionManager = selectionManager,
selectionManager = KeyguardQuickAffordanceSelectionManager(), legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
scope = scope,
backgroundDispatcher = IMMEDIATE,
secureSettings = FakeSettings(),
selectionsManager = selectionManager,
),
configs = configs =
setOf( setOf(
homeControlsQuickAffordanceConfig, homeControlsQuickAffordanceConfig,