Support for unavailable and disabled affordances. am: af8fd1b3c5

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20441246

Change-Id: I6282dd626c1d84815aabf567ee144330954b417c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Alejandro Nijamkin
2022-11-21 19:20:25 +00:00
committed by Automerger Merge Worker
18 changed files with 467 additions and 69 deletions

View File

@@ -2679,4 +2679,50 @@
<!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] -->
<string name="dream_time_complication_24_hr_time_format">kk:mm</string>
<!--
Template for an action that opens a specific app. [CHAR LIMIT=16]
-->
<string name="keyguard_affordance_enablement_dialog_action_template">Open <xliff:g id="appName" example="Wallet">%1$s</xliff:g></string>
<!--
Template for a message shown right before a list of instructions that tell the user what to do
in order to enable a shortcut to a specific app. [CHAR LIMIT=NONE]
-->
<string name="keyguard_affordance_enablement_dialog_message">To add the <xliff:g id="appName" example="Wallet">%1$s</xliff:g> app as a shortcut, make sure</string>
<!--
Requirement for the wallet app to be available for the user to use. This is shown as part of a
bulleted list of requirements. When all requirements are met, the app can be accessed through a
shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_wallet_instruction_1">&#8226; The app is set up</string>
<!--
Requirement for the wallet app to be available for the user to use. This is shown as part of a
bulleted list of requirements. When all requirements are met, the app can be accessed through a
shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_wallet_instruction_2">&#8226; At least one card has been added to Wallet</string>
<!--
Requirement for the QR code scanner functionality to be available for the user to use. This is
shown as part of a bulleted list of requirements. When all requirements are met, the piece of
functionality can be accessed through a shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_qr_scanner_instruction">&#8226; Install a camera app</string>
<!--
Requirement for the home app to be available for the user to use. This is shown as part of a
bulleted list of requirements. When all requirements are met, the app can be accessed through a
shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_home_instruction_1">&#8226; The app is set up</string>
<!--
Requirement for the home app to be available for the user to use. This is shown as part of a
bulleted list of requirements. When all requirements are met, the app can be accessed through a
shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; At least one device is available</string>
</resources>

View File

