Merge changes Ie810d37d,I6947e1f7 into tm-qpr-dev

* changes:
  Quick affordance interactor uses repository.
  Quick affordance repository.
This commit is contained in:
Ale Nijamkin
2022-11-03 11:52:07 +00:00
committed by Android (Google) Code Review
21 changed files with 1165 additions and 52 deletions

View File

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

View File

@@ -125,6 +125,14 @@ object Flags {
// TODO(b/255607168): Tracking Bug // TODO(b/255607168): Tracking Bug
@JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213) @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 // 300 - power menu
// TODO(b/254512600): Tracking Bug // TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = ReleasedFlag(300) @JvmField val POWER_MENU_LITE = ReleasedFlag(300)

View File

@@ -42,6 +42,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator; 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.data.repository.KeyguardRepositoryModule;
import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
@@ -71,6 +72,7 @@ import dagger.Provides;
KeyguardUserSwitcherComponent.class}, KeyguardUserSwitcherComponent.class},
includes = { includes = {
FalsingModule.class, FalsingModule.class,
KeyguardDataQuickAffordanceModule.class,
KeyguardQuickAffordanceModule.class, KeyguardQuickAffordanceModule.class,
KeyguardRepositoryModule.class, KeyguardRepositoryModule.class,
StartKeyguardTransitionModule.class, StartKeyguardTransitionModule.class,

View File

@@ -53,6 +53,10 @@ constructor(
override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS 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> = override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked -> component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
if (canShowWhileLocked) { if (canShowWhileLocked) {

View File

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

View File

@@ -29,6 +29,10 @@ interface KeyguardQuickAffordanceConfig {
/** Unique identifier for this quick affordance. It must be globally unique. */ /** Unique identifier for this quick affordance. It must be globally unique. */
val key: String val key: String
val pickerName: String
val pickerIconResourceId: Int
/** /**
* The ever-changing state of the affordance. * The ever-changing state of the affordance.
* *

View File

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

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.animation.Expandable import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 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.ContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@@ -34,11 +36,16 @@ import kotlinx.coroutines.flow.Flow
class QrCodeScannerKeyguardQuickAffordanceConfig class QrCodeScannerKeyguardQuickAffordanceConfig
@Inject @Inject
constructor( constructor(
@Application context: Context,
private val controller: QRCodeScannerController, private val controller: QRCodeScannerController,
) : KeyguardQuickAffordanceConfig { ) : KeyguardQuickAffordanceConfig {
override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER 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> = override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
conflatedCallbackFlow { conflatedCallbackFlow {
val callback = val callback =

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.service.quickaccesswallet.GetWalletCardsError import android.service.quickaccesswallet.GetWalletCardsError
import android.service.quickaccesswallet.GetWalletCardsResponse 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.ContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.wallet.controller.QuickAccessWalletController import com.android.systemui.wallet.controller.QuickAccessWalletController
import javax.inject.Inject import javax.inject.Inject
@@ -40,12 +42,17 @@ import kotlinx.coroutines.flow.Flow
class QuickAccessWalletKeyguardQuickAffordanceConfig class QuickAccessWalletKeyguardQuickAffordanceConfig
@Inject @Inject
constructor( constructor(
@Application context: Context,
private val walletController: QuickAccessWalletController, private val walletController: QuickAccessWalletController,
private val activityStarter: ActivityStarter, private val activityStarter: ActivityStarter,
) : KeyguardQuickAffordanceConfig { ) : KeyguardQuickAffordanceConfig {
override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET 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> = override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
conflatedCallbackFlow { conflatedCallbackFlow {
val callback = val callback =

View File

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

View File

@@ -18,19 +18,30 @@
package com.android.systemui.keyguard.domain.interactor package com.android.systemui.keyguard.domain.interactor
import android.content.Intent import android.content.Intent
import android.util.Log
import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils
import com.android.systemui.animation.Expandable import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton 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.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
import dagger.Lazy
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine 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 import kotlinx.coroutines.flow.onStart
@SysUISingleton @SysUISingleton
@@ -43,7 +54,12 @@ constructor(
private val keyguardStateController: KeyguardStateController, private val keyguardStateController: KeyguardStateController,
private val userTracker: UserTracker, private val userTracker: UserTracker,
private val activityStarter: ActivityStarter, 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. */ /** Returns an observable for the quick affordance at the given position. */
fun quickAffordance( fun quickAffordance(
position: KeyguardQuickAffordancePosition position: KeyguardQuickAffordancePosition
@@ -72,7 +88,19 @@ constructor(
configKey: String, configKey: String,
expandable: Expandable?, 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)) { when (val result = config.onTriggered(expandable)) {
is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity ->
launchQuickAffordance( 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( private fun quickAffordanceInternal(
position: KeyguardQuickAffordancePosition position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceModel> { ): 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( return combine(
configs.map { config -> configs.map { config ->
// We emit an initial "Hidden" value to make sure that there's always an initial // We emit an initial "Hidden" value to make sure that there's always an
// value and avoid subtle bugs where the downstream isn't receiving any values // initial value and avoid subtle bugs where the downstream isn't receiving
// because one config implementation is not emitting an initial value. For example, // any values because one config implementation is not emitting an initial
// see b/244296596. // value. For example, see b/244296596.
config.lockScreenState.onStart { config.lockScreenState.onStart {
emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
} }
} }
) { states -> ) { states ->
val index = val index =
states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible } states.indexOfFirst { state ->
state is KeyguardQuickAffordanceConfig.LockScreenState.Visible
}
if (index != -1) { if (index != -1) {
val visibleState = val visibleState =
states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible
val configKey = configs[index].key
KeyguardQuickAffordanceModel.Visible( KeyguardQuickAffordanceModel.Visible(
configKey = configs[index].key, configKey =
if (isUsingRepository) {
configKey.encode(position.toSlotId())
} else {
configKey
},
icon = visibleState.icon, icon = visibleState.icon,
activationState = visibleState.activationState, 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 = "::"
}
} }

View File

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

View File

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

View File

@@ -23,14 +23,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
/** /** Fake implementation of a quick affordance data source. */
* Fake implementation of a quick affordance data source. class FakeKeyguardQuickAffordanceConfig(
*
* 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(
override val key: String, override val key: String,
override val pickerName: String = key,
override val pickerIconResourceId: Int = 0,
) : KeyguardQuickAffordanceConfig { ) : KeyguardQuickAffordanceConfig {
var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled

View File

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

View File

@@ -50,7 +50,7 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(controller.intent).thenReturn(INTENT_1) whenever(controller.intent).thenReturn(INTENT_1)
underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller) underTest = QrCodeScannerKeyguardQuickAffordanceConfig(mock(), controller)
} }
@Test @Test

View File

@@ -59,6 +59,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
underTest = underTest =
QuickAccessWalletKeyguardQuickAffordanceConfig( QuickAccessWalletKeyguardQuickAffordanceConfig(
mock(),
walletController, walletController,
activityStarter, activityStarter,
) )

View File

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

View File

@@ -25,10 +25,14 @@ import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon 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.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
@@ -37,6 +41,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -189,6 +195,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
/* startActivity= */ true, /* startActivity= */ true,
), ),
) )
private val IMMEDIATE = Dispatchers.Main.immediate
} }
@Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -213,10 +221,20 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
whenever(expandable.activityLaunchController()).thenReturn(animationController) whenever(expandable.activityLaunchController()).thenReturn(animationController)
homeControls = homeControls =
object : FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
FakeKeyguardQuickAffordanceConfig( val quickAccessWallet =
BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS 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 = underTest =
KeyguardQuickAffordanceInteractor( KeyguardQuickAffordanceInteractor(
keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
@@ -229,14 +247,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
), ),
KeyguardQuickAffordancePosition.BOTTOM_END to KeyguardQuickAffordancePosition.BOTTOM_END to
listOf( listOf(
object : quickAccessWallet,
FakeKeyguardQuickAffordanceConfig( qrCodeScanner,
BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
) {},
object :
FakeKeyguardQuickAffordanceConfig(
BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
) {},
), ),
), ),
), ),
@@ -244,6 +256,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
keyguardStateController = keyguardStateController, keyguardStateController = keyguardStateController,
userTracker = userTracker, userTracker = userTracker,
activityStarter = activityStarter, activityStarter = activityStarter,
featureFlags =
FakeFeatureFlags().apply {
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
},
repository = { quickAffordanceRepository },
) )
} }

