Merge "Add logic for building content description for pressed keys" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index 6eef5eb..b91e259 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -337,6 +337,43 @@
}
}
+ @Test
+ fun uiState_pressedKeysDescription_emptyByDefault() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+
+ assertThat((uiState as AddShortcutDialog).pressedKeysDescription).isEmpty()
+ }
+ }
+
+ @Test
+ fun uiState_pressedKeysDescription_updatesToNonEmptyDescriptionWhenKeyCombinationIsPressed() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
+
+ // Note that Action Key is excluded as it's already displayed on the UI
+ assertThat((uiState as AddShortcutDialog).pressedKeysDescription)
+ .isEqualTo("Ctrl, plus A")
+ }
+ }
+
+ @Test
+ fun uiState_pressedKeysDescription_resetsToEmpty_onClearSelectedShortcutKeyCombination() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
+ viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+ viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
+ viewModel.clearSelectedKeyCombination()
+
+ assertThat((uiState as AddShortcutDialog).pressedKeysDescription).isEmpty()
+ }
+ }
+
private suspend fun openAddShortcutDialogAndSetShortcut() {
openAddShortcutDialogAndPressKeyCombination()
viewModel.onSetShortcut()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index 61d11f4..f89421f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -17,19 +17,16 @@
package com.android.systemui.keyboard.shortcut.domain.interactor
import android.content.Context
-import android.view.KeyEvent.META_META_ON
import com.android.systemui.Flags.keyboardShortcutHelperShortcutCustomizer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesRepository
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys.metaModifierIconResId
+import com.android.systemui.keyboard.shortcut.extensions.toContentDescription
import com.android.systemui.keyboard.shortcut.qualifiers.CustomShortcutCategories
import com.android.systemui.keyboard.shortcut.qualifiers.DefaultShortcutCategories
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.res.R
import dagger.Lazy
@@ -105,8 +102,6 @@
context.getString(R.string.shortcut_helper_key_combinations_and_conjunction)
val orConjunction =
context.getString(R.string.shortcut_helper_key_combinations_or_separator)
- val forwardSlash =
- context.getString(R.string.shortcut_helper_key_combinations_forward_slash)
return buildString {
append("$label, $pressKey")
commands.forEachIndexed { i, shortcutCommand ->
@@ -117,29 +112,7 @@
if (j > 0) {
append(" $andConjunction")
}
- if (shortcutKey is ShortcutKey.Text) {
- // Special handling for "/" as TalkBack will not read punctuation by
- // default.
- if (shortcutKey.value.equals("/")) {
- append(" $forwardSlash")
- } else {
- append(" ${shortcutKey.value}")
- }
- } else if (shortcutKey is ShortcutKey.Icon.ResIdIcon) {
- val keyLabel =
- if (shortcutKey.drawableResId == metaModifierIconResId) {
- ShortcutHelperKeys.modifierLabels[META_META_ON]
- } else {
- val keyCode =
- ShortcutHelperKeys.keyIcons.entries
- .firstOrNull { it.value == shortcutKey.drawableResId }
- ?.key
- ShortcutHelperKeys.specialKeyLabels[keyCode]
- }
- if (keyLabel != null) {
- append(" ${keyLabel.invoke(context)}")
- }
- } // No-Op when shortcutKey is ShortcutKey.Icon.DrawableIcon
+ shortcutKey.toContentDescription(context)?.let { append(" $it") }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/ShortcutKeyExtensions.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/ShortcutKeyExtensions.kt
new file mode 100644
index 0000000..5637747
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/ShortcutKeyExtensions.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.extensions
+
+import android.content.Context
+import android.view.KeyEvent.META_META_ON
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys.metaModifierIconResId
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import com.android.systemui.res.R
+
+fun ShortcutKey.toContentDescription(context: Context): String? {
+ val forwardSlash = context.getString(R.string.shortcut_helper_key_combinations_forward_slash)
+ when (this) {
+ is ShortcutKey.Text -> {
+ // Special handling for "/" as TalkBack will not read punctuation by
+ // default.
+ return if (this.value == "/") {
+ forwardSlash
+ } else {
+ this.value
+ }
+ }
+
+ is ShortcutKey.Icon.ResIdIcon -> {
+ val keyLabel =
+ if (this.drawableResId == metaModifierIconResId) {
+ ShortcutHelperKeys.modifierLabels[META_META_ON]
+ } else {
+ val keyCode =
+ ShortcutHelperKeys.keyIcons.entries
+ .firstOrNull { it.value == this.drawableResId }
+ ?.key
+ ShortcutHelperKeys.specialKeyLabels[keyCode]
+ }
+
+ if (keyLabel != null) {
+ return keyLabel.invoke(context)
+ }
+ }
+
+ is ShortcutKey.Icon.DrawableIcon -> {
+ // No-Op when shortcutKey is ShortcutKey.Icon.DrawableIcon
+ }
+ }
+
+ return null
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index 66e4505..7e0fa2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -60,6 +60,7 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
@@ -127,6 +128,7 @@
shouldShowError = uiState.errorMessage.isNotEmpty(),
onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected,
pressedKeys = uiState.pressedKeys,
+ contentDescription = uiState.pressedKeysDescription,
onConfirmSetShortcut = onConfirmSetShortcut,
onClearSelectedKeyCombination = onClearSelectedKeyCombination,
)
@@ -267,6 +269,7 @@
shouldShowError: Boolean,
onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
pressedKeys: List<ShortcutKey>,
+ contentDescription: String,
onConfirmSetShortcut: () -> Unit,
onClearSelectedKeyCombination: () -> Unit,
) {
@@ -313,6 +316,7 @@
} else {
null
},
+ contentDescription = contentDescription,
)
}
@@ -331,8 +335,7 @@
@Composable
private fun PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) {
Row(
- modifier =
- Modifier.semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite },
+ modifier = Modifier.semantics { hideFromAccessibility() },
verticalAlignment = Alignment.CenterVertically,
) {
pressedKeys.forEachIndexed { keyIndex, key ->
@@ -495,6 +498,7 @@
trailingIcon: @Composable () -> Unit,
isError: Boolean,
modifier: Modifier = Modifier,
+ contentDescription: String,
) {
OutlinedTextField(
value = "",
@@ -502,7 +506,10 @@
placeholder = if (content == null) placeholder else null,
prefix = content,
singleLine = true,
- modifier = modifier,
+ modifier =
+ modifier.semantics(mergeDescendants = true) {
+ this.contentDescription = contentDescription
+ },
trailingIcon = trailingIcon,
colors =
OutlinedTextFieldDefaults.colors()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
index 36c5ae0..688573d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/ShortcutCustomizationUiState.kt
@@ -24,6 +24,7 @@
val errorMessage: String = "",
val defaultCustomShortcutModifierKey: ShortcutKey.Icon.ResIdIcon,
val pressedKeys: List<ShortcutKey> = emptyList(),
+ val pressedKeysDescription: String = "",
) : ShortcutCustomizationUiState
data object DeleteShortcutDialog : ShortcutCustomizationUiState
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index f4ba99c..aeedc4b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.input.key.type
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor
+import com.android.systemui.keyboard.shortcut.extensions.toContentDescription
import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
@@ -185,7 +186,11 @@
_shortcutCustomizationUiState.update { uiState ->
if (uiState is AddShortcutDialog) {
- uiState.copy(pressedKeys = keys, errorMessage = errorMessage)
+ uiState.copy(
+ pressedKeys = keys,
+ errorMessage = errorMessage,
+ pressedKeysDescription = getAccessibilityDescForPressedKeys(keys),
+ )
} else {
uiState
}
@@ -193,11 +198,25 @@
}
}
+ private fun getAccessibilityDescForPressedKeys(keys: List<ShortcutKey>): String {
+ val andConjunction =
+ context.getString(R.string.shortcut_helper_key_combinations_and_conjunction)
+ return buildString {
+ keys.forEach { key ->
+ key.toContentDescription(context)?.let {
+ if (isNotEmpty()) {
+ append(", $andConjunction ")
+ }
+ append(it)
+ }
+ }
+ }
+ }
+
private suspend fun getErrorMessageForPressedKeys(keys: List<ShortcutKey>): String {
return if (keys.isEmpty() or isSelectedKeyCombinationAvailable()) {
""
- }
- else {
+ } else {
context.getString(R.string.shortcut_customizer_key_combination_in_use_error_message)
}
}