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

Commit 43dac35e authored by Yuri Lin's avatar Yuri Lin Committed by Matías Hernández
Browse files

Add on/off state description to mode tile toggles.

This allows screen readers to read the state of the button, and react to presses that change it.

The description now reads "[on/off], [mode name], [trigger description]" similarly to how other quick settings tiles behave.

Also adds long click label so it will read "double tap and hold to open settings".

Bug: 359845144
Test: manual with TalkBack, ModesDialogViewModelTest
Flag: android.app.modes_ui
Change-Id: Icb31b77ebe31e607fde87eb51356ea27bafc9278
parent 6501946f
Loading
Loading
Loading
Loading
+77 −0
Original line number Diff line number Diff line
@@ -329,6 +329,83 @@ class ModesDialogViewModelTest : SysuiTestCase() {
            assertThat(tiles!![5].subtext).isEqualTo("Set up")
        }

    @Test
    fun tiles_populatesFieldsForAccessibility() =
        testScope.runTest {
            val tiles by collectLastValue(underTest.tiles)

            repository.addModes(
                listOf(
                    TestModeBuilder()
                        .setName("With description, inactive")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription("When the going gets tough")
                        .setActive(false)
                        .build(),
                    TestModeBuilder()
                        .setName("With description, active")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription("When in Rome")
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("With description, needs setup")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription("When you find yourself in a hole")
                        .setEnabled(false, /* byUser= */ false)
                        .build(),
                    TestModeBuilder()
                        .setName("Without description, inactive")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription(null)
                        .setActive(false)
                        .build(),
                    TestModeBuilder()
                        .setName("Without description, active")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription(null)
                        .setActive(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Without description, needs setup")
                        .setManualInvocationAllowed(true)
                        .setTriggerDescription(null)
                        .setEnabled(false, /* byUser= */ false)
                        .build(),
                )
            )
            runCurrent()

            assertThat(tiles!!).hasSize(6)
            with(tiles?.elementAt(0)!!) {
                assertThat(this.stateDescription).isEqualTo("Off")
                assertThat(this.subtextDescription).isEqualTo("When the going gets tough")
            }
            with(tiles?.elementAt(1)!!) {
                assertThat(this.stateDescription).isEqualTo("On")
                assertThat(this.subtextDescription).isEqualTo("When in Rome")
            }
            with(tiles?.elementAt(2)!!) {
                assertThat(this.stateDescription).isEqualTo("Off")
                assertThat(this.subtextDescription).isEqualTo("Set up")
            }
            with(tiles?.elementAt(3)!!) {
                assertThat(this.stateDescription).isEqualTo("Off")
                assertThat(this.subtextDescription).isEmpty()
            }
            with(tiles?.elementAt(4)!!) {
                assertThat(this.stateDescription).isEqualTo("On")
                assertThat(this.subtextDescription).isEmpty()
            }
            with(tiles?.elementAt(5)!!) {
                assertThat(this.stateDescription).isEqualTo("Off")
                assertThat(this.subtextDescription).isEqualTo("Set up")
            }

            // All tiles have the same long click info
            tiles!!.forEach { assertThat(it.onLongClickLabel).isEqualTo("Open settings") }
        }

    @Test
    fun onClick_togglesTileState() =
        testScope.runTest {
+12 −3
Original line number Diff line number Diff line
@@ -33,6 +33,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.android.systemui.common.ui.compose.Icon
@@ -56,9 +60,11 @@ fun ModeTile(viewModel: ModeTileViewModel) {
                modifier =
                    Modifier.combinedClickable(
                            onClick = viewModel.onClick,
                            onLongClick = viewModel.onLongClick
                            onLongClick = viewModel.onLongClick,
                            onLongClickLabel = viewModel.onLongClickLabel
                        )
                        .padding(20.dp),
                        .padding(20.dp)
                        .semantics { stateDescription = viewModel.stateDescription },
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement =
                    Arrangement.spacedBy(
@@ -76,7 +82,10 @@ fun ModeTile(viewModel: ModeTileViewModel) {
                    Text(
                        viewModel.subtext,
                        fontWeight = FontWeight.W400,
                        modifier = Modifier.tileMarquee().testTag("state")
                        modifier =
                            Modifier.tileMarquee().testTag("state").clearAndSetSemantics {
                                contentDescription = viewModel.subtextDescription
                            }
                    )
                }
            }
+3 −0
Original line number Diff line number Diff line
@@ -28,7 +28,10 @@ data class ModeTileViewModel(
    val icon: Icon,
    val text: String,
    val subtext: String,
    val subtextDescription: String, // version of subtext without "on"/"off" for screen readers
    val enabled: Boolean,
    val stateDescription: String, // "on"/"off" state of the tile, for screen readers
    val onClick: () -> Unit,
    val onLongClick: () -> Unit,
    val onLongClickLabel: String, // for screen readers
)
+26 −6
Original line number Diff line number Diff line
@@ -92,7 +92,12 @@ constructor(
                        icon = zenModeInteractor.getModeIcon(mode).drawable().asIcon(),
                        text = mode.name,
                        subtext = getTileSubtext(mode),
                        subtextDescription = getModeDescription(mode) ?: "",
                        enabled = mode.isActive,
                        stateDescription =
                            context.getString(
                                if (mode.isActive) R.string.zen_mode_on else R.string.zen_mode_off
                            ),
                        onClick = {
                            if (!mode.rule.isEnabled) {
                                openSettings(mode)
@@ -113,7 +118,9 @@ constructor(
                                }
                            }
                        },
                        onLongClick = { openSettings(mode) }
                        onLongClick = { openSettings(mode) },
                        onLongClickLabel =
                            context.resources.getString(R.string.accessibility_long_click_tile)
                    )
                }
            }
@@ -128,23 +135,36 @@ constructor(
        dialogDelegate.launchFromDialog(intent)
    }

    private fun getTileSubtext(mode: ZenMode): String {
    /**
     * Returns a description of the mode, which is:
     *   * a prompt to set up the mode if it is not enabled
     *   * if it cannot be manually activated, text that says so
     *   * otherwise, the trigger description of the mode if it exists...
     *   * ...or null if it doesn't
     *
     * This description is used directly for the content description of a mode tile for screen
     * readers, and for the tile subtext will be augmented with the current status of the mode.
     */
    private fun getModeDescription(mode: ZenMode): String? {
        if (!mode.rule.isEnabled) {
            return context.resources.getString(R.string.zen_mode_set_up)
        }
        if (!mode.rule.isManualInvocationAllowed && !mode.isActive) {
            return context.resources.getString(R.string.zen_mode_no_manual_invocation)
        }
        return mode.getDynamicDescription(context)
    }

        val modeSubtext = mode.getDynamicDescription(context)
    private fun getTileSubtext(mode: ZenMode): String {
        val modeDescription = getModeDescription(mode)
        return if (mode.isActive) {
            if (modeSubtext != null) {
                context.getString(R.string.zen_mode_on_with_details, modeSubtext)
            if (modeDescription != null) {
                context.getString(R.string.zen_mode_on_with_details, modeDescription)
            } else {
                context.getString(R.string.zen_mode_on)
            }
        } else {
            modeSubtext ?: context.getString(R.string.zen_mode_off)
            modeDescription ?: context.getString(R.string.zen_mode_off)
        }
    }