@@ -67,6 +67,8 @@ object KeyguardQuickAffordanceProviderContract {
object AffordanceTable {
const val TABLE_NAME = "affordances"
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
const val ENABLEMENT_INSTRUCTIONS_DELIMITER = "]["
const val COMPONENT_NAME_SEPARATOR = "/"
object Columns {
/** String. Unique ID for this affordance. */
@@ -78,6 +80,25 @@ object KeyguardQuickAffordanceProviderContract {
* ID from the system UI package.
*/
const val ICON = "icon"
/** Integer. `1` if the affordance is enabled or `0` if it disabled. */
const val IS_ENABLED = "is_enabled"
/**
* String. List of strings, delimited by [ENABLEMENT_INSTRUCTIONS_DELIMITER] to be shown
* to the user if the affordance is disabled and the user selects the affordance. The
* first one is a title while the rest are the steps needed to re-enable the affordance.
*/
const val ENABLEMENT_INSTRUCTIONS = "enablement_instructions"
/**
* String. Optional label for a button that, when clicked, opens a destination activity
* where the user can re-enable the disabled affordance.
*/
const val ENABLEMENT_ACTION_TEXT = "enablement_action_text"
/**
* String. Optional package name and activity action string, delimited by
* [COMPONENT_NAME_SEPARATOR] to use with an `Intent` to start an activity that opens a
* destination where the user can re-enable the disabled affordance.
*/
const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent"
}
}

View File

@@ -31,6 +31,7 @@ import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCall
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
class KeyguardQuickAffordanceProvider :
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
@@ -118,9 +119,9 @@ class KeyguardQuickAffordanceProvider :
sortOrder: String?,
): Cursor? {
return when (uriMatcher.match(uri)) {
MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
MATCH_CODE_ALL_AFFORDANCES -> runBlocking { queryAffordances() }
MATCH_CODE_ALL_SLOTS -> querySlots()
MATCH_CODE_ALL_SELECTIONS -> querySelections()
MATCH_CODE_ALL_SELECTIONS -> runBlocking { querySelections() }
MATCH_CODE_ALL_FLAGS -> queryFlags()
else -> null
}
@@ -194,7 +195,7 @@ class KeyguardQuickAffordanceProvider :
}
}
private fun querySelections(): Cursor {
private suspend fun querySelections(): Cursor {
return MatrixCursor(
arrayOf(
Contract.SelectionTable.Columns.SLOT_ID,
@@ -219,12 +220,16 @@ class KeyguardQuickAffordanceProvider :
}
}
private fun queryAffordances(): Cursor {
private suspend fun queryAffordances(): Cursor {
return MatrixCursor(
arrayOf(
Contract.AffordanceTable.Columns.ID,
Contract.AffordanceTable.Columns.NAME,
Contract.AffordanceTable.Columns.ICON,
Contract.AffordanceTable.Columns.IS_ENABLED,
Contract.AffordanceTable.Columns.ENABLEMENT_INSTRUCTIONS,
Contract.AffordanceTable.Columns.ENABLEMENT_ACTION_TEXT,
Contract.AffordanceTable.Columns.ENABLEMENT_COMPONENT_NAME,
)
)
.apply {
@@ -234,6 +239,12 @@ class KeyguardQuickAffordanceProvider :
representation.id,
representation.name,
representation.iconResourceId,
if (representation.isEnabled) 1 else 0,
representation.instructions?.joinToString(
Contract.AffordanceTable.ENABLEMENT_INSTRUCTIONS_DELIMITER
),
representation.actionText,
representation.actionComponentName,
)
)
}

View File

@@ -20,6 +20,7 @@ package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import android.content.Intent
import androidx.annotation.DrawableRes
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -45,7 +46,7 @@ import kotlinx.coroutines.flow.flowOf
class HomeControlsKeyguardQuickAffordanceConfig
@Inject
constructor(
@Application context: Context,
@Application private val context: Context,
private val component: ControlsComponent,
) : KeyguardQuickAffordanceConfig {
@@ -66,6 +67,36 @@ constructor(
}
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
if (!component.isEnabled()) {
return KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
val currentServices =
component.getControlsListingController().getOrNull()?.getCurrentServices()
val hasFavorites =
component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
if (currentServices.isNullOrEmpty() || !hasFavorites) {
return KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
instructions =
listOf(
context.getString(
R.string.keyguard_affordance_enablement_dialog_message,
pickerName,
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_1
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_home_instruction_2
),
),
)
}
return KeyguardQuickAffordanceConfig.PickerScreenState.Default
}
override fun onTriggered(
expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnTriggeredResult {

View File

@@ -21,6 +21,7 @@ import android.content.Intent
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */
@@ -40,6 +41,12 @@ interface KeyguardQuickAffordanceConfig {
*/
val lockScreenState: Flow<LockScreenState>
/**
* Returns the [PickerScreenState] representing the affordance in the settings or selector
* experience.
*/
suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default
/**
* Notifies that the affordance was clicked by the user.
*
@@ -48,6 +55,58 @@ interface KeyguardQuickAffordanceConfig {
*/
fun onTriggered(expandable: Expandable?): OnTriggeredResult
/**
* Encapsulates the state of a quick affordance within the context of the settings or selector
* experience.
*/
sealed class PickerScreenState {
/** The picker shows the item for selecting this affordance as it normally would. */
object Default : PickerScreenState()
/**
* The picker does not show an item for selecting this affordance as it is not supported on
* the device at all. For example, missing hardware requirements.
*/
object UnavailableOnDevice : PickerScreenState()
/**
* The picker shows the item for selecting this affordance as disabled. Clicking on it will
* show the given instructions to the user. If [actionText] and [actionComponentName] are
* provided (optional) a button will be shown to open an activity to help the user complete
* the steps described in the instructions.
*/
data class Disabled(
/** List of human-readable instructions for setting up the quick affordance. */
val instructions: List<String>,
/**
* Optional text to display on a button that the user can click to start a flow to go
* and set up the quick affordance and make it enabled.
*/
val actionText: String? = null,
/**
* Optional component name to be able to build an `Intent` that opens an `Activity` for
* the user to be able to set up the quick affordance and make it enabled.
*
* This is either just an action for the `Intent` or a package name and action,
* separated by [Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR] for convenience, you
* can use the [componentName] function.
*/
val actionComponentName: String? = null,
) : PickerScreenState() {
init {
check(instructions.isNotEmpty()) { "Instructions must not be empty!" }
check(
(actionText.isNullOrEmpty() && actionComponentName.isNullOrEmpty()) ||
(!actionText.isNullOrEmpty() && !actionComponentName.isNullOrEmpty())
) {
"actionText and actionComponentName must either both be null/empty or both be" +
" non-empty!"
}
}
}
}
/**
* Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
* button on the lock-screen).
@@ -83,4 +142,18 @@ interface KeyguardQuickAffordanceConfig {
val canShowWhileLocked: Boolean,
) : OnTriggeredResult()
}
companion object {
fun componentName(
packageName: String? = null,
action: String?,
): String? {
return when {
action.isNullOrEmpty() -> null
!packageName.isNullOrEmpty() ->
"$packageName${Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR}$action"
else -> action
}
}
}
}

View File

@@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.Flow
class QrCodeScannerKeyguardQuickAffordanceConfig
@Inject
constructor(
@Application context: Context,
@Application private val context: Context,
private val controller: QRCodeScannerController,
) : KeyguardQuickAffordanceConfig {
@@ -75,6 +75,28 @@ constructor(
}
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
return when {
!controller.isAvailableOnDevice ->
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
!controller.isAbleToOpenCameraApp ->
KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
instructions =
listOf(
context.getString(
R.string.keyguard_affordance_enablement_dialog_message,
pickerName,
),
context.getString(
R.string
.keyguard_affordance_enablement_dialog_qr_scanner_instruction
),
),
)
else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
}
}
override fun onTriggered(
expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnTriggeredResult {

View File

@@ -18,10 +18,12 @@
package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.service.quickaccesswallet.GetWalletCardsError
import android.service.quickaccesswallet.GetWalletCardsResponse
import android.service.quickaccesswallet.QuickAccessWalletClient
import android.service.quickaccesswallet.WalletCard
import android.util.Log
import com.android.systemui.R
import com.android.systemui.animation.Expandable
@@ -31,25 +33,27 @@ 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.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.Companion.componentName
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.wallet.controller.QuickAccessWalletController
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.suspendCancellableCoroutine
/** Quick access wallet quick affordance data source. */
@SysUISingleton
class QuickAccessWalletKeyguardQuickAffordanceConfig
@Inject
constructor(
@Application context: Context,
@Application private val 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 pickerName: String = context.getString(R.string.accessibility_wallet_button)
override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen
@@ -58,10 +62,11 @@ constructor(
val callback =
object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
val hasCards = response?.walletCards?.isNotEmpty() == true
trySendWithFailureLogging(
state(
isFeatureEnabled = walletController.isWalletEnabled,
hasCard = response?.walletCards?.isNotEmpty() == true,
hasCard = hasCards,
tileIcon = walletController.walletClient.tileIcon,
),
TAG,
@@ -93,6 +98,44 @@ constructor(
}
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
return when {
!walletController.isWalletEnabled ->
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
walletController.walletClient.tileIcon == null || queryCards().isEmpty() -> {
val componentName =
walletController.walletClient.createWalletSettingsIntent().toComponentName()
val actionText =
if (componentName != null) {
context.getString(
R.string.keyguard_affordance_enablement_dialog_action_template,
pickerName,
)
} else {
null
}
KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
instructions =
listOf(
context.getString(
R.string.keyguard_affordance_enablement_dialog_message,
pickerName,
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_wallet_instruction_1
),
context.getString(
R.string.keyguard_affordance_enablement_dialog_wallet_instruction_2
),
),
actionText = actionText,
actionComponentName = componentName,
)
}
else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
}
}
override fun onTriggered(
expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
@@ -104,6 +147,24 @@ constructor(
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
private suspend fun queryCards(): List<WalletCard> {
return suspendCancellableCoroutine { continuation ->
val callback =
object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
continuation.resumeWith(
Result.success(response?.walletCards ?: emptyList())
)
}
override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
continuation.resumeWith(Result.success(emptyList()))
}
}
walletController.queryWalletCards(callback)
}
}
private fun state(
isFeatureEnabled: Boolean,
hasCard: Boolean,
@@ -125,6 +186,14 @@ constructor(
}
}
private fun Intent?.toComponentName(): String? {
if (this == null) {
return null
}
return componentName(packageName = `package`, action = action)
}
companion object {
private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
}

View File

@@ -121,18 +121,32 @@ constructor(
}
/**
* 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.
* Returns the list of representation objects for all known, device-available 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,
)
}
suspend fun getAffordancePickerRepresentations():
List<KeyguardQuickAffordancePickerRepresentation> {
return configs
.associateWith { config -> config.getPickerScreenState() }
.filterNot { (_, pickerState) ->
pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
.map { (config, pickerState) ->
val disabledPickerState =
pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
KeyguardQuickAffordancePickerRepresentation(
id = config.key,
name = config.pickerName,
iconResourceId = config.pickerIconResourceId,
isEnabled =
pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default,
instructions = disabledPickerState?.instructions,
actionText = disabledPickerState?.actionText,
actionComponentName = disabledPickerState?.actionComponentName,
)
}
}
/**

View File

@@ -189,7 +189,7 @@ constructor(
}
/** Returns affordance IDs indexed by slot ID, for all known slots. */
fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
check(isUsingRepository)
val slots = repository.get().getSlotPickerRepresentations()
@@ -310,7 +310,8 @@ constructor(
return Pair(splitUp[0], splitUp[1])
}
fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
suspend fun getAffordancePickerRepresentations():
List<KeyguardQuickAffordancePickerRepresentation> {
check(isUsingRepository)
return repository.get().getAffordancePickerRepresentations()

View File

@@ -27,4 +27,22 @@ data class KeyguardQuickAffordancePickerRepresentation(
val id: String,
val name: String,
@DrawableRes val iconResourceId: Int,
/** Whether this quick affordance is enabled. */
val isEnabled: Boolean = true,
/** If not enabled, the list of user-visible steps to re-enable it. */
val instructions: List<String>? = null,
/**
* If not enabled, an optional label for a button that takes the user to a destination where
* they can re-enable it.
*/
val actionText: String? = null,
/**
* If not enabled, an optional component name (package and action) for a button that takes the
* user to a destination where they can re-enable it.
*/
val actionComponentName: String? = null,
)

View File

@@ -158,14 +158,18 @@ public class QRCodeScannerController implements
* Returns true if lock screen entry point for QR Code Scanner is to be enabled.
*/
public boolean isEnabledForLockScreenButton() {
return mQRCodeScannerEnabled && mIntent != null && mConfigEnableLockScreenButton
&& isActivityCallable(mIntent);
return mQRCodeScannerEnabled && isAbleToOpenCameraApp() && isAvailableOnDevice();
}
/** Returns whether the feature is available on the device. */
public boolean isAvailableOnDevice() {
return mConfigEnableLockScreenButton;
}
/**
* Returns true if quick settings entry point for QR Code Scanner is to be enabled.
* Returns true if the feature can open a camera app on the device.
*/
public boolean isEnabledForQuickSettings() {
public boolean isAbleToOpenCameraApp() {
return mIntent != null && isActivityCallable(mIntent);
}

View File

@@ -115,7 +115,7 @@ public class QRCodeScannerTile extends QSTileImpl<QSTile.State> {
state.label = mContext.getString(R.string.qr_code_scanner_title);
state.contentDescription = state.label;
state.icon = ResourceIcon.get(R.drawable.ic_qr_code_scanner);
state.state = mQRCodeScannerController.isEnabledForQuickSettings() ? Tile.STATE_INACTIVE
state.state = mQRCodeScannerController.isAbleToOpenCameraApp() ? Tile.STATE_INACTIVE
: Tile.STATE_UNAVAILABLE;
}

View File

@@ -24,8 +24,9 @@ import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -40,7 +41,6 @@ import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@@ -93,6 +93,14 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
whenever(component.getControlsListingController())
.thenReturn(Optional.of(controlsListingController))
whenever(controlsListingController.getCurrentServices())
.thenReturn(
if (hasServiceInfos) {
listOf(mock(), mock())
} else {
emptyList()
}
)
whenever(component.canShowWhileLockedSetting)
.thenReturn(MutableStateFlow(canShowWhileLocked))
whenever(component.getVisibility())
@@ -144,6 +152,17 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes
KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java
}
)
assertThat(underTest.getPickerScreenState())
.isInstanceOf(
when {
!isFeatureEnabled ->
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice::class
.java
hasServiceInfos && hasFavorites ->
KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java
else -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java
}
)
job.cancel()
}
}

View File

@@ -24,17 +24,18 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@@ -134,6 +135,33 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
)
}
@Test
fun `getPickerScreenState - enabled if configured on device - can open camera`() = runTest {
whenever(controller.isAvailableOnDevice).thenReturn(true)
whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
assertThat(underTest.getPickerScreenState())
.isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
}
@Test
fun `getPickerScreenState - disabled if configured on device - cannot open camera`() = runTest {
whenever(controller.isAvailableOnDevice).thenReturn(true)
whenever(controller.isAbleToOpenCameraApp).thenReturn(false)
assertThat(underTest.getPickerScreenState())
.isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
}
@Test
fun `getPickerScreenState - unavailable if not configured on device`() = runTest {
whenever(controller.isAvailableOnDevice).thenReturn(false)
whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
assertThat(underTest.getPickerScreenState())
.isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
}
private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.LockScreenState?) {
assertThat(latest)
.isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)

