diff options
| author | 2024-08-28 15:58:02 -0400 | |
|---|---|---|
| committer | 2024-08-30 10:54:53 +0000 | |
| commit | 43dac35e5e07b79717677662253e950401e6eb2c (patch) | |
| tree | c1c20cdbbe82f4e00b305a75952510d4f0c55f02 | |
| parent | 6501946fda971d6f68813be0cbf022d7efdadf90 (diff) | |
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
4 files changed, 118 insertions, 9 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index d2bc54e09944..a0f64314098c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -330,6 +330,83 @@ class ModesDialogViewModelTest : SysuiTestCase() { } @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 { val tiles by collectLastValue(underTest.tiles) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt index 3fffd9f77f1f..cacb3843866b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -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 + } ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt index 7c1cb6a9b62e..abd24533e1c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt @@ -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 ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt index 841071347c08..6764839c32d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -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) } } |