Merge "[conflict] Merge changes Ie810d37d,I6947e1f7 into tm-qpr-dev am: 33b100711a am: 9e708c9958"
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 0000000..2dc7a28
--- /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 2a94e52..e6e0e52 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -129,6 +129,14 @@
@JvmField val NEW_UDFPS_OVERLAY = UnreleasedFlag(215)
+ /**
+ * 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(216, 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 56a1f1a..f08463b 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.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 @@
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 c600e13..d6f521c 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 @@
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 0000000..bea9363
--- /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 0a8090b..fd40d1d 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 @@
/** 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 0000000..9c9354f
--- /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 d620b2a..11f72ff 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.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 @@
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 be57a32..303e6a1 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.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 @@
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 0000000..95f614f
--- /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 13d97aa..92caa89 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 @@
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 @@
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 @@
}
}
+ /**
+ * 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 @@
)
}
}
+
+ 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 0000000..a56bc90
--- /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 0000000..86f2756
--- /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 f18acba..0fb181d 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.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 0000000..d2422ad
--- /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 61a3f9f..2bd8e9a 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 @@
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 c05beef..5178154 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 @@
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 0000000..5a7f2bb
--- /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 7116cc1..8b6603d 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.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.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 @@
/* startActivity= */ true,
),
)
+
+ private val IMMEDIATE = Dispatchers.Main.immediate
}
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -213,10 +221,20 @@
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 @@
),
KeyguardQuickAffordancePosition.BOTTOM_END to
listOf(
- object :
- FakeKeyguardQuickAffordanceConfig(
- BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
- ) {},
- object :
- FakeKeyguardQuickAffordanceConfig(
- BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
- ) {},
+ quickAccessWallet,
+ qrCodeScanner,
),
),
),
@@ -244,6 +256,11 @@
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 ae32ba6..3364535 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.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.mockito.Mock
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@@ -62,6 +72,7 @@
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 @@
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 @@
keyguardStateController = keyguardStateController,
userTracker = userTracker,
activityStarter = activityStarter,
+ featureFlags = featureFlags,
+ repository = { quickAffordanceRepository },
)
}
@@ -210,6 +228,270 @@
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 @@
)
}
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 f73d1ec..78148c4 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.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.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 @@
.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 @@
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 @@
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 @@
companion object {
private const val DEFAULT_BURN_IN_OFFSET = 5
private const val RETURNED_BURN_IN_OFFSET = 3
+ private val IMMEDIATE = Dispatchers.Main.immediate
}
}