View File

@@ -33,9 +33,11 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.wallet.controller.QuickAccessWalletController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,6 +46,7 @@ import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@@ -59,7 +62,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
underTest =
QuickAccessWalletKeyguardQuickAffordanceConfig(
mock(),
context,
walletController,
activityStarter,
)
@@ -151,6 +154,44 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
)
}
@Test
fun `getPickerScreenState - default`() = runTest {
setUpState()
assertThat(underTest.getPickerScreenState())
.isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
}
@Test
fun `getPickerScreenState - unavailable`() = runTest {
setUpState(
isWalletEnabled = false,
)
assertThat(underTest.getPickerScreenState())
.isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
}
@Test
fun `getPickerScreenState - disabled when there is no icon`() = runTest {
setUpState(
hasWalletIcon = false,
)
assertThat(underTest.getPickerScreenState())
.isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
}
@Test
fun `getPickerScreenState - disabled when there is no card`() = runTest {
setUpState(
hasSelectedCard = false,
)
assertThat(underTest.getPickerScreenState())
.isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
}
private fun setUpState(
isWalletEnabled: Boolean = true,
isWalletQuerySuccessful: Boolean = true,

View File

@@ -133,23 +133,24 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
}
@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,
),
fun getAffordancePickerRepresentations() =
runBlocking(IMMEDIATE) {
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() {

View File

@@ -31,7 +31,6 @@ import static org.mockito.Mockito.when;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -133,7 +132,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails(null);
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isFalse();
assertThat(mController.isAbleToOpenCameraApp()).isFalse();
}
@Test
@@ -152,7 +151,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
}
@Test
@@ -162,7 +161,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
}
@Test
@@ -172,7 +171,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
}
@Test
@@ -182,7 +181,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/abc.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
}
@Test
@@ -192,7 +191,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails(null);
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isFalse();
assertThat(mController.isAbleToOpenCameraApp()).isFalse();
}
@Test
@@ -202,21 +201,21 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
"def/.ijk", false);
verifyActivityDetails("def/.ijk");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
null, false);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
// Once from setup + twice from this function
verify(mCallback, times(3)).onQRCodeScannerActivityChanged();
@@ -229,7 +228,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails(null);
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isFalse();
assertThat(mController.isAbleToOpenCameraApp()).isFalse();
mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
@@ -237,14 +236,14 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
verifyActivityDetails("def/.ijk");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
null, false);
verifyActivityDetails(null);
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isFalse();
assertThat(mController.isAbleToOpenCameraApp()).isFalse();
verify(mCallback, times(2)).onQRCodeScannerActivityChanged();
}
@@ -296,19 +295,19 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "0",
UserHandle.USER_CURRENT);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "1",
UserHandle.USER_CURRENT);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
// Once from setup + twice from this function
verify(mCallback, times(3)).onQRCodeScannerPreferenceChanged();
}
@@ -320,13 +319,13 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ true);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
mController.unregisterQRCodeScannerChangeObservers(DEFAULT_QR_CODE_SCANNER_CHANGE,
QR_CODE_SCANNER_PREFERENCE_CHANGE);
verifyActivityDetails(null);
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isFalse();
assertThat(mController.isAbleToOpenCameraApp()).isFalse();
// Unregister once again and make sure it affects the next register event
mController.unregisterQRCodeScannerChangeObservers(DEFAULT_QR_CODE_SCANNER_CHANGE,
@@ -335,7 +334,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
QR_CODE_SCANNER_PREFERENCE_CHANGE);
verifyActivityDetails("abc/.def");
assertThat(mController.isEnabledForLockScreenButton()).isTrue();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
}
@Test
@@ -345,7 +344,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase {
/* enableOnLockScreen */ false);
assertThat(mController.getIntent()).isNotNull();
assertThat(mController.isEnabledForLockScreenButton()).isFalse();
assertThat(mController.isEnabledForQuickSettings()).isTrue();
assertThat(mController.isAbleToOpenCameraApp()).isTrue();
assertThat(getSettingsQRCodeDefaultComponent()).isNull();
}
}

View File

@@ -108,7 +108,7 @@ public class QRCodeScannerTileTest extends SysuiTestCase {
@Test
public void testQRCodeTileUnavailable() {
when(mController.isEnabledForQuickSettings()).thenReturn(false);
when(mController.isAbleToOpenCameraApp()).thenReturn(false);
QSTile.State state = new QSTile.State();
mTile.handleUpdateState(state, null);
assertEquals(state.state, Tile.STATE_UNAVAILABLE);
@@ -116,7 +116,7 @@ public class QRCodeScannerTileTest extends SysuiTestCase {
@Test
public void testQRCodeTileAvailable() {
when(mController.isEnabledForQuickSettings()).thenReturn(true);
when(mController.isAbleToOpenCameraApp()).thenReturn(true);
QSTile.State state = new QSTile.State();
mTile.handleUpdateState(state, null);
assertEquals(state.state, Tile.STATE_INACTIVE);