diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 88af1793d7716..7a362040427a8 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -817,4 +817,13 @@ bottom_end:1 + + + + diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt index c2658a9e61b12..f60db2ad2687f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -108,4 +108,30 @@ object KeyguardQuickAffordanceProviderContract { 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" + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 0f4581ce3e616..1f1ed007fca0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -31,7 +31,6 @@ import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCall import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject -import kotlinx.coroutines.runBlocking class KeyguardQuickAffordanceProvider : ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { @@ -57,6 +56,11 @@ class KeyguardQuickAffordanceProvider : Contract.SelectionTable.TABLE_NAME, MATCH_CODE_ALL_SELECTIONS, ) + addURI( + Contract.AUTHORITY, + Contract.FlagsTable.TABLE_NAME, + MATCH_CODE_ALL_FLAGS, + ) } override fun onCreate(): Boolean { @@ -77,6 +81,7 @@ class KeyguardQuickAffordanceProvider : when (uriMatcher.match(uri)) { MATCH_CODE_ALL_SLOTS, MATCH_CODE_ALL_AFFORDANCES, + MATCH_CODE_ALL_FLAGS, MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd." else -> null } @@ -86,6 +91,7 @@ class KeyguardQuickAffordanceProvider : MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME + MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME else -> null } @@ -115,6 +121,7 @@ class KeyguardQuickAffordanceProvider : MATCH_CODE_ALL_AFFORDANCES -> queryAffordances() MATCH_CODE_ALL_SLOTS -> querySlots() MATCH_CODE_ALL_SELECTIONS -> querySelections() + MATCH_CODE_ALL_FLAGS -> queryFlags() else -> null } } @@ -171,12 +178,11 @@ class KeyguardQuickAffordanceProvider : throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!") } - val success = runBlocking { + val success = interactor.select( slotId = slotId, affordanceId = affordanceId, ) - } return if (success) { Log.d(TAG, "Successfully selected $affordanceId for slot $slotId") @@ -196,7 +202,7 @@ class KeyguardQuickAffordanceProvider : ) ) .apply { - val affordanceIdsBySlotId = runBlocking { interactor.getSelections() } + val affordanceIdsBySlotId = interactor.getSelections() affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) -> affordanceIds.forEach { affordanceId -> 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( uri: Uri, selectionArgs: Array?, @@ -271,12 +300,11 @@ class KeyguardQuickAffordanceProvider : ) } - val deleted = runBlocking { + val deleted = interactor.unselect( slotId = slotId, affordanceId = affordanceId, ) - } return if (deleted) { 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_AFFORDANCES = 2 private const val MATCH_CODE_ALL_SELECTIONS = 3 + private const val MATCH_CODE_ALL_FLAGS = 4 } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt new file mode 100644 index 0000000000000..766096f1fa2bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt @@ -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 = 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> + .values // Collection> + .flatten() // List + .toSet() // Set + .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, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index 9c9354fec6950..b29cf45cc7094 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -17,46 +17,138 @@ 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.qualifiers.Application +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?". */ @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 _selections = MutableStateFlow>>(emptyMap()) + private val sharedPrefs: SharedPreferences + get() = + userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + + private val userId: Flow = conflatedCallbackFlow { + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging(newUser, TAG) + } + } + + userTracker.addCallback(callback) { it.run() } + trySendWithFailureLogging(userTracker.userId, TAG) + + awaitClose { userTracker.removeCallback(callback) } + } + private val defaults: Map> by lazy { + context.resources + .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) + .associate { item -> + val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) + check(splitUp.size == 2) + val slotId = splitUp[0] + val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) + slotId to affordanceIds + } + } /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ - val selections: Flow>> = _selections.asStateFlow() + val selections: Flow>> = + 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 * descending priority order. */ - suspend fun getSelections(): Map> { - return _selections.value + fun getSelections(): Map> { + val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } + val result = + slotKeys + .associate { key -> + val slotId = key.substring(KEY_PREFIX_SLOT.length) + val value = sharedPrefs.getString(key, null) + val affordanceIds = + if (!value.isNullOrEmpty()) { + value.split(AFFORDANCE_DELIMITER) + } else { + emptyList() + } + slotId to affordanceIds + } + .toMutableMap() + + // If the result map is missing keys, it means that the system has never set anything for + // those slots. This is where we need examine our defaults and see if there should be a + // default value for the affordances in the slot IDs that are missing from the result. + // + // Once the user makes any selection for a slot, even when they select "None", this class + // will persist a key for that slot ID. In the case of "None", it will have a value of the + // empty string. This is why this system works. + defaults.forEach { (slotId, affordanceIds) -> + if (!result.containsKey(slotId)) { + result[slotId] = affordanceIds + } + } + + return result } /** * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance * IDs should be descending priority order. */ - suspend fun setSelections( + fun setSelections( slotId: String, affordanceIds: List, ) { - // Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit - // when we set its value to the same instance of the original map, even if we change the - // map by updating the value of one of its keys. - val copy = _selections.value.toMutableMap() - copy[slotId] = affordanceIds - _selections.value = copy + val key = "$KEY_PREFIX_SLOT$slotId" + val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) + sharedPrefs.edit().putString(key, value).apply() + } + + 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 = "," } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index 9fda98c5f7c2c..533b3abf4fb65 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -21,19 +21,17 @@ import android.content.Context import com.android.systemui.R 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.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** Abstracts access to application state related to keyguard quick affordances. */ @SysUISingleton @@ -42,8 +40,8 @@ class KeyguardQuickAffordanceRepository constructor( @Application private val appContext: Context, @Application private val scope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, private val selectionManager: KeyguardQuickAffordanceSelectionManager, + legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer, 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 * slot with the given ID. The configs are sorted in descending priority order. */ - suspend fun getSelections(slotId: String): List { + fun getSelections(slotId: String): List { val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) 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 * are sorted in descending priority order. */ - suspend fun getSelections(): Map> { + fun getSelections(): Map> { return selectionManager.getSelections() } @@ -112,12 +114,10 @@ constructor( slotId: String, affordanceIds: List, ) { - scope.launch(backgroundDispatcher) { - selectionManager.setSelections( - slotId = slotId, - affordanceIds = affordanceIds, - ) - } + selectionManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIds, + ) } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 92caa89bb0e8b..45eb6f5012874 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -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.domain.model.KeyguardQuickAffordanceModel 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.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter 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.statusbar.policy.KeyguardStateController import dagger.Lazy @@ -117,7 +119,7 @@ constructor( * * @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) val slots = repository.get().getSlotPickerRepresentations() @@ -152,7 +154,7 @@ constructor( * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if * 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) val slots = repository.get().getSlotPickerRepresentations() @@ -187,7 +189,7 @@ constructor( } /** Returns affordance IDs indexed by slot ID, for all known slots. */ - suspend fun getSelections(): Map> { + fun getSelections(): Map> { check(isUsingRepository) val selections = repository.get().getSelections() @@ -314,6 +316,15 @@ constructor( return repository.get().getSlotPickerRepresentations() } + fun getPickerFlags(): List { + return listOf( + KeyguardPickerFlag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES), + ) + ) + } + companion object { private const val TAG = "KeyguardQuickAffordanceInteractor" private const val DELIMITER = "::" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt new file mode 100644 index 0000000000000..a7a5957cd5275 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt @@ -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, +) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index ed08849fe70c0..8395f02cbc41e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -27,17 +27,22 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots 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.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -46,6 +51,8 @@ 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.Mockito.verify import org.mockito.MockitoAnnotations @@ -66,12 +73,28 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) underTest = KeyguardQuickAffordanceProvider() + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + scope = scope, + selectionManager = selectionManager, configs = setOf( FakeKeyguardQuickAffordanceConfig( @@ -83,6 +106,13 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { pickerIconResourceId = 2, ), ), + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), ) underTest.interactor = KeyguardQuickAffordanceInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt new file mode 100644 index 0000000000000..8ef921eaa50ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index d2422ad7b53b3..d8ee9f113d33c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -17,111 +17,312 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.SharedPreferences +import android.content.pm.UserInfo 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.settings.UserFileManager +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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 @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { + @Mock private lateinit var userFileManager: UserFileManager + private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + private lateinit var userTracker: FakeUserTracker + private lateinit var sharedPrefs: MutableMap + @Before 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 - fun setSelections() = - runBlocking(IMMEDIATE) { - var affordanceIdsBySlotId: Map>? = null - val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this) - val slotId1 = "slot1" - val slotId2 = "slot2" - val affordanceId1 = "affordance1" - val affordanceId2 = "affordance2" - val affordanceId3 = "affordance3" + fun setSelections() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf()) + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1), + underTest.setSelections( + slotId = slotId1, + 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>>() + 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( - slotId1 to listOf(affordanceId1), + slot1 to listOf(affordance2), + slot2 to listOf(affordance3), ), - ) + ) - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(affordanceId2), - ) - assertSelections( - affordanceIdsBySlotId, + // Switch back to user 0. + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + // Assert that we still remember the old selections for user 0. + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = mapOf( - slotId1 to listOf(affordanceId1), - slotId2 to listOf(affordanceId2), - ) - ) + slot1 to listOf(affordance1), + slot2 to listOf(affordance2), + ), + ) - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1, affordanceId3), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId1, affordanceId3), - slotId2 to listOf(affordanceId2), - ) - ) + job.cancel() + } - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId3), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(affordanceId2), - ) - ) + @Test + fun `selections respects defaults`() = 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>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(), - ) - ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ), + ) - 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>>() + 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>>() + 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>?, expected: Map>, ) { assertThat(underTest.getSelections()).isEqualTo(expected) assertThat(observed).isEqualTo(expected) } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index dceb492af2ed0..d8a360567a07f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -22,9 +22,16 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.settings.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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,6 +43,8 @@ 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 @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -51,12 +60,36 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { fun setUp() { config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = FakeUserTracker(), + ) + underTest = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(config1, config2), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 737f242247273..1e1d3f19d83cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -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.FakeKeyguardQuickAffordanceConfig 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.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition 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.statusbar.policy.KeyguardStateController +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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runBlockingTest @@ -50,6 +55,8 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter 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.same import org.mockito.Mock @@ -201,7 +208,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller @Mock private lateinit var expandable: Expandable @@ -214,12 +220,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false @JvmField @Parameter(4) var startActivity: Boolean = false private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig + private lateinit var userTracker: UserTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(expandable.activityLaunchController()).thenReturn(animationController) + userTracker = FakeUserTracker() homeControls = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) val quickAccessWallet = @@ -228,12 +236,35 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ) val qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) underTest = @@ -319,7 +350,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { needStrongAuthAfterBoot: Boolean = true, keyguardIsUnlocked: Boolean = false, ) { - whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(any())) .thenReturn( if (needStrongAuthAfterBoot) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index ffcf832b95aec..c47e6f52c5962 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -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.FakeKeyguardQuickAffordanceConfig 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.repository.FakeKeyguardRepository 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.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots 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.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -53,6 +57,8 @@ 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 @@ -89,13 +95,36 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) featureFlags = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index fa2ac46f46d20..ecc63ecc879d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -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.FakeKeyguardQuickAffordanceConfig 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.repository.FakeKeyguardRepository 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.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker 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.mock +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min @@ -56,6 +60,7 @@ 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.Mockito import org.mockito.Mockito.verifyZeroInteractions @@ -115,12 +120,35 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf( homeControlsQuickAffordanceConfig,