View File

@@ -22,22 +22,31 @@ import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon 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.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker 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.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.junit.Before import org.junit.Before
@@ -47,6 +56,7 @@ import org.junit.runners.JUnit4
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@@ -62,6 +72,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig
private lateinit var featureFlags: FakeFeatureFlags
@Before @Before
fun setUp() { fun setUp() {
@@ -71,20 +82,25 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
repository.setKeyguardShowing(true) repository.setKeyguardShowing(true)
homeControls = homeControls =
object : FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
FakeKeyguardQuickAffordanceConfig(
BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
) {}
quickAccessWallet = quickAccessWallet =
object : FakeKeyguardQuickAffordanceConfig(
FakeKeyguardQuickAffordanceConfig( BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET )
) {}
qrCodeScanner = 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 = underTest =
KeyguardQuickAffordanceInteractor( KeyguardQuickAffordanceInteractor(
@@ -107,6 +123,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
keyguardStateController = keyguardStateController, keyguardStateController = keyguardStateController,
userTracker = userTracker, userTracker = userTracker,
activityStarter = activityStarter, activityStarter = activityStarter,
featureFlags = featureFlags,
repository = { quickAffordanceRepository },
) )
} }
@@ -210,6 +228,270 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
job.cancel() 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 { companion object {
private val ICON: Icon = mock { private val ICON: Icon = mock {
whenever(this.contentDescription) whenever(this.contentDescription)
@@ -220,5 +502,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
) )
} }
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
private val IMMEDIATE = Dispatchers.Main.immediate
} }
} }

