diff options
| author | 2022-11-03 11:52:07 +0000 | |
|---|---|---|
| committer | 2022-11-03 11:52:07 +0000 | |
| commit | 33b100711a97d723b0ba151b2a614e39b76fbb33 (patch) | |
| tree | e1eaef38fcdcf22e5c51fbdc233ebba620bf2e06 | |
| parent | 9dca5002952136cfacb1b6cb889fbfa67ce2b91e (diff) | |
| parent | d3d2edb731e2e04c00829dfa2a70dcc60bfdeec8 (diff) | |
Merge changes Ie810d37d,I6947e1f7 into tm-qpr-dev
* changes:
Quick affordance interactor uses repository.
Quick affordance repository.
21 files changed, 1165 insertions, 52 deletions
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt new file mode 100644 index 000000000000..2dc7a280e423 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt @@ -0,0 +1,27 @@ +/* + * 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.shared.keyguard.shared.model + +/** + * Collection of all supported "slots", placements where keyguard quick affordances can appear on + * the lock screen. + */ +object KeyguardQuickAffordanceSlots { + const val SLOT_ID_BOTTOM_START = "bottom_start" + const val SLOT_ID_BOTTOM_END = "bottom_end" +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index a677acf2ce6d..28378c93501b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -125,6 +125,14 @@ object Flags { // TODO(b/255607168): Tracking Bug @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213) + /** + * Whether to enable the code powering customizable lock screen quick affordances. + * + * Note that this flag does not enable individual implementations of quick affordances like the + * new camera quick affordance. Look for individual flags for those. + */ + @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = UnreleasedFlag(214, teamfood = false) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = ReleasedFlag(300) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56a1f1ae936e..f08463bfd5e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -42,6 +42,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; @@ -71,6 +72,7 @@ import dagger.Provides; KeyguardUserSwitcherComponent.class}, includes = { FalsingModule.class, + KeyguardDataQuickAffordanceModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, StartKeyguardTransitionModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index c600e13cc2dd..d6f521c6dc87 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -53,6 +53,10 @@ constructor( override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + override val pickerName: String by lazy { context.getString(component.getTileTitleId()) } + + override val pickerIconResourceId: Int by lazy { component.getTileImageId() } + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked -> if (canShowWhileLocked) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt new file mode 100644 index 000000000000..bea9363efc81 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -0,0 +1,39 @@ +/* + * 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 dagger.Module +import dagger.Provides +import dagger.multibindings.ElementsIntoSet + +@Module +object KeyguardDataQuickAffordanceModule { + @Provides + @ElementsIntoSet + fun quickAffordanceConfigs( + home: HomeControlsKeyguardQuickAffordanceConfig, + quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, + qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, + ): Set<KeyguardQuickAffordanceConfig> { + return setOf( + home, + quickAccessWallet, + qrCodeScanner, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 0a8090bb11ac..fd40d1dd1a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -29,6 +29,10 @@ interface KeyguardQuickAffordanceConfig { /** Unique identifier for this quick affordance. It must be globally unique. */ val key: String + val pickerName: String + + val pickerIconResourceId: Int + /** * The ever-changing state of the affordance. * 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 new file mode 100644 index 000000000000..9c9354fec695 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * 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() { + + // TODO(b/254858695): implement a persistence layer (database). + private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + + /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ + val selections: Flow<Map<String, List<String>>> = _selections.asStateFlow() + + /** + * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in + * descending priority order. + */ + suspend fun getSelections(): Map<String, List<String>> { + return _selections.value + } + + /** + * 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( + slotId: String, + affordanceIds: List<String>, + ) { + // 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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index d620b2a1654c..11f72ffa4757 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Context import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging @@ -24,6 +25,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -34,11 +36,16 @@ import kotlinx.coroutines.flow.Flow class QrCodeScannerKeyguardQuickAffordanceConfig @Inject constructor( + @Application context: Context, private val controller: QRCodeScannerController, ) : KeyguardQuickAffordanceConfig { override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + override val pickerName = context.getString(R.string.qr_code_scanner_title) + + override val pickerIconResourceId = R.drawable.ic_qr_code_scanner + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index be57a323b5d1..303e6a1a95cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Context import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsError import android.service.quickaccesswallet.GetWalletCardsResponse @@ -29,6 +30,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.wallet.controller.QuickAccessWalletController import javax.inject.Inject @@ -40,12 +42,17 @@ import kotlinx.coroutines.flow.Flow class QuickAccessWalletKeyguardQuickAffordanceConfig @Inject constructor( + @Application context: Context, private val walletController: QuickAccessWalletController, private val activityStarter: ActivityStarter, ) : KeyguardQuickAffordanceConfig { override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + override val pickerName = context.getString(R.string.accessibility_wallet_button) + + override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = conflatedCallbackFlow { val callback = 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 new file mode 100644 index 000000000000..95f614fbf7b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -0,0 +1,128 @@ +/* + * 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.repository + +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.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation +import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +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 +class KeyguardQuickAffordanceRepository +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val selectionManager: KeyguardQuickAffordanceSelectionManager, + private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, +) { + /** + * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the + * given ID. The configs are sorted in descending priority order. + */ + val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> = + selectionManager.selections + .map { selectionsBySlotId -> + selectionsBySlotId.mapValues { (_, selections) -> + configs.filter { selections.contains(it.key) } + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) + + /** + * 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<KeyguardQuickAffordanceConfig> { + val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) + return configs.filter { selections.contains(it.key) } + } + + /** + * 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<String, List<String>> { + return selectionManager.getSelections() + } + + /** + * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance + * IDs should be descending priority order. + */ + fun setSelections( + slotId: String, + affordanceIds: List<String>, + ) { + scope.launch(backgroundDispatcher) { + selectionManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIds, + ) + } + } + + /** + * Returns the list of representation objects for all known affordances, regardless of what is + * selected. This is useful for building experiences like the picker/selector or user settings + * so the user can see everything that can be selected in a menu. + */ + fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> { + return configs.map { config -> + KeyguardQuickAffordancePickerRepresentation( + id = config.key, + name = config.pickerName, + iconResourceId = config.pickerIconResourceId, + ) + } + } + + /** + * Returns the list of representation objects for all available slots on the keyguard. This is + * useful for building experiences like the picker/selector or user settings so the user can see + * each slot and select which affordance(s) is/are installed in each slot on the keyguard. + */ + fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { + // TODO(b/256195304): source these from a config XML file. + return listOf( + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + ), + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + ), + ) + } +} 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 13d97aaf28da..92caa89bb0e8 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 @@ -18,19 +18,30 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.util.Log import com.android.internal.widget.LockPatternUtils import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +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.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.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @SysUISingleton @@ -43,7 +54,12 @@ constructor( private val keyguardStateController: KeyguardStateController, private val userTracker: UserTracker, private val activityStarter: ActivityStarter, + private val featureFlags: FeatureFlags, + private val repository: Lazy<KeyguardQuickAffordanceRepository>, ) { + private val isUsingRepository: Boolean + get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** Returns an observable for the quick affordance at the given position. */ fun quickAffordance( position: KeyguardQuickAffordancePosition @@ -72,7 +88,19 @@ constructor( configKey: String, expandable: Expandable?, ) { - @Suppress("UNCHECKED_CAST") val config = registry.get(configKey) + @Suppress("UNCHECKED_CAST") + val config = + if (isUsingRepository) { + val (slotId, decodedConfigKey) = configKey.decode() + repository.get().selections.value[slotId]?.find { it.key == decodedConfigKey } + } else { + registry.get(configKey) + } + if (config == null) { + Log.e(TAG, "Affordance config with key of \"$configKey\" not found!") + return + } + when (val result = config.onTriggered(expandable)) { is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> launchQuickAffordance( @@ -84,28 +112,138 @@ constructor( } } + /** + * Selects an affordance with the given ID on the slot with the given ID. + * + * @return `true` if the affordance was selected successfully; `false` otherwise. + */ + suspend fun select(slotId: String, affordanceId: String): Boolean { + check(isUsingRepository) + + val slots = repository.get().getSlotPickerRepresentations() + val slot = slots.find { it.id == slotId } ?: return false + val selections = + repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + val alreadySelected = selections.remove(affordanceId) + if (!alreadySelected) { + while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) { + selections.removeAt(0) + } + } + + selections.add(affordanceId) + + repository + .get() + .setSelections( + slotId = slotId, + affordanceIds = selections, + ) + + return true + } + + /** + * Unselects one or all affordances from the slot with the given ID. + * + * @param slotId The ID of the slot. + * @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances + * from the slot. + * @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 { + check(isUsingRepository) + + val slots = repository.get().getSlotPickerRepresentations() + if (slots.find { it.id == slotId } == null) { + return false + } + + if (affordanceId.isNullOrEmpty()) { + return if ( + repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty() + ) { + false + } else { + repository.get().setSelections(slotId = slotId, affordanceIds = emptyList()) + true + } + } + + val selections = + repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + return if (selections.remove(affordanceId)) { + repository + .get() + .setSelections( + slotId = slotId, + affordanceIds = selections, + ) + true + } else { + false + } + } + + /** Returns affordance IDs indexed by slot ID, for all known slots. */ + suspend fun getSelections(): Map<String, List<String>> { + check(isUsingRepository) + + val selections = repository.get().getSelections() + return repository.get().getSlotPickerRepresentations().associate { slotRepresentation -> + slotRepresentation.id to (selections[slotRepresentation.id] ?: emptyList()) + } + } + private fun quickAffordanceInternal( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceModel> { - val configs = registry.getAll(position) + return if (isUsingRepository) { + repository + .get() + .selections + .map { it[position.toSlotId()] ?: emptyList() } + .flatMapLatest { configs -> combinedConfigs(position, configs) } + } else { + combinedConfigs(position, registry.getAll(position)) + } + } + + private fun combinedConfigs( + position: KeyguardQuickAffordancePosition, + configs: List<KeyguardQuickAffordanceConfig>, + ): Flow<KeyguardQuickAffordanceModel> { + if (configs.isEmpty()) { + return flowOf(KeyguardQuickAffordanceModel.Hidden) + } + return combine( configs.map { config -> - // We emit an initial "Hidden" value to make sure that there's always an initial - // value and avoid subtle bugs where the downstream isn't receiving any values - // because one config implementation is not emitting an initial value. For example, - // see b/244296596. + // We emit an initial "Hidden" value to make sure that there's always an + // initial value and avoid subtle bugs where the downstream isn't receiving + // any values because one config implementation is not emitting an initial + // value. For example, see b/244296596. config.lockScreenState.onStart { emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) } } ) { states -> val index = - states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible } + states.indexOfFirst { state -> + state is KeyguardQuickAffordanceConfig.LockScreenState.Visible + } if (index != -1) { val visibleState = states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible + val configKey = configs[index].key KeyguardQuickAffordanceModel.Visible( - configKey = configs[index].key, + configKey = + if (isUsingRepository) { + configKey.encode(position.toSlotId()) + } else { + configKey + }, icon = visibleState.icon, activationState = visibleState.activationState, ) @@ -145,4 +283,39 @@ constructor( ) } } + + private fun KeyguardQuickAffordancePosition.toSlotId(): String { + return when (this) { + KeyguardQuickAffordancePosition.BOTTOM_START -> + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + KeyguardQuickAffordancePosition.BOTTOM_END -> + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + } + } + + private fun String.encode(slotId: String): String { + return "$slotId$DELIMITER$this" + } + + private fun String.decode(): Pair<String, String> { + val splitUp = this.split(DELIMITER) + return Pair(splitUp[0], splitUp[1]) + } + + fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> { + check(isUsingRepository) + + return repository.get().getAffordancePickerRepresentations() + } + + fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { + check(isUsingRepository) + + return repository.get().getSlotPickerRepresentations() + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceInteractor" + private const val DELIMITER = "::" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt new file mode 100644 index 000000000000..a56bc900f936 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt @@ -0,0 +1,30 @@ +/* + * 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 + +import androidx.annotation.DrawableRes + +/** + * Representation of a quick affordance for use to build "picker", "selector", or "settings" + * experiences. + */ +data class KeyguardQuickAffordancePickerRepresentation( + val id: String, + val name: String, + @DrawableRes val iconResourceId: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt new file mode 100644 index 000000000000..86f27567ddf8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt @@ -0,0 +1,28 @@ +/* + * 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 + +/** + * Representation of a quick affordance slot (or position) for use to build "picker", "selector", or + * "settings" experiences. + */ +data class KeyguardSlotPickerRepresentation( + val id: String, + /** The maximum number of selected affordances that can be present on this slot. */ + val maxSelectedAffordances: Int = 1, +) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index f18acbad14f2..0fb181d41484 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -23,14 +23,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.yield -/** - * Fake implementation of a quick affordance data source. - * - * This class is abstract to force tests to provide extensions of it as the system that references - * these configs uses each implementation's class type to refer to them. - */ -abstract class FakeKeyguardQuickAffordanceConfig( +/** Fake implementation of a quick affordance data source. */ +class FakeKeyguardQuickAffordanceConfig( override val key: String, + override val pickerName: String = key, + override val pickerIconResourceId: Int = 0, ) : KeyguardQuickAffordanceConfig { var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled 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 new file mode 100644 index 000000000000..d2422ad7b53b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -0,0 +1,127 @@ +/* + * 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 androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +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 org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + + @Before + fun setUp() { + underTest = KeyguardQuickAffordanceSelectionManager() + } + + @Test + fun setSelections() = + runBlocking(IMMEDIATE) { + var affordanceIdsBySlotId: Map<String, List<String>>? = 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" + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1), + ), + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(affordanceId2), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1, affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(), + ) + ) + + job.cancel() + } + + private suspend fun assertSelections( + observed: Map<String, List<String>>?, + expected: Map<String, List<String>>, + ) { + 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/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 61a3f9f07600..2bd8e9aabab3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -50,7 +50,7 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(controller.intent).thenReturn(INTENT_1) - underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller) + underTest = QrCodeScannerKeyguardQuickAffordanceConfig(mock(), controller) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index c05beef6d624..5178154bdeee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -59,6 +59,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { underTest = QuickAccessWalletKeyguardQuickAffordanceConfig( + mock(), walletController, activityStarter, ) 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 new file mode 100644 index 000000000000..5a7f2bb5cb37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -0,0 +1,152 @@ +/* + * 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.repository + +import androidx.test.filters.SmallTest +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.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation +import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardQuickAffordanceRepository + + private lateinit var config1: FakeKeyguardQuickAffordanceConfig + private lateinit var config2: FakeKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") + config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + underTest = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(config1, config2), + ) + } + + @Test + fun setSelections() = + runBlocking(IMMEDIATE) { + var configsBySlotId: Map<String, List<KeyguardQuickAffordanceConfig>>? = null + val job = underTest.selections.onEach { configsBySlotId = it }.launchIn(this) + val slotId1 = "slot1" + val slotId2 = "slot2" + + underTest.setSelections(slotId1, listOf(config1.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to listOf(config1), + ), + ) + + underTest.setSelections(slotId2, listOf(config2.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to listOf(config1), + slotId2 to listOf(config2), + ), + ) + + underTest.setSelections(slotId1, emptyList()) + underTest.setSelections(slotId2, listOf(config1.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to emptyList(), + slotId2 to listOf(config1), + ), + ) + + job.cancel() + } + + @Test + fun getAffordancePickerRepresentations() { + assertThat(underTest.getAffordancePickerRepresentations()) + .isEqualTo( + listOf( + KeyguardQuickAffordancePickerRepresentation( + id = config1.key, + name = config1.pickerName, + iconResourceId = config1.pickerIconResourceId, + ), + KeyguardQuickAffordancePickerRepresentation( + id = config2.key, + name = config2.pickerName, + iconResourceId = config2.pickerIconResourceId, + ), + ) + ) + } + + @Test + fun getSlotPickerRepresentations() { + assertThat(underTest.getSlotPickerRepresentations()) + .isEqualTo( + listOf( + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + maxSelectedAffordances = 1, + ), + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + maxSelectedAffordances = 1, + ), + ) + ) + } + + private suspend fun assertSelections( + observed: Map<String, List<KeyguardQuickAffordanceConfig>>?, + expected: Map<String, List<KeyguardQuickAffordanceConfig>>, + ) { + assertThat(observed).isEqualTo(expected) + assertThat(underTest.getSelections()) + .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } }) + expected.forEach { (slotId, configs) -> + assertThat(underTest.getSelections(slotId)).isEqualTo(configs) + } + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} 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 7116cc101d3f..8b6603d79244 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 @@ -25,10 +25,14 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +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 @@ -37,6 +41,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test @@ -189,6 +195,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { /* startActivity= */ true, ), ) + + private val IMMEDIATE = Dispatchers.Main.immediate } @Mock private lateinit var lockPatternUtils: LockPatternUtils @@ -213,10 +221,20 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { whenever(expandable.activityLaunchController()).thenReturn(animationController) homeControls = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + val quickAccessWallet = + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) + val qrCodeScanner = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), + ) underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), @@ -229,14 +247,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ), KeyguardQuickAffordancePosition.BOTTOM_END to listOf( - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {}, - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {}, + quickAccessWallet, + qrCodeScanner, ), ), ), @@ -244,6 +256,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + }, + repository = { quickAffordanceRepository }, ) } 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 ae32ba6676be..33645354d9f3 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 @@ -22,22 +22,31 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +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.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry 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.UserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.yield import org.junit.Before @@ -47,6 +56,7 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @@ -62,6 +72,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig + private lateinit var featureFlags: FakeFeatureFlags @Before fun setUp() { @@ -71,20 +82,25 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { repository.setKeyguardShowing(true) homeControls = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) quickAccessWallet = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {} + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) qrCodeScanner = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), + ) + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + } underTest = KeyguardQuickAffordanceInteractor( @@ -107,6 +123,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = featureFlags, + repository = { quickAffordanceRepository }, ) } @@ -210,6 +228,270 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun select() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + var startConfig: KeyguardQuickAffordanceModel? = null + val job1 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { startConfig = it } + .launchIn(this) + var endConfig: KeyguardQuickAffordanceModel? = null + val job2 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { endConfig = it } + .launchIn(this) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${homeControls.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(homeControls.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + underTest.select( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + quickAccessWallet.key + ) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(quickAccessWallet.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + + "::${qrCodeScanner.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(quickAccessWallet.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(qrCodeScanner.key), + ) + ) + + job1.cancel() + job2.cancel() + } + + @Test + fun `unselect - one`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + var startConfig: KeyguardQuickAffordanceModel? = null + val job1 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { startConfig = it } + .launchIn(this) + var endConfig: KeyguardQuickAffordanceModel? = null + val job2 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { endConfig = it } + .launchIn(this) + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) + yield() + yield() + + underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(quickAccessWallet.key), + ) + ) + + underTest.unselect( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + quickAccessWallet.key + ) + yield() + yield() + + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + job1.cancel() + job2.cancel() + } + + @Test + fun `unselect - all`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) + yield() + yield() + + underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null) + yield() + yield() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(quickAccessWallet.key), + ) + ) + + underTest.unselect( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + null, + ) + yield() + yield() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + } + companion object { private val ICON: Icon = mock { whenever(this.contentDescription) @@ -220,5 +502,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 + private val IMMEDIATE = Dispatchers.Main.immediate } } 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 f73d1ecf9373..78148c4d3d1b 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 @@ -23,10 +23,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +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.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor @@ -41,6 +45,8 @@ import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runBlockingTest @@ -82,20 +88,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { .thenReturn(RETURNED_BURN_IN_OFFSET) homeControlsQuickAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) quickAccessWalletAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {} + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) qrCodeScannerAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) registry = FakeKeyguardQuickAffordanceRegistry( mapOf( @@ -116,6 +115,18 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = + setOf( + homeControlsQuickAffordanceConfig, + quickAccessWalletAffordanceConfig, + qrCodeScannerAffordanceConfig, + ), + ) underTest = KeyguardBottomAreaViewModel( keyguardInteractor = keyguardInteractor, @@ -127,6 +138,11 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + }, + repository = { quickAffordanceRepository }, ), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), burnInHelperWrapper = burnInHelperWrapper, @@ -576,5 +592,6 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { companion object { private const val DEFAULT_BURN_IN_OFFSET = 5 private const val RETURNED_BURN_IN_OFFSET = 3 + private val IMMEDIATE = Dispatchers.Main.immediate } } |