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

Commit 312ecf91 authored by Josh's avatar Josh
Browse files

Pressing enter should correspond to default button action

In shortcut customization dialogs, pressing enter should correspond to
the default button action. This is achieved by setting default focus to
the default buttons in reset and delete confirmation dialogs, and by
capturing and processing Enter key event for Add shortcut dialog. For
this key interception to work seamlessly, I've updated the
implementation of viewModel.onKeyPressed to only process key events on
ACTION_UP events as opposed to ACTION_DOWN previously. This is necessary
to ensure that when the user selects an keycombination such as
Meta+Enter in the add shortcut dialog, the dialog doesn't close
immediately in response to Enter key being pressed.

Test: ShortcutCustomizationViewModelTest
Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer
Fix: 381065266
Change-Id: I9f8fbc107ea3c1725ae3e756a62c95a4336c549a
parent 5f678da0
Loading
Loading
Loading
Loading
+3 −6
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
import android.os.SystemClock
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_DOWN
import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_A
import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
@@ -540,11 +541,7 @@ object TestShortcuts {
            simpleShortcutCategory(System, "System apps", "Take a note"),
            simpleShortcutCategory(System, "System controls", "Take screenshot"),
            simpleShortcutCategory(System, "System controls", "Go back"),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
                "Switch to full screen",
            ),
            simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"),
            simpleShortcutCategory(
                MultiTasking,
                "Split screen",
@@ -704,7 +701,7 @@ object TestShortcuts {
            android.view.KeyEvent(
                /* downTime = */ SystemClock.uptimeMillis(),
                /* eventTime = */ SystemClock.uptimeMillis(),
                /* action = */ ACTION_DOWN,
                /* action = */ ACTION_UP,
                /* code = */ KEYCODE_A,
                /* repeat = */ 0,
                /* metaState = */ 0,
+23 −23
Original line number Diff line number Diff line
@@ -92,7 +92,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)

            assertThat(uiState).isEqualTo(
            assertThat(uiState)
                .isEqualTo(
                    AddShortcutDialog(
                        shortcutLabel = "Standard shortcut",
                        defaultCustomShortcutModifierKey =
@@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
        testScope.runTest {
            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            assertThat((uiState as AddShortcutDialog).pressedKeys)
                .isEmpty()
            assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
        }
    }

@@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
            viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)

            assertThat((uiState as AddShortcutDialog).errorMessage)
                .isEmpty()
            assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty()
        }
    }

@@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
    }

    @Test
    fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
    fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
        testScope.runTest {
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
            val isHandled =
                viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)

            assertThat(isHandled).isTrue()
        }
    }

    @Test
    fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
    fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
        testScope.runTest {
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed)
            val isHandled =
                viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed)

            assertThat(isHandled).isFalse()
        }
    }

    @Test
    fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
    fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
        testScope.runTest {
            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
            viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
            viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)

            // Note that Action Key is excluded as it's already displayed on the UI
            assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
        testScope.runTest {
            val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
            viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
            viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)

            // Note that Action Key is excluded as it's already displayed on the UI
            assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() {
            // Close the dialog and show it again
            viewModel.onDialogDismissed()
            viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
            assertThat((uiState as AddShortcutDialog).pressedKeys)
                .isEmpty()
            assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
        }
    }

    private suspend fun openAddShortcutDialogAndSetShortcut() {
        viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)

        viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
        viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
        viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
        viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)

        viewModel.onSetShortcut()
    }
