From 5a0147f03387937ec30167bb01c03a4d43e776c5 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 25 Jan 2025 18:31:33 +0000 Subject: Redesigned Add Shortcut Dialog Edit Box The add shortcut dialog has a box where the users' selected key combination is shown. Previously this box was just a clickable surface that displayed some text/icon composable. There are some downsides to this design: 1. No blinking cursor to show the user that they're currently editing. 2. A11y - No talkback actions on double click despite talback reading "double tap to activate" The main consideration for not using a textfield composable previously was that textfields can only show text, but keyboard keys can be represented as text or glyphs(drawables) which traditional textfields don't support. This CL implements a custom InputField composable `OutlinedInputField` which has all the benefits of a text field(blinking cursor, A11y features) but rather than displaying just text, it generally supports displaying any composable content which can be text, icon, or anything else.this solution hence addresses both Downsides to the previous design. Test: Manual - Ensure the desired behaviour is observed in the add shortcut dialog. Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer Fix: 390278358 Fix: 390102227 Fix: 390281127 Bug: 387995731 Change-Id: Ic5af2c9ba06a8cd0c204f742804d6202f2043e8a --- .../shortcut/ui/composable/ShortcutCustomizer.kt | 160 +++++++++++++-------- 1 file changed, 97 insertions(+), 63 deletions(-) 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 6e9265167b7d..8f857a707e70 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 @@ -17,14 +17,10 @@ package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -38,13 +34,15 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester @@ -55,6 +53,7 @@ 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.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode @@ -247,10 +246,11 @@ private fun ErrorMessageContainer(errorMessage: String) { lineHeight = 20.sp, fontWeight = FontWeight.W500, color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(start = 24.dp).width(252.dp).semantics { - contentDescription = errorMessage - liveRegion = LiveRegionMode.Polite - }, + modifier = + Modifier.padding(start = 24.dp).width(252.dp).semantics { + contentDescription = errorMessage + liveRegion = LiveRegionMode.Polite + }, ) } } @@ -263,71 +263,76 @@ private fun SelectedKeyCombinationContainer( pressedKeys: List, onConfirmSetShortcut: () -> Unit, ) { - val interactionSource = remember { MutableInteractionSource() } - val isFocused by interactionSource.collectIsFocusedAsState() - val outlineColor = - if (!isFocused) MaterialTheme.colorScheme.outline - else if (shouldShowError) MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.primary val focusRequester = remember { FocusRequester() } - + val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { focusRequester.requestFocus() } - ClickableShortcutSurface( - onClick = {}, - color = Color.Transparent, - shape = RoundedCornerShape(50.dp), + OutlinedInputField( modifier = Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) - .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) + .focusRequester(focusRequester) + .focusProperties { canFocus = true } .onPreviewKeyEvent { keyEvent -> val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) - if ( - !keyEventProcessed && - keyEvent.key == Key.Enter && - keyEvent.type == KeyEventType.KeyUp - ) { - onConfirmSetShortcut() + if (keyEventProcessed) { true - } else keyEventProcessed - } - .focusProperties { canFocus = true } // enables keyboard focus when in touch mode - .focusRequester(focusRequester), - interactionSource = interactionSource, - ) { - Row( - modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (pressedKeys.isEmpty()) { - PressKeyPrompt() + } else { + if (keyEvent.type == KeyEventType.KeyUp) { + when (keyEvent.key) { + Key.Enter -> { + onConfirmSetShortcut() + return@onPreviewKeyEvent true + } + Key.DirectionDown -> { + focusManager.moveFocus(FocusDirection.Down) + return@onPreviewKeyEvent true + } + else -> return@onPreviewKeyEvent false + } + } else false + } + }, + trailingIcon = { ErrorIcon(shouldShowError) }, + isError = shouldShowError, + placeholder = { PressKeyPrompt() }, + content = + if (pressedKeys.isNotEmpty()) { + { PressedKeysTextContainer(pressedKeys) } } else { - PressedKeysTextContainer(pressedKeys) - } - Spacer(modifier = Modifier.weight(1f)) - if (shouldShowError) { - Icon( - imageVector = Icons.Default.ErrorOutline, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - } + null + }, + ) +} + +@Composable +private fun ErrorIcon(shouldShowError: Boolean) { + if (shouldShowError) { + Icon( + imageVector = Icons.Default.ErrorOutline, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.error, + ) } } @Composable -private fun RowScope.PressedKeysTextContainer(pressedKeys: List) { - pressedKeys.forEachIndexed { keyIndex, key -> - if (keyIndex > 0) { - ShortcutKeySeparator() - } - if (key is ShortcutKey.Text) { - ShortcutTextKey(key) - } else if (key is ShortcutKey.Icon) { - ShortcutIconKey(key) +private fun PressedKeysTextContainer(pressedKeys: List) { + Row( + modifier = + Modifier.semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite }, + verticalAlignment = Alignment.CenterVertically, + ) { + pressedKeys.forEachIndexed { keyIndex, key -> + if (keyIndex > 0) { + ShortcutKeySeparator() + } + if (key is ShortcutKey.Text) { + ShortcutTextKey(key) + } else if (key is ShortcutKey.Icon) { + ShortcutIconKey(key) + } } } } @@ -344,7 +349,7 @@ private fun ShortcutKeySeparator() { } @Composable -private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) { +private fun ShortcutIconKey(key: ShortcutKey.Icon) { Icon( painter = when (key) { @@ -352,7 +357,8 @@ private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) { is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) }, contentDescription = null, - modifier = Modifier.align(Alignment.CenterVertically).height(24.dp), + modifier = + Modifier.height(24.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -403,7 +409,7 @@ private fun Description(text: String) { .width(316.dp) .wrapContentSize(Alignment.Center), color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } @@ -467,3 +473,31 @@ private fun PlusIconContainer() { modifier = Modifier.padding(vertical = 12.dp).size(24.dp).wrapContentSize(Alignment.Center), ) } + +@Composable +private fun OutlinedInputField( + content: @Composable (() -> Unit)?, + placeholder: @Composable () -> Unit, + trailingIcon: @Composable () -> Unit, + isError: Boolean, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = "", + onValueChange = {}, + placeholder = if (content == null) placeholder else null, + prefix = content, + singleLine = true, + modifier = modifier, + trailingIcon = trailingIcon, + colors = + OutlinedTextFieldDefaults.colors() + .copy( + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.outline, + errorIndicatorColor = MaterialTheme.colorScheme.error, + ), + shape = RoundedCornerShape(50.dp), + isError = isError, + ) +} -- cgit v1.2.3-59-g8ed1b From 092cb7e6cf0d6079c15b221801c8cb128c355306 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 25 Jan 2025 21:42:16 +0000 Subject: Backspace to clear selected shortcut key combination After selecting a key combination, user can now press backspace to clear the selected key combo. Fix: 387995731 Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer Test: ShortcutCustomizationViewModelTest Change-Id: Idcec42d9f2094ffb75915d75d6aa9f49033dc115 --- .../ui/viewmodel/ShortcutCustomizationViewModelTest.kt | 13 +++++++++++++ .../shortcut/ui/ShortcutCustomizationDialogStarter.kt | 1 + .../keyboard/shortcut/ui/composable/ShortcutCustomizer.kt | 12 ++++++++++-- .../shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt | 6 +++++- 4 files changed, 29 insertions(+), 3 deletions(-) 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 d9d34f5ace7b..11efe67cb2ec 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 @@ -297,6 +297,19 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } } + @Test + fun uiState_pressedKeys_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).pressedKeys).isEmpty() + } + } + private suspend fun openAddShortcutDialogAndSetShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index bd3d46d09f5e..864d02ef4a93 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -101,6 +101,7 @@ constructor( onConfirmResetShortcut = { coroutineScope.launch { viewModel.resetAllCustomShortcuts() } }, + onClearSelectedKeyCombination = { viewModel.clearSelectedKeyCombination() }, ) setDialogProperties(dialog, uiState) } 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 8f857a707e70..b13f50cd4320 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 @@ -78,6 +78,7 @@ fun ShortcutCustomizationDialog( onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, onConfirmResetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { @@ -87,6 +88,7 @@ fun ShortcutCustomizationDialog( onShortcutKeyCombinationSelected, onCancel, onConfirmSetShortcut, + onClearSelectedKeyCombination, ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { @@ -108,6 +110,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { Column(modifier = modifier) { Title(uiState.shortcutLabel) @@ -126,6 +129,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, onConfirmSetShortcut = onConfirmSetShortcut, + onClearSelectedKeyCombination = onClearSelectedKeyCombination, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -262,6 +266,7 @@ private fun SelectedKeyCombinationContainer( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current @@ -284,6 +289,10 @@ private fun SelectedKeyCombinationContainer( onConfirmSetShortcut() return@onPreviewKeyEvent true } + Key.Backspace -> { + onClearSelectedKeyCombination() + return@onPreviewKeyEvent true + } Key.DirectionDown -> { focusManager.moveFocus(FocusDirection.Down) return@onPreviewKeyEvent true @@ -357,8 +366,7 @@ private fun ShortcutIconKey(key: ShortcutKey.Icon) { is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) }, contentDescription = null, - modifier = - Modifier.height(24.dp), + modifier = Modifier.height(24.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } 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 915a66c43a12..2a3c23d148bd 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 @@ -92,7 +92,7 @@ constructor( fun onDialogDismissed() { _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive shortcutCustomizationInteractor.onCustomizationRequested(null) - shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + clearSelectedKeyCombination() } fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { @@ -158,6 +158,10 @@ constructor( } } + fun clearSelectedKeyCombination() { + shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + } + private fun getUiStateWithErrorMessage( uiState: ShortcutCustomizationUiState, errorMessage: String, -- cgit v1.2.3-59-g8ed1b From f8bb3ed5a9a8f3fea09a294f3fa65cf9f5789ae8 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 15 Jan 2025 13:25:04 +0000 Subject: Added early verification of selected key combination Test: ShortcutCustomizationViewModelTest CustomShortcutCategoriesRepositoryTest CustomInputGesturesRepositoryTest Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer Fix: 381063978 Change-Id: I6dedfcf0ecccfeb44d6752af7cbb3202ae1ba385 --- .../repository/CustomInputGesturesRepository.kt | 43 +++++++++-------- .../CustomShortcutCategoriesRepository.kt | 25 ++++++---- .../interactor/ShortcutCustomizationInteractor.kt | 3 ++ .../ui/ShortcutCustomizationDialogStarter.kt | 28 ++++++----- .../shortcut/ui/ShortcutHelperDialogStarter.kt | 12 +++-- .../ui/viewmodel/ShortcutCustomizationViewModel.kt | 54 ++++++++++++++-------- 6 files changed, 101 insertions(+), 64 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt index e5c638cbdfba..d355f761e5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt @@ -32,18 +32,19 @@ import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestRe import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.SUCCESS import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext @SysUISingleton class CustomInputGesturesRepository @Inject -constructor(private val userTracker: UserTracker, - @Background private val bgCoroutineContext: CoroutineContext) -{ +constructor( + private val userTracker: UserTracker, + @Background private val bgCoroutineContext: CoroutineContext, +) { private val userContext: Context get() = userTracker.createCurrentUserContext(userTracker.userContext) @@ -55,8 +56,7 @@ constructor(private val userTracker: UserTracker, private val _customInputGesture = MutableStateFlow>(emptyList()) - val customInputGestures = - _customInputGesture.onStart { refreshCustomInputGestures() } + val customInputGestures = _customInputGesture.onStart { refreshCustomInputGestures() } fun refreshCustomInputGestures() { setCustomInputGestures(inputGestures = retrieveCustomInputGestures()) @@ -72,24 +72,24 @@ constructor(private val userTracker: UserTracker, } else emptyList() } - suspend fun addCustomInputGesture(inputGesture: InputGestureData): ShortcutCustomizationRequestResult { + suspend fun addCustomInputGesture( + inputGesture: InputGestureData + ): ShortcutCustomizationRequestResult { return withContext(bgCoroutineContext) { when (val result = inputManager.addCustomInputGesture(inputGesture)) { CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> { refreshCustomInputGestures() SUCCESS } - CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS -> - ERROR_RESERVED_COMBINATION + CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS -> ERROR_RESERVED_COMBINATION - CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -> - ERROR_RESERVED_COMBINATION + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -> ERROR_RESERVED_COMBINATION else -> { Log.w( TAG, "Attempted to add inputGesture: $inputGesture " + - "but ran into an error with code: $result", + "but ran into an error with code: $result", ) ERROR_OTHER } @@ -97,11 +97,11 @@ constructor(private val userTracker: UserTracker, } } - suspend fun deleteCustomInputGesture(inputGesture: InputGestureData): ShortcutCustomizationRequestResult { - return withContext(bgCoroutineContext){ - when ( - val result = inputManager.removeCustomInputGesture(inputGesture) - ) { + suspend fun deleteCustomInputGesture( + inputGesture: InputGestureData + ): ShortcutCustomizationRequestResult { + return withContext(bgCoroutineContext) { + when (val result = inputManager.removeCustomInputGesture(inputGesture)) { CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> { refreshCustomInputGestures() SUCCESS @@ -110,7 +110,7 @@ constructor(private val userTracker: UserTracker, Log.w( TAG, "Attempted to delete inputGesture: $inputGesture " + - "but ran into an error with code: $result", + "but ran into an error with code: $result", ) ERROR_OTHER } @@ -134,7 +134,10 @@ constructor(private val userTracker: UserTracker, } } + suspend fun getInputGestureByTrigger(trigger: InputGestureData.Trigger): InputGestureData? = + withContext(bgCoroutineContext) { inputManager.getInputGesture(trigger) } + private companion object { private const val TAG = "CustomInputGesturesRepository" } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt index 18ca877775df..6ae948d2da2e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.hardware.input.InputGestureData import android.hardware.input.InputGestureData.Builder +import android.hardware.input.InputGestureData.Trigger import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent.KeyGestureType @@ -175,6 +176,11 @@ constructor( return customInputGesturesRepository.resetAllCustomInputGestures() } + suspend fun isSelectedKeyCombinationAvailable(): Boolean { + val trigger = buildTriggerFromSelectedKeyCombination() ?: return false + return customInputGesturesRepository.getInputGestureByTrigger(trigger) == null + } + private fun Builder.addKeyGestureTypeForShortcutBeingCustomized(): Builder { val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized() @@ -222,7 +228,10 @@ constructor( ) } - private fun Builder.addTriggerFromSelectedKeyCombination(): Builder { + private fun Builder.addTriggerFromSelectedKeyCombination(): Builder = + setTrigger(buildTriggerFromSelectedKeyCombination()) + + private fun buildTriggerFromSelectedKeyCombination(): Trigger? { val selectedKeyCombination = _selectedKeyCombination.value if (selectedKeyCombination?.keyCode == null) { Log.w( @@ -230,16 +239,14 @@ constructor( "User requested to set shortcut but selected key combination is " + "$selectedKeyCombination", ) - return this + return null } - return setTrigger( - createKeyTrigger( - /* keycode = */ selectedKeyCombination.keyCode, - /* modifierState = */ shortcutCategoriesUtils.removeUnsupportedModifiers( - selectedKeyCombination.modifiers - ), - ) + return createKeyTrigger( + /* keycode= */ selectedKeyCombination.keyCode, + /* modifierState= */ shortcutCategoriesUtils.removeUnsupportedModifiers( + selectedKeyCombination.modifiers + ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt index ef242678a8ac..1a62517ad01d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt @@ -53,4 +53,7 @@ constructor(private val customShortcutRepository: CustomShortcutCategoriesReposi suspend fun resetAllCustomShortcuts(): ShortcutCustomizationRequestResult { return customShortcutRepository.resetAllCustomShortcuts() } + + suspend fun isSelectedKeyCombinationAvailable(): Boolean = + customShortcutRepository.isSelectedKeyCombinationAvailable() } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 864d02ef4a93..54e27a61ac78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.phone.create import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch class ShortcutCustomizationDialogStarter @@ -57,20 +58,25 @@ constructor( private val viewModel = viewModelFactory.create() override suspend fun onActivated(): Nothing { - viewModel.shortcutCustomizationUiState.collect { uiState -> - when (uiState) { - is AddShortcutDialog, - is DeleteShortcutDialog, - is ResetShortcutDialog -> { - if (dialog == null) { - dialog = createDialog().also { it.show() } + coroutineScope { + launch { + viewModel.shortcutCustomizationUiState.collect { uiState -> + when (uiState) { + is AddShortcutDialog, + is DeleteShortcutDialog, + is ResetShortcutDialog -> { + if (dialog == null) { + dialog = createDialog().also { it.show() } + } + } + is ShortcutCustomizationUiState.Inactive -> { + dialog?.dismiss() + dialog = null + } } } - is ShortcutCustomizationUiState.Inactive -> { - dialog?.dismiss() - dialog = null - } } + launch { viewModel.activate() } } awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt index fa03883e2a35..ea36a10fb01a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt @@ -24,7 +24,6 @@ import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.width -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -36,6 +35,7 @@ import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelperBottomSheet import com.android.systemui.keyboard.shortcut.ui.composable.getWidth import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel +import com.android.systemui.lifecycle.rememberActivated import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory @@ -51,14 +51,13 @@ class ShortcutHelperDialogStarter constructor( @Application private val applicationScope: CoroutineScope, private val shortcutHelperViewModel: ShortcutHelperViewModel, - shortcutCustomizationDialogStarterFactory: ShortcutCustomizationDialogStarter.Factory, + private val shortcutCustomizationDialogStarterFactory: + ShortcutCustomizationDialogStarter.Factory, private val dialogFactory: SystemUIDialogFactory, private val activityStarter: ActivityStarter, ) : CoreStartable { @VisibleForTesting var dialog: Dialog? = null - private val shortcutCustomizationDialogStarter = - shortcutCustomizationDialogStarterFactory.create() override fun start() { shortcutHelperViewModel.shouldShow @@ -77,7 +76,10 @@ constructor( content = { dialog -> val shortcutsUiState by shortcutHelperViewModel.shortcutsUiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { shortcutCustomizationDialogStarter.activate() } + val shortcutCustomizationDialogStarter = + rememberActivated(traceName = "shortcutCustomizationDialogStarter") { + shortcutCustomizationDialogStarterFactory.create() + } ShortcutHelper( modifier = Modifier.width(getWidth()), shortcutsUiState = shortcutsUiState, 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 2a3c23d148bd..f4ba99c6a394 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 @@ -28,16 +28,17 @@ import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestRe import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor 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 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.AddShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.DeleteShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.ResetShortcutDialog +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.res.R import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class ShortcutCustomizationViewModel @@ -45,26 +46,12 @@ class ShortcutCustomizationViewModel constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, -) { +) : ExclusiveActivatable() { private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow(ShortcutCustomizationUiState.Inactive) - val shortcutCustomizationUiState = - shortcutCustomizationInteractor.pressedKeys - .map { keys -> - // Note that Action Key is excluded as it's already displayed on the UI - keys.filter { - it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() - } - } - .combine(_shortcutCustomizationUiState) { keys, uiState -> - if (uiState is AddShortcutDialog) { - uiState.copy(pressedKeys = keys) - } else { - uiState - } - } + val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow() fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) { shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) @@ -112,7 +99,6 @@ constructor( suspend fun onSetShortcut() { val result = shortcutCustomizationInteractor.confirmAndSetShortcutCurrentlyBeingCustomized() - _shortcutCustomizationUiState.update { uiState -> when (result) { ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive @@ -184,11 +170,41 @@ constructor( keyDownEventCache = null } + private suspend fun isSelectedKeyCombinationAvailable() = + shortcutCustomizationInteractor.isSelectedKeyCombinationAvailable() + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel } + override suspend fun onActivated(): Nothing { + shortcutCustomizationInteractor.pressedKeys.collect { + val keys = filterDefaultCustomShortcutModifierKey(it) + val errorMessage = getErrorMessageForPressedKeys(keys) + + _shortcutCustomizationUiState.update { uiState -> + if (uiState is AddShortcutDialog) { + uiState.copy(pressedKeys = keys, errorMessage = errorMessage) + } else { + uiState + } + } + } + } + + private suspend fun getErrorMessageForPressedKeys(keys: List): String { + return if (keys.isEmpty() or isSelectedKeyCombinationAvailable()) { + "" + } + else { + context.getString(R.string.shortcut_customizer_key_combination_in_use_error_message) + } + } + + private fun filterDefaultCustomShortcutModifierKey(keys: List) = + keys.filter { it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() } + companion object { private val SUPPORTED_MODIFIERS = listOf( -- cgit v1.2.3-59-g8ed1b From 7836525e9274e988cc7f064522569945af6875cc Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 26 Jan 2025 20:02:46 +0000 Subject: Added Tests for Early custom shortcut verification Test: ShortcutCustomizationViewModelTest CustomShortcutCategoriesRepositoryTest CustomInputGesturesRepositoryTest Flag: com.android.systemui.keyboard_shortcut_helper_shortcut_customizer Fix: 381063978 Change-Id: Iba9538f0889c113dd364b6557e8cd8af259de6c0 --- .../CustomInputGesturesRepositoryTest.kt | 62 +++++++++------ .../CustomShortcutCategoriesRepositoryTest.kt | 79 +++++++++++++------ .../ShortcutCustomizationViewModelTest.kt | 87 +++++++++++++------- .../src/android/hardware/input/FakeInputManager.kt | 92 ++++++++++++++++------ 4 files changed, 226 insertions(+), 94 deletions(-) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt index 698fac107a1d..4d81cb0ce726 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE import android.content.Intent +import android.hardware.input.FakeInputManager import android.hardware.input.InputGestureData import android.hardware.input.InputManager import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS @@ -57,13 +58,14 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { private val secondaryUserContext: Context = mock() private var activeUserContext: Context = primaryUserContext - private val kosmos = testKosmos().also { - it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext }) - } + private val kosmos = + testKosmos().also { + it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext }) + } private val inputManager = kosmos.fakeInputManager.inputManager private val broadcastDispatcher = kosmos.broadcastDispatcher - private val inputManagerForSecondaryUser: InputManager = mock() + private val inputManagerForSecondaryUser: InputManager = FakeInputManager().inputManager private val testScope = kosmos.testScope private val testHelper = kosmos.shortcutHelperTestHelper private val customInputGesturesRepository = kosmos.customInputGesturesRepository @@ -94,9 +96,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun customInputGestures_initialValueReturnsDataFromAPI() { testScope.runTest { val customInputGestures = listOf(allAppsInputGestureData) - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures) @@ -108,9 +111,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun customInputGestures_isUpdatedToMostRecentDataAfterNewGestureIsAdded() { testScope.runTest { var customInputGestures = listOf() - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } whenever(inputManager.addCustomInputGesture(any())).then { invocation -> val inputGesture = invocation.getArgument(0) customInputGestures = customInputGestures + inputGesture @@ -129,10 +133,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun retrieveCustomInputGestures_retrievesMostRecentData() { testScope.runTest { var customInputGestures = listOf() - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } - + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } assertThat(customInputGesturesRepository.retrieveCustomInputGestures()).isEmpty() @@ -143,24 +147,38 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { } } + @Test + fun getInputGestureByTrigger_returnsInputGestureFromInputManager() = + testScope.runTest { + inputManager.addCustomInputGesture(allAppsInputGestureData) + + val inputGestureData = + customInputGesturesRepository.getInputGestureByTrigger( + allAppsInputGestureData.trigger + ) + + assertThat(inputGestureData).isEqualTo(allAppsInputGestureData) + } + private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) { - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).thenReturn(inputGesture.toList()) + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .thenReturn(inputGesture.toList()) } private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) { whenever( - inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).thenReturn(inputGesture.toList()) + inputManagerForSecondaryUser.getCustomInputGestures( + /* filter= */ InputGestureData.Filter.KEY + ) + ) + .thenReturn(inputGesture.toList()) } private fun switchToSecondaryUser() { activeUserContext = secondaryUserContext broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, - Intent(Intent.ACTION_USER_SWITCHED) + Intent(Intent.ACTION_USER_SWITCHED), ) } - -} \ No newline at end of file +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt index 4cfb26e6555b..522572dcffb7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt @@ -24,6 +24,7 @@ import android.hardware.input.InputGestureData import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION import android.hardware.input.fakeInputManager import android.platform.test.annotations.DisableFlags @@ -336,28 +337,6 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } - private suspend fun customizeShortcut( - customizationRequest: ShortcutCustomizationRequestInfo, - keyCombination: KeyCombination? = null, - ): ShortcutCustomizationRequestResult { - repo.onCustomizationRequested(customizationRequest) - repo.updateUserKeyCombination(keyCombination) - - return when (customizationRequest) { - is SingleShortcutCustomization.Add -> { - repo.confirmAndSetShortcutCurrentlyBeingCustomized() - } - - is SingleShortcutCustomization.Delete -> { - repo.deleteShortcutCurrentlyBeingCustomized() - } - - else -> { - ShortcutCustomizationRequestResult.ERROR_OTHER - } - } - } - @Test @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER) fun categories_isUpdatedAfterCustomShortcutsAreReset() { @@ -387,10 +366,66 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } + @Test + fun selectedKeyCombinationIsAvailable_whenTriggerIsNotRegisteredInInputManager() = + testScope.runTest { + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(allAppsShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + assertThat(repo.isSelectedKeyCombinationAvailable()).isTrue() + } + + @Test + fun selectedKeyCombinationIsNotAvailable_whenTriggerIsRegisteredInInputManager() = + testScope.runTest { + inputManager.addCustomInputGesture(buildInputGestureWithStandardKeyCombination()) + + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(allAppsShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + assertThat(repo.isSelectedKeyCombinationAvailable()).isFalse() + } + private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List) { whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks) } + private suspend fun customizeShortcut( + customizationRequest: ShortcutCustomizationRequestInfo, + keyCombination: KeyCombination? = null, + ): ShortcutCustomizationRequestResult { + repo.onCustomizationRequested(customizationRequest) + repo.updateUserKeyCombination(keyCombination) + + return when (customizationRequest) { + is SingleShortcutCustomization.Add -> { + repo.confirmAndSetShortcutCurrentlyBeingCustomized() + } + + is SingleShortcutCustomization.Delete -> { + repo.deleteShortcutCurrentlyBeingCustomized() + } + + else -> { + ShortcutCustomizationRequestResult.ERROR_OTHER + } + } + } + + private fun buildInputGestureWithStandardKeyCombination() = + InputGestureData.Builder() + .setKeyGestureType(KEY_GESTURE_TYPE_HOME) + .setTrigger( + createKeyTrigger( + /* keycode= */ standardKeyCombination.keyCode!!, + /* modifierState= */ standardKeyCombination.modifiers and + ALL_SUPPORTED_MODIFIERS, + ) + ) + .build() + private fun simpleInputGestureDataForAppLaunchShortcut( keyCode: Int = KEYCODE_A, modifiers: Int = META_CTRL_ON or META_ALT_ON, 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 11efe67cb2ec..6eef5eb09812 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 @@ -18,11 +18,15 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel import android.content.Context import android.content.Context.INPUT_SERVICE -import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS +import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.hardware.input.fakeInputManager +import android.view.KeyEvent.KEYCODE_A +import android.view.KeyEvent.META_CTRL_ON +import android.view.KeyEvent.META_META_ON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -30,7 +34,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsShortcutAddRequest import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsShortcutDeleteRequest -import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithActionKeyPressed import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithoutActionKeyPressed import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyUpEventWithActionKeyPressed @@ -44,16 +47,17 @@ import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiSt import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.DeleteShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.ResetShortcutDialog import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -63,7 +67,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { private val mockUserContext: Context = mock() private val kosmos = - testKosmos().also { + testKosmos().useUnconfinedTestDispatcher().also { it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } private val testScope = kosmos.testScope @@ -75,6 +79,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun setup() { helper.showFromActivity() whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) + testScope.backgroundScope.launch { viewModel.activate() } } @Test @@ -146,8 +151,6 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomeInactiveAfterSuccessfullySettingShortcut() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) openAddShortcutDialogAndSetShortcut() @@ -166,11 +169,38 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { + fun uiState_errorMessage_onKeyPressed_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + + openAddShortcutDialogAndPressKeyCombination() + + assertThat((uiState as AddShortcutDialog).errorMessage) + .isEqualTo( + context.getString( + R.string.shortcut_customizer_key_combination_in_use_error_message + ) + ) + } + } + + @Test + fun uiState_errorMessage_onKeyPressed_isEmpty_whenKeyCombinationIsAvailable() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + + openAddShortcutDialogAndPressKeyCombination() + + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() + } + } + + @Test + fun uiState_errorMessage_onSetShortcut_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { + testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS) openAddShortcutDialogAndSetShortcut() @@ -184,11 +214,12 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationIsReserved() { + fun uiState_errorMessage_onSetShortcut_isKeyCombinationInUse_whenKeyCombinationIsReserved() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + kosmos.fakeInputManager.addCustomInputGestureErrorCode = + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE) openAddShortcutDialogAndSetShortcut() @@ -202,11 +233,12 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isGenericError_whenErrorIsUnknown() { + fun uiState_errorMessage_onSetShortcut_isGenericError_whenErrorIsUnknown() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + kosmos.fakeInputManager.addCustomInputGestureErrorCode = + CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER) openAddShortcutDialogAndSetShortcut() @@ -219,10 +251,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomesInactiveAfterSuccessfullyDeletingShortcut() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.getCustomInputGestures(any())) - .thenReturn(listOf(goHomeInputGestureData, allAppsInputGestureData)) - whenever(inputManager.removeCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) + inputManager.addCustomInputGesture(allAppsInputGestureData) openDeleteShortcutDialogAndDeleteShortcut() @@ -234,7 +263,6 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomesInactiveAfterSuccessfullyResettingShortcuts() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.getCustomInputGestures(any())).thenReturn(emptyList()) openResetShortcutDialogAndResetAllCustomShortcuts() @@ -305,29 +333,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) viewModel.clearSelectedKeyCombination() - assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } private suspend fun openAddShortcutDialogAndSetShortcut() { - viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) + openAddShortcutDialogAndPressKeyCombination() + viewModel.onSetShortcut() + } + private fun openAddShortcutDialogAndPressKeyCombination() { + viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) - - viewModel.onSetShortcut() } private suspend fun openDeleteShortcutDialogAndDeleteShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutDeleteRequest) - viewModel.deleteShortcutCurrentlyBeingCustomized() } private suspend fun openResetShortcutDialogAndResetAllCustomShortcuts() { viewModel.onShortcutCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) - viewModel.resetAllCustomShortcuts() } + + private fun buildSimpleInputGestureWithMetaCtrlATrigger() = + InputGestureData.Builder() + .setKeyGestureType(KEY_GESTURE_TYPE_HOME) + .setTrigger(createKeyTrigger(KEYCODE_A, META_CTRL_ON or META_META_ON)) + .build() } diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt index de4bbecaaf0e..42c509eeaa0b 100644 --- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt +++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt @@ -16,16 +16,19 @@ package android.hardware.input +import android.hardware.input.InputGestureData.Trigger +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS import android.hardware.input.InputManager.InputDeviceListener import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD import android.view.KeyEvent -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import org.mockito.ArgumentMatchers.anyInt import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.mock class FakeInputManager { @@ -49,36 +52,79 @@ class FakeInputManager { ) private var inputDeviceListener: InputDeviceListener? = null + private val customInputGestures: MutableMap = mutableMapOf() + var addCustomInputGestureErrorCode = CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS + + val inputManager: InputManager = mock { + on { getCustomInputGestures(any()) }.then { customInputGestures.values.toList() } + + on { addCustomInputGesture(any()) } + .then { + val inputGestureData = it.getArgument(0) + val trigger = inputGestureData.trigger + + if (customInputGestures.containsKey(trigger)) { + addCustomInputGestureErrorCode + } else { + customInputGestures[trigger] = inputGestureData + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS + } + } + + on { removeCustomInputGesture(any()) } + .then { + val inputGestureData = it.getArgument(0) + val trigger = inputGestureData.trigger + + if (customInputGestures.containsKey(trigger)) { + customInputGestures.remove(trigger) + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS + } else { + CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST + } + } + + on { removeAllCustomInputGestures(any()) }.then { customInputGestures.clear() } - val inputManager = - mock { - whenever(getInputDevice(anyInt())).thenAnswer { invocation -> + on { getInputGesture(any()) } + .then { + val trigger = it.getArgument(0) + customInputGestures[trigger] + } + + on { getInputDevice(anyInt()) } + .thenAnswer { invocation -> val deviceId = invocation.arguments[0] as Int return@thenAnswer devices[deviceId] } - whenever(inputDeviceIds).thenAnswer { + on { inputDeviceIds } + .thenAnswer { return@thenAnswer devices.keys.toIntArray() } - fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) { - val deviceId = invocation.arguments[0] as Int - val device = devices[deviceId] ?: return - devices[deviceId] = device.copy(enabled = enabled) - } + fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) { + val deviceId = invocation.arguments[0] as Int + val device = devices[deviceId] ?: return + devices[deviceId] = device.copy(enabled = enabled) + } - whenever(disableInputDevice(anyInt())).thenAnswer { invocation -> - setDeviceEnabled(invocation, enabled = false) - } - whenever(enableInputDevice(anyInt())).thenAnswer { invocation -> - setDeviceEnabled(invocation, enabled = true) - } - whenever(deviceHasKeys(any(), any())).thenAnswer { invocation -> + on { disableInputDevice(anyInt()) } + .thenAnswer { invocation -> setDeviceEnabled(invocation, enabled = false) } + on { enableInputDevice(anyInt()) } + .thenAnswer { invocation -> setDeviceEnabled(invocation, enabled = true) } + on { deviceHasKeys(any(), any()) } + .thenAnswer { invocation -> val deviceId = invocation.arguments[0] as Int val keyCodes = invocation.arguments[1] as IntArray val supportedKeyCodes = supportedKeyCodesByDeviceId[deviceId]!! return@thenAnswer keyCodes.map { supportedKeyCodes.contains(it) }.toBooleanArray() } - } + } + + fun resetCustomInputGestures() { + customInputGestures.clear() + addCustomInputGestureErrorCode = CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS + } fun addPhysicalKeyboardIfNotPresent(deviceId: Int, enabled: Boolean = true) { if (devices.containsKey(deviceId)) { @@ -97,7 +143,7 @@ class FakeInputManager { vendorId: Int = 0, productId: Int = 0, isFullKeyboard: Boolean = true, - enabled: Boolean = true + enabled: Boolean = true, ) { check(id > 0) { "Physical keyboard ids have to be > 0" } addKeyboard(id, vendorId, productId, isFullKeyboard, enabled) @@ -113,7 +159,7 @@ class FakeInputManager { vendorId: Int = 0, productId: Int = 0, isFullKeyboard: Boolean = true, - enabled: Boolean = true + enabled: Boolean = true, ) { val keyboardType = if (isFullKeyboard) InputDevice.KEYBOARD_TYPE_ALPHABETIC @@ -152,7 +198,7 @@ class FakeInputManager { id: Int = getId(), type: Int = keyboardType, sources: Int = getSources(), - enabled: Boolean = isEnabled + enabled: Boolean = isEnabled, ) = InputDevice.Builder() .setId(id) -- cgit v1.2.3-59-g8ed1b