Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit af8fd1b3 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Ale Nijamkin
Browse files

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
Merged-In: I77637f28cdacecaa02ab33603bbc008a501d03d1
parent c4588f70
Loading
Loading
Loading
Loading
+46 −0
Original line number Diff line number Diff line
@@ -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>
+21 −0
Original line number Diff line number Diff line
@@ -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"
        }
    }

+15 −4
Original line number Diff line number Diff line
@@ -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,
                        )
                    )
                }
+32 −1
Original line number Diff line number Diff line
@@ -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 {
+73 −0
Original line number Diff line number Diff line
@@ -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
            }
        }
    }
}
Loading