+5 −3
Original line number Diff line number Diff line
@@ -85,7 +85,9 @@ constructor(
            ShortcutCustomizationDialog(
                uiState = uiState,
                modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
                onKeyPress = { viewModel.onKeyPressed(it) },
                onShortcutKeyCombinationSelected = {
                    viewModel.onShortcutKeyCombinationSelected(it)
                },
                onCancel = { dialog.dismiss() },
                onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } },
                onConfirmDeleteShortcut = {
+53 −39
Original line number Diff line number Diff line
@@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -65,7 +69,7 @@ import com.android.systemui.res.R
fun ShortcutCustomizationDialog(
    uiState: ShortcutCustomizationUiState,
    modifier: Modifier = Modifier,
    onKeyPress: (KeyEvent) -> Boolean,
    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
    onCancel: () -> Unit,
    onConfirmSetShortcut: () -> Unit,
    onConfirmDeleteShortcut: () -> Unit,
@@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog(
) {
    when (uiState) {
        is ShortcutCustomizationUiState.AddShortcutDialog -> {
            AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut)
            AddShortcutDialog(
                modifier,
                uiState,
                onShortcutKeyCombinationSelected,
                onCancel,
                onConfirmSetShortcut,
            )
        }
        is ShortcutCustomizationUiState.DeleteShortcutDialog -> {
            DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut)
@@ -91,17 +101,14 @@ fun ShortcutCustomizationDialog(
private fun AddShortcutDialog(
    modifier: Modifier,
    uiState: ShortcutCustomizationUiState.AddShortcutDialog,
    onKeyPress: (KeyEvent) -> Boolean,
    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
    onCancel: () -> Unit,
    onConfirmSetShortcut: () -> Unit
    onConfirmSetShortcut: () -> Unit,
) {
    Column(modifier = modifier) {
        Title(uiState.shortcutLabel)
        Description(
            text =
            stringResource(
                id = R.string.shortcut_customize_mode_add_shortcut_description
            )
            text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description)
        )
        PromptShortcutModifier(
            modifier =
@@ -112,8 +119,9 @@ private fun AddShortcutDialog(
        )
        SelectedKeyCombinationContainer(
            shouldShowError = uiState.errorMessage.isNotEmpty(),
            onKeyPress = onKeyPress,
            onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected,
            pressedKeys = uiState.pressedKeys,
            onConfirmSetShortcut = onConfirmSetShortcut,
        )
        ErrorMessageContainer(uiState.errorMessage)
        DialogButtons(
@@ -121,9 +129,7 @@ private fun AddShortcutDialog(
            isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(),
            onConfirm = onConfirmSetShortcut,
            confirmButtonText =
            stringResource(
                R.string.shortcut_helper_customize_dialog_set_shortcut_button_label
            ),
                stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
        )
    }
}
@@ -132,18 +138,13 @@ private fun AddShortcutDialog(
private fun DeleteShortcutDialog(
    modifier: Modifier,
    onCancel: () -> Unit,
    onConfirmDeleteShortcut: () -> Unit
    onConfirmDeleteShortcut: () -> Unit,
) {
    ConfirmationDialog(
        modifier = modifier,
        title =
        stringResource(
            id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title
        ),
        title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title),
        description =
        stringResource(
            id = R.string.shortcut_customize_mode_remove_shortcut_description
        ),
            stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description),
        confirmButtonText =
            stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
        onCancel = onCancel,
@@ -155,18 +156,13 @@ private fun DeleteShortcutDialog(
private fun ResetShortcutDialog(
    modifier: Modifier,
    onCancel: () -> Unit,
    onConfirmResetShortcut: () -> Unit
    onConfirmResetShortcut: () -> Unit,
) {
    ConfirmationDialog(
        modifier = modifier,
        title =
        stringResource(
            id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title
        ),
        title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title),
        description =
        stringResource(
            id = R.string.shortcut_customize_mode_reset_shortcut_description
        ),
            stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description),
        confirmButtonText =
            stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
        onCancel = onCancel,
@@ -201,6 +197,9 @@ private fun DialogButtons(
    onConfirm: () -> Unit,
    confirmButtonText: String,
) {
    val focusRequester = remember { FocusRequester() }
    LaunchedEffect(Unit) { focusRequester.requestFocus() }

    Row(
        modifier =
            Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
@@ -218,6 +217,10 @@ private fun DialogButtons(
        )
        Spacer(modifier = Modifier.width(8.dp))
        ShortcutHelperButton(
            modifier =
                Modifier.focusRequester(focusRequester).focusProperties {
                    canFocus = true
                }, // enable focus on touch/click mode
            onClick = onConfirm,
            color = MaterialTheme.colorScheme.primary,
            width = 116.dp,
@@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) {
@Composable
private fun SelectedKeyCombinationContainer(
    shouldShowError: Boolean,
    onKeyPress: (KeyEvent) -> Boolean,
    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
    pressedKeys: List<ShortcutKey>,
    onConfirmSetShortcut: () -> Unit,
) {
    val interactionSource = remember { MutableInteractionSource() }
    val isFocused by interactionSource.collectIsFocusedAsState()
@@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer(
            Modifier.padding(all = 16.dp)
                .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
                .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
                .onKeyEvent { onKeyPress(it) }
                .onPreviewKeyEvent { keyEvent ->
                    val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent)
                    if (
                        !keyEventProcessed &&
                            keyEvent.key == Key.Enter &&
                            keyEvent.type == KeyEventType.KeyUp
                    ) {
                        onConfirmSetShortcut()
                        true
                    } else keyEventProcessed
                }
                .focusProperties { canFocus = true } // enables keyboard focus when in touch mode
                .focusRequester(focusRequester),
        interactionSource = interactionSource,
+18 −5
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ constructor(
    private val context: Context,
    private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor,
) {
    private var keyDownEventCache: KeyEvent? = null
    private val _shortcutCustomizationUiState =
        MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)

@@ -94,9 +95,16 @@ constructor(
        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null)
    }

    fun onKeyPressed(keyEvent: KeyEvent): Boolean {
        if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) {
            updatePressedKeys(keyEvent)
    fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean {
        if (isModifier(keyEvent)) {
            return false
        }
        if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) {
            keyDownEventCache = keyEvent
            return true
        } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) {
            updatePressedKeys(keyDownEventCache!!)
            clearKeyDownEventCache()
            return true
        }
        return false
@@ -157,16 +165,21 @@ constructor(
        return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState
    }

    private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key)

    private fun updatePressedKeys(keyEvent: KeyEvent) {
        val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key)
        val keyCombination =
            KeyCombination(
                modifiers = keyEvent.nativeKeyEvent.modifiers,
                keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null,
                keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null,
            )
        shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination)
    }

    private fun clearKeyDownEventCache() {
        keyDownEventCache = null
    }

    @AssistedFactory
    interface Factory {
        fun create(): ShortcutCustomizationViewModel