diff options
13 files changed, 437 insertions, 38 deletions
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 27b8c59a076d..f04540426fc1 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 @@ -18,16 +18,22 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE +import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS import android.hardware.input.fakeInputManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CAPS_LOCK_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_FUNCTION_ON +import android.view.KeyEvent.META_META_LEFT_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON +import android.view.KeyEvent.META_SHIFT_RIGHT_ON import android.view.KeyEvent.META_SYM_ON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -40,6 +46,8 @@ import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCusto import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.kosmos.testScope @@ -188,6 +196,69 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } + @Test + fun shortcutBeingCustomized_updatedOnCustomizationRequested() { + testScope.runTest { + repo.onCustomizationRequested(standardCustomizationRequestInfo) + + val shortcutBeingCustomized = repo.getShortcutBeingCustomized() + + assertThat(shortcutBeingCustomized).isEqualTo(standardCustomizationRequestInfo) + } + } + + @Test + fun buildInputGestureDataForShortcutBeingCustomized_noShortcutBeingCustomized_returnsNull() { + testScope.runTest { + helper.toggle(deviceId = 123) + repo.updateUserKeyCombination(standardKeyCombination) + + val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized() + + assertThat(inputGestureData).isNull() + } + } + + @Test + fun buildInputGestureDataForShortcutBeingCustomized_noKeyCombinationSelected_returnsNull() { + testScope.runTest { + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(standardCustomizationRequestInfo) + + val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized() + + assertThat(inputGestureData).isNull() + } + } + + @Test + fun buildInputGestureDataForShortcutBeingCustomized_successfullyBuildInputGestureData() { + testScope.runTest { + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(standardCustomizationRequestInfo) + repo.updateUserKeyCombination(standardKeyCombination) + val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized() + + // using toString as we're testing for only structural equality not referential. + // inputGestureData is a java class and isEqual Tests for referential equality + // as well which would cause this assert to fail + assertThat(inputGestureData.toString()).isEqualTo(standardInputGestureData.toString()) + } + } + + private val standardCustomizationRequestInfo = + ShortcutCustomizationRequestInfo.Add( + label = "Open apps list", + categoryType = ShortcutCategoryType.System, + subCategoryLabel = "System controls", + ) + + private val standardKeyCombination = + KeyCombination( + modifiers = META_META_ON or META_SHIFT_ON or META_META_LEFT_ON or META_SHIFT_RIGHT_ON, + keyCode = KEYCODE_A, + ) + private val allSupportedModifiers = META_META_ON or META_CTRL_ON or @@ -195,4 +266,15 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { META_SHIFT_ON or META_ALT_ON or META_SYM_ON + + private val standardInputGestureData = + InputGestureData.Builder() + .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS) + .setTrigger( + createKeyTrigger( + /* keycode = */ standardKeyCombination.keyCode!!, + /* modifierState = */ standardKeyCombination.modifiers and allSupportedModifiers, + ) + ) + .build() } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 372f6a506287..3dd49bfed385 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3833,13 +3833,17 @@ <!-- Error message displayed when the user select a key combination that is already in use while assigning a new custom key combination to a shortcut in shortcut helper. The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> - <string name="shortcut_helper_customize_dialog_error_message">Key combination already in use. Try another key.</string> + <string name="shortcut_customizer_key_combination_in_use_error_message">Key combination already in use. Try another key.</string> + <!-- Generic error message displayed when the user selected key combination cannot be used as + custom keyboard shortcut in shortcut helper. The helper is a component that shows the user + which keyboard shortcuts they can use and allows users to customize their keyboard + shortcuts. [CHAR LIMIT=NONE] --> + <string name="shortcut_customizer_generic_error_message">Shortcut cannot be set.</string> <!-- Plus sign, used in keyboard shortcut helper to combine keys for shortcut. E.g. Ctrl + A The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_plus_symbol">+</string> - <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_tutorial_notification_title">Navigate using your keyboard</string> diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/ShortcutCustomizationRequestResult.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/ShortcutCustomizationRequestResult.kt new file mode 100644 index 000000000000..bb563b1f6561 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/ShortcutCustomizationRequestResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.shared.model + +enum class ShortcutCustomizationRequestResult { + SUCCESS, + ERROR_RESERVED_COMBINATION, + ERROR_OTHER, +} 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 da5590ae27fa..99cafd3daacb 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 @@ -19,23 +19,31 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.Builder import android.hardware.input.InputGestureData.KeyTrigger +import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS +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.InputSettings import android.hardware.input.KeyGestureEvent +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.mutableStateOf import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.settings.UserTracker -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -44,6 +52,8 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext @SysUISingleton class CustomShortcutCategoriesRepository @@ -52,9 +62,10 @@ constructor( stateRepository: ShortcutHelperStateRepository, private val userTracker: UserTracker, @Background private val backgroundScope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val bgCoroutineContext: CoroutineContext, private val shortcutCategoriesUtils: ShortcutCategoriesUtils, private val context: Context, + private val inputGestureMaps: InputGestureMaps ) : ShortcutCategoriesRepository { private val userContext: Context @@ -66,11 +77,12 @@ constructor( get() = userContext.getSystemService(INPUT_SERVICE) as InputManager private val _selectedKeyCombination = MutableStateFlow<KeyCombination?>(null) + private val _shortcutBeingCustomized = mutableStateOf<ShortcutCustomizationRequestInfo?>(null) private val activeInputDevice = stateRepository.state.map { if (it is Active) { - withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) } + withContext(bgCoroutineContext) { inputManager.getInputDevice(it.deviceId) } } else { null } @@ -150,6 +162,92 @@ constructor( _selectedKeyCombination.value = keyCombination } + fun onCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo?) { + _shortcutBeingCustomized.value = requestInfo + } + + @VisibleForTesting + fun buildInputGestureDataForShortcutBeingCustomized(): InputGestureData? { + try { + return Builder() + .addKeyGestureTypeFromShortcutLabel() + .addTriggerFromSelectedKeyCombination() + .build() + // TODO(b/379648200) add app launch data for application categories shortcut after + // dynamic + // label/icon mapping implementation + } catch (e: IllegalArgumentException) { + Log.w(TAG, "could not add custom shortcut: $e") + return null + } + } + + suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult { + return withContext(bgCoroutineContext) { + val inputGestureData = + buildInputGestureDataForShortcutBeingCustomized() + ?: return@withContext ShortcutCustomizationRequestResult.ERROR_OTHER + + return@withContext when (inputManager.addCustomInputGesture(inputGestureData)) { + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> ShortcutCustomizationRequestResult.SUCCESS + CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS -> + ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION + + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -> + ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION + + else -> ShortcutCustomizationRequestResult.ERROR_OTHER + } + } + } + + private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder { + val shortcutBeingCustomized = + getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add + + if (shortcutBeingCustomized == null) { + Log.w(TAG, "User requested to set shortcut but shortcut being customized is null") + return this + } + + val keyGestureType = + inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label] + + if (keyGestureType == null) { + Log.w(TAG, "Could not find KeyGestureType for shortcut $shortcutBeingCustomized") + return this + } + + return setKeyGestureType(keyGestureType) + } + + private fun Builder.addTriggerFromSelectedKeyCombination(): Builder { + val selectedKeyCombination = _selectedKeyCombination.value + if (selectedKeyCombination?.keyCode == null) { + Log.w( + TAG, + "User requested to set shortcut but selected key combination is " + + "$selectedKeyCombination", + ) + return this + } + + return setTrigger( + createKeyTrigger( + /* keycode = */ selectedKeyCombination.keyCode, + /* modifierState = */ + shortcutCategoriesUtils.removeUnsupportedModifiers( + selectedKeyCombination.modifiers + ), + ) + ) + } + + @VisibleForTesting + fun getShortcutBeingCustomized(): ShortcutCustomizationRequestInfo? { + return _shortcutBeingCustomized.value + } + private fun toInternalGroupSources( inputGestures: List<InputGestureData> ): List<InternalGroupsSource> { @@ -158,8 +256,10 @@ constructor( val keyTrigger = gestureData.trigger as KeyTrigger val keyGestureType = gestureData.action.keyGestureType() fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel -> - toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let { - internalKeyboardShortcutInfo -> + toInternalKeyboardShortcutInfo( + keyGestureType, + keyTrigger + )?.let { internalKeyboardShortcutInfo -> val group = InternalKeyboardShortcutGroup( label = groupLabel, @@ -194,7 +294,7 @@ constructor( private fun fetchGroupLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - InputGestures.gestureToInternalKeyboardShortcutGroupLabelResIdMap[keyGestureType]?.let { + inputGestureMaps.gestureToInternalKeyboardShortcutGroupLabelResIdMap[keyGestureType]?.let { return context.getString(it) } ?: return null } @@ -202,7 +302,7 @@ constructor( private fun fetchShortcutInfoLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - InputGestures.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let { + inputGestureMaps.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let { return context.getString(it) } ?: return null } @@ -210,11 +310,15 @@ constructor( private fun fetchShortcutCategoryTypeByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): ShortcutCategoryType? { - return InputGestures.gestureToShortcutCategoryTypeMap[keyGestureType] + return inputGestureMaps.gestureToShortcutCategoryTypeMap[keyGestureType] } private data class InternalGroupsSource( val groups: List<InternalKeyboardShortcutGroup>, val type: ShortcutCategoryType, ) + + private companion object { + private const val TAG = "CustomShortcutCategoriesRepository" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index 7bb294df98cd..d228a15e51b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyboard.shortcut.data.repository +import android.content.Context import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_BACK import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT @@ -45,8 +46,11 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType. import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.res.R +import javax.inject.Inject -object InputGestures { +class InputGestureMaps +@Inject +constructor(private val context: Context) { val gestureToShortcutCategoryTypeMap = mapOf( // System Category @@ -174,4 +178,11 @@ object InputGestures { KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to R.string.keyboard_shortcut_group_applications_sms, ) + + val shortcutLabelToKeyGestureTypeMap: Map<String, Int> + get() = gestureToInternalKeyboardShortcutInfoLabelResIdMap.entries.associateBy({ + context.getString(it.value) + }) { + it.key + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt index 3988d1f155bd..95bc9f66618c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt @@ -36,9 +36,9 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory +import kotlinx.coroutines.withContext import javax.inject.Inject import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.withContext class ShortcutCategoriesUtils @Inject @@ -47,6 +47,11 @@ constructor( @Background private val backgroundCoroutineContext: CoroutineContext, private val inputManager: InputManager, ) { + + fun removeUnsupportedModifiers(modifierMask: Int): Int { + return SUPPORTED_MODIFIERS.reduce { acc, modifier -> acc or modifier } and modifierMask + } + fun fetchShortcutCategory( type: ShortcutCategoryType?, groups: List<InternalKeyboardShortcutGroup>, 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 aad55dc11c16..f4e2f05379bb 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 @@ -16,9 +16,11 @@ package com.android.systemui.keyboard.shortcut.domain.interactor +import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys 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 javax.inject.Inject @@ -34,4 +36,12 @@ constructor(private val customShortcutRepository: CustomShortcutCategoriesReposi fun getDefaultCustomShortcutModifierKey(): ShortcutKey.Icon.ResIdIcon { return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.metaModifierIconResId) } + + fun onCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo?) { + customShortcutRepository.onCustomizationRequested(requestInfo) + } + + suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult { + return customShortcutRepository.confirmAndSetShortcutCurrentlyBeingCustomized() + } } 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 2cb822eb0609..310078a647e6 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 @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -35,6 +36,7 @@ import com.android.systemui.statusbar.phone.create import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch class ShortcutCustomizationDialogStarter @AssistedInject @@ -70,14 +72,21 @@ constructor( return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog -> val uiState by - viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle( - initialValue = ShortcutCustomizationUiState.Inactive - ) + viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle( + initialValue = ShortcutCustomizationUiState.Inactive + ) + val coroutineScope = rememberCoroutineScope() AssignNewShortcutDialog( uiState = uiState, - modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp), + modifier = Modifier + .width(364.dp) + .wrapContentHeight() + .padding(vertical = 24.dp), onKeyPress = { viewModel.onKeyPressed(it) }, onCancel = { dialog.dismiss() }, + onConfirmSetShortcut = { + coroutineScope.launch { viewModel.onSetShortcut() } + }, ) dialog.setOnDismissListener { viewModel.onDialogDismissed() } 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 d7229336c0d7..c9b778e96dd1 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 @@ -66,6 +66,7 @@ fun AssignNewShortcutDialog( modifier: Modifier = Modifier, onKeyPress: (KeyEvent) -> Boolean, onCancel: () -> Unit, + onConfirmSetShortcut: () -> Unit, ) { if (uiState is ShortcutCustomizationUiState.AddShortcutDialog) { Column(modifier = modifier) { @@ -84,18 +85,26 @@ fun AssignNewShortcutDialog( defaultModifierKey = uiState.defaultCustomShortcutModifierKey, ) SelectedKeyCombinationContainer( - shouldShowErrorMessage = uiState.shouldShowErrorMessage, + shouldShowError = uiState.errorMessage.isNotEmpty(), onKeyPress = onKeyPress, pressedKeys = uiState.pressedKeys, ) - KeyCombinationAlreadyInUseErrorMessage(uiState.shouldShowErrorMessage) - DialogButtons(onCancel, isSetShortcutButtonEnabled = uiState.pressedKeys.isNotEmpty()) + ErrorMessageContainer(uiState.errorMessage) + DialogButtons( + onCancel, + isSetShortcutButtonEnabled = uiState.pressedKeys.isNotEmpty(), + onConfirm = onConfirmSetShortcut, + ) } } } @Composable -fun DialogButtons(onCancel: () -> Unit, isSetShortcutButtonEnabled: Boolean) { +fun DialogButtons( + onCancel: () -> Unit, + isSetShortcutButtonEnabled: Boolean, + onConfirm: () -> Unit, +) { Row( modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp) @@ -113,7 +122,7 @@ fun DialogButtons(onCancel: () -> Unit, isSetShortcutButtonEnabled: Boolean) { ) Spacer(modifier = Modifier.width(8.dp)) ShortcutHelperButton( - onClick = {}, + onClick = onConfirm, color = MaterialTheme.colorScheme.primary, width = 116.dp, contentColor = MaterialTheme.colorScheme.onPrimary, @@ -125,11 +134,11 @@ fun DialogButtons(onCancel: () -> Unit, isSetShortcutButtonEnabled: Boolean) { } @Composable -fun KeyCombinationAlreadyInUseErrorMessage(shouldShowErrorMessage: Boolean) { - if (shouldShowErrorMessage) { +fun ErrorMessageContainer(errorMessage: String) { + if (errorMessage.isNotEmpty()) { Box(modifier = Modifier.padding(horizontal = 16.dp).width(332.dp).height(40.dp)) { Text( - text = stringResource(R.string.shortcut_helper_customize_dialog_error_message), + text = errorMessage, style = MaterialTheme.typography.bodyMedium, fontSize = 14.sp, lineHeight = 20.sp, @@ -143,7 +152,7 @@ fun KeyCombinationAlreadyInUseErrorMessage(shouldShowErrorMessage: Boolean) { @Composable fun SelectedKeyCombinationContainer( - shouldShowErrorMessage: Boolean, + shouldShowError: Boolean, onKeyPress: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, ) { @@ -151,7 +160,7 @@ fun SelectedKeyCombinationContainer( val isFocused by interactionSource.collectIsFocusedAsState() val outlineColor = if (!isFocused) MaterialTheme.colorScheme.outline - else if (shouldShowErrorMessage) MaterialTheme.colorScheme.error + else if (shouldShowError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary val focusRequester = remember { FocusRequester() } @@ -179,7 +188,7 @@ fun SelectedKeyCombinationContainer( PressedKeysTextContainer(pressedKeys) } Spacer(modifier = Modifier.weight(1f)) - if (shouldShowErrorMessage) { + if (shouldShowError) { Icon( imageVector = Icons.Default.ErrorOutline, contentDescription = null, 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 552c53dff6a0..adadeebd0769 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 @@ -21,7 +21,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey sealed interface ShortcutCustomizationUiState { data class AddShortcutDialog( val shortcutLabel: String, - val shouldShowErrorMessage: Boolean, + val errorMessage: String = "", val defaultCustomShortcutModifierKey: ShortcutKey.Icon.ResIdIcon, val isDialogShowing: Boolean, val pressedKeys: List<ShortcutKey> = emptyList(), 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 2455ce49033d..8178c6a1c705 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 @@ -16,7 +16,7 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel -import androidx.compose.runtime.mutableStateOf +import android.content.Context import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType @@ -24,10 +24,12 @@ import androidx.compose.ui.input.key.isMetaPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.nativeKeyCode 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.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState +import com.android.systemui.res.R import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow @@ -37,9 +39,10 @@ import kotlinx.coroutines.flow.update class ShortcutCustomizationViewModel @AssistedInject -constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor) { - private val _shortcutBeingCustomized = mutableStateOf<ShortcutCustomizationRequestInfo?>(null) - +constructor( + private val context: Context, + private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, +) { private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) @@ -65,13 +68,12 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.AddShortcutDialog( shortcutLabel = requestInfo.label, - shouldShowErrorMessage = false, defaultCustomShortcutModifierKey = shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(), isDialogShowing = false, pressedKeys = emptyList(), ) - _shortcutBeingCustomized.value = requestInfo + shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) } } } @@ -85,8 +87,8 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn } fun onDialogDismissed() { - _shortcutBeingCustomized.value = null _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive + shortcutCustomizationInteractor.onCustomizationRequested(null) shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) } @@ -98,6 +100,40 @@ constructor(private val shortcutCustomizationInteractor: ShortcutCustomizationIn return false } + suspend fun onSetShortcut() { + val result = shortcutCustomizationInteractor.confirmAndSetShortcutCurrentlyBeingCustomized() + + _shortcutCustomizationUiState.update { uiState -> + when (result) { + ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive + ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION -> { + getUiStateWithErrorMessage( + uiState = uiState, + errorMessage = + context.getString( + R.string.shortcut_customizer_key_combination_in_use_error_message + ), + ) + } + ShortcutCustomizationRequestResult.ERROR_OTHER -> + getUiStateWithErrorMessage( + uiState = uiState, + errorMessage = + context.getString(R.string.shortcut_customizer_generic_error_message), + ) + } + } + } + + private fun getUiStateWithErrorMessage( + uiState: ShortcutCustomizationUiState, + errorMessage: String, + ): ShortcutCustomizationUiState { + return (uiState as? ShortcutCustomizationUiState.AddShortcutDialog)?.copy( + errorMessage = errorMessage + ) ?: uiState + } + private fun updatePressedKeys(keyEvent: KeyEvent) { val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key) val keyCombination = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index c6f01ea9af19..85ab746211d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -18,6 +18,10 @@ 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.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.fakeInputManager import android.os.SystemClock import android.view.KeyEvent.ACTION_DOWN @@ -46,6 +50,7 @@ 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 @@ -123,6 +128,81 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test + fun uiState_becomeInactiveAfterSuccessfullySettingShortcut() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + whenever(inputManager.addCustomInputGesture(any())) + .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) + + openAddShortcutDialogAndSetShortcut() + + assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive) + } + } + + @Test + fun uiState_errorMessage_isEmptyByDefault() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest) + viewModel.onAddShortcutDialogShown() + + assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage) + .isEmpty() + } + } + + @Test + fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + whenever(inputManager.addCustomInputGesture(any())) + .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS) + + openAddShortcutDialogAndSetShortcut() + + assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage) + .isEqualTo( + context.getString( + R.string.shortcut_customizer_key_combination_in_use_error_message + ) + ) + } + } + + @Test + fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationIsReserved() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + whenever(inputManager.addCustomInputGesture(any())) + .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE) + + openAddShortcutDialogAndSetShortcut() + + assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage) + .isEqualTo( + context.getString( + R.string.shortcut_customizer_key_combination_in_use_error_message + ) + ) + } + } + + @Test + fun uiState_errorMessage_isGenericError_whenErrorIsUnknown() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + whenever(inputManager.addCustomInputGesture(any())) + .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER) + + openAddShortcutDialogAndSetShortcut() + + assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage) + .isEqualTo(context.getString(R.string.shortcut_customizer_generic_error_message)) + } + } + + @Test fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) @@ -176,6 +256,16 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } } + private suspend fun openAddShortcutDialogAndSetShortcut() { + viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest) + viewModel.onAddShortcutDialogShown() + + viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) + viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + + viewModel.onSetShortcut() + } + private val keyDownEventWithoutActionKeyPressed = KeyEvent( android.view.KeyEvent( @@ -219,10 +309,16 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { subCategoryLabel = "Standard subcategory", ) + private val allAppsShortcutCustomizationRequest = + ShortcutCustomizationRequestInfo.Add( + label = "Open apps list", + categoryType = ShortcutCategoryType.System, + subCategoryLabel = "System controls", + ) + private val expectedStandardAddShortcutUiState = ShortcutCustomizationUiState.AddShortcutDialog( shortcutLabel = "Standard shortcut", - shouldShowErrorMessage = false, defaultCustomShortcutModifierKey = ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), isDialogShowing = false, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 9cb15c5b816d..721c0b8339db 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -24,6 +24,7 @@ import android.view.windowManager import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository +import com.android.systemui.keyboard.shortcut.data.repository.InputGestureMaps import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper @@ -101,6 +102,11 @@ val Kosmos.defaultShortcutCategoriesRepository by ) } +val Kosmos.inputGestureMaps by + Kosmos.Fixture { + InputGestureMaps(applicationContext) + } + val Kosmos.customShortcutCategoriesRepository by Kosmos.Fixture { CustomShortcutCategoriesRepository( @@ -110,6 +116,7 @@ val Kosmos.customShortcutCategoriesRepository by testDispatcher, shortcutCategoriesUtils, applicationContext, + inputGestureMaps ) } @@ -173,7 +180,10 @@ val Kosmos.shortcutCustomizationViewModelFactory by Kosmos.Fixture { object : ShortcutCustomizationViewModel.Factory { override fun create(): ShortcutCustomizationViewModel { - return ShortcutCustomizationViewModel(shortcutCustomizationInteractor) + return ShortcutCustomizationViewModel( + applicationContext, + shortcutCustomizationInteractor, + ) } } } |