View File

@@ -23,10 +23,14 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.doze.util.BurnInHelperWrapper 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.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
@@ -41,6 +45,8 @@ import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
@@ -82,20 +88,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
.thenReturn(RETURNED_BURN_IN_OFFSET) .thenReturn(RETURNED_BURN_IN_OFFSET)
homeControlsQuickAffordanceConfig = homeControlsQuickAffordanceConfig =
object : FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
FakeKeyguardQuickAffordanceConfig(
BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
) {}
quickAccessWalletAffordanceConfig = quickAccessWalletAffordanceConfig =
object : FakeKeyguardQuickAffordanceConfig(
FakeKeyguardQuickAffordanceConfig( BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET )
) {}
qrCodeScannerAffordanceConfig = qrCodeScannerAffordanceConfig =
object : FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
FakeKeyguardQuickAffordanceConfig(
BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
) {}
registry = registry =
FakeKeyguardQuickAffordanceRegistry( FakeKeyguardQuickAffordanceRegistry(
mapOf( mapOf(
@@ -116,6 +115,18 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
whenever(userTracker.userHandle).thenReturn(mock()) whenever(userTracker.userHandle).thenReturn(mock())
whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(
scope = CoroutineScope(IMMEDIATE),
backgroundDispatcher = IMMEDIATE,
selectionManager = KeyguardQuickAffordanceSelectionManager(),
configs =
setOf(
homeControlsQuickAffordanceConfig,
quickAccessWalletAffordanceConfig,
qrCodeScannerAffordanceConfig,
),
)
underTest = underTest =
KeyguardBottomAreaViewModel( KeyguardBottomAreaViewModel(
keyguardInteractor = keyguardInteractor, keyguardInteractor = keyguardInteractor,
@@ -127,6 +138,11 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
keyguardStateController = keyguardStateController, keyguardStateController = keyguardStateController,
userTracker = userTracker, userTracker = userTracker,
activityStarter = activityStarter, activityStarter = activityStarter,
featureFlags =
FakeFeatureFlags().apply {
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
},
repository = { quickAffordanceRepository },
), ),
bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
burnInHelperWrapper = burnInHelperWrapper, burnInHelperWrapper = burnInHelperWrapper,
@@ -576,5 +592,6 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
companion object { companion object {
private const val DEFAULT_BURN_IN_OFFSET = 5 private const val DEFAULT_BURN_IN_OFFSET = 5
private const val RETURNED_BURN_IN_OFFSET = 3 private const val RETURNED_BURN_IN_OFFSET = 3
private val IMMEDIATE = Dispatchers.Main.immediate
} }
} }