From 2bc579152d7cd92aac66cafd2b23aff5301d18ed Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 10 Nov 2022 09:15:10 -0800 Subject: Support for unavailable and disabled affordances. End-to-end implementation (across all layers of both System UI and the shared library) for supporting a picker state for affordances, allowing config implementations on the system UI to easily convey whether their affordance should be hidden on the device or should be marked as disabled and show a dialog with instructions for re-enablement. Bug: 256695447,256695925,256695924 Test: unit tests still pass. Was able to see on a tablet that doesn't support NFC that wallet is gone as an option and, when I deselected all home devices, the home affordance was disabled with the correct instruction dialog when picked. Then, marking at least one home device as favorite, the picker allowed me to select "home" again. Change-Id: I77637f28cdacecaa02ab33603bbc008a501d03d1 --- packages/SystemUI/res/values/strings.xml | 46 +++++++++++++ .../KeyguardQuickAffordanceProviderContract.kt | 21 ++++++ .../keyguard/KeyguardQuickAffordanceProvider.kt | 19 ++++-- .../HomeControlsKeyguardQuickAffordanceConfig.kt | 33 +++++++++- .../KeyguardQuickAffordanceConfig.kt | 73 +++++++++++++++++++++ .../QrCodeScannerKeyguardQuickAffordanceConfig.kt | 24 ++++++- ...ickAccessWalletKeyguardQuickAffordanceConfig.kt | 75 +++++++++++++++++++++- .../KeyguardQuickAffordanceRepository.kt | 36 +++++++---- .../KeyguardQuickAffordanceInteractor.kt | 5 +- .../KeyguardQuickAffordancePickerRepresentation.kt | 18 ++++++ .../controller/QRCodeScannerController.java | 12 ++-- .../systemui/qs/tiles/QRCodeScannerTile.java | 2 +- ...dQuickAffordanceConfigParameterizedStateTest.kt | 23 ++++++- ...CodeScannerKeyguardQuickAffordanceConfigTest.kt | 30 ++++++++- ...ccessWalletKeyguardQuickAffordanceConfigTest.kt | 43 ++++++++++++- .../KeyguardQuickAffordanceRepositoryTest.kt | 33 +++++----- .../controller/QRCodeScannerControllerTest.java | 39 ++++++----- .../systemui/qs/tiles/QRCodeScannerTileTest.java | 4 +- 18 files changed, 467 insertions(+), 69 deletions(-) diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9eafdb959f07..785a1d403934 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2798,4 +2798,50 @@ <a href="https://support.google.com/android?p=system_logs#topic=7313011">Learn more</a> + + + Open %1$s + + + To add the %1$s app as a shortcut, make sure + + + • The app is set up + + + • At least one card has been added to Wallet + + + • Install a camera app + + + • The app is set up + + + • At least one device is available diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt index 71469a363f74..98d8d3eb9a4a 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -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" } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index bfc60c17225f..29febb6dd0d9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -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, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index d6f521c6dc87..2558fab216a0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index fd40d1dd1a0e..4477310dca41 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -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 + /** + * 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, + /** + * 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 + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index 11f72ffa4757..a96ce77ee15c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index 303e6a1a95cc..beb20ce4540c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -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 { + 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" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index 533b3abf4fb6..d30050090617 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -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 { - return configs.map { config -> - KeyguardQuickAffordancePickerRepresentation( - id = config.key, - name = config.pickerName, - iconResourceId = config.pickerIconResourceId, - ) - } + suspend fun getAffordancePickerRepresentations(): + List { + 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, + ) + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index c8216c5d72d7..2d94d760cb54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -189,7 +189,7 @@ constructor( } /** Returns affordance IDs indexed by slot ID, for all known slots. */ - fun getSelections(): Map> { + suspend fun getSelections(): Map> { check(isUsingRepository) val slots = repository.get().getSlotPickerRepresentations() @@ -310,7 +310,8 @@ constructor( return Pair(splitUp[0], splitUp[1]) } - fun getAffordancePickerRepresentations(): List { + suspend fun getAffordancePickerRepresentations(): + List { check(isUsingRepository) return repository.get().getAffordancePickerRepresentations() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt index a56bc900f936..7d133598e105 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt @@ -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? = 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, ) diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index 2c20feb19342..fa3f878ff431 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java index 376d3d8da8e7..1a24af10ab08 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java @@ -115,7 +115,7 @@ public class QRCodeScannerTile extends QSTileImpl { 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; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index c94cec6e313a..322014a61a73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -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() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 2bd8e9aabab3..6255980601ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index 5178154bdeee..d875dd94da3e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -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, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index d8a360567a07..bfd5190e2fa1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -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() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java index 346d1e60fcf9..65210d63e5c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java @@ -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(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java index cac90a14096e..6a3f785e1d52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java @@ -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); -- cgit v1.2.3-59-g8ed1b