diff options
11 files changed, 450 insertions, 221 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<InputGestureData>() - 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<InputGestureData>(0) customInputGestures = customInputGestures + inputGesture @@ -129,10 +133,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun retrieveCustomInputGestures_retrievesMostRecentData() { testScope.runTest { var customInputGestures = listOf<InputGestureData>() - 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<InputGestureData>) { 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 d9d34f5ace7b..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() @@ -297,24 +325,42 @@ 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) + 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/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<List<InputGestureData>>(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 bd3d46d09f5e..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() } @@ -101,6 +107,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/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/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 9d43c48ee274..66e45056989d 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 @@ -18,14 +18,10 @@ package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke 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.heightIn @@ -40,13 +36,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 @@ -57,6 +55,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 @@ -81,6 +80,7 @@ fun ShortcutCustomizationDialog( onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, onConfirmResetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { @@ -90,6 +90,7 @@ fun ShortcutCustomizationDialog( onShortcutKeyCombinationSelected, onCancel, onConfirmSetShortcut, + onClearSelectedKeyCombination, ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { @@ -111,6 +112,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Title(uiState.shortcutLabel) @@ -126,6 +128,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, onConfirmSetShortcut = onConfirmSetShortcut, + onClearSelectedKeyCombination = onClearSelectedKeyCombination, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -249,10 +252,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 + }, ) } } @@ -264,72 +268,82 @@ private fun SelectedKeyCombinationContainer( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> 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.Backspace -> { + onClearSelectedKeyCombination() + 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<ShortcutKey>) { - 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<ShortcutKey>) { + 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) + } } } } @@ -346,7 +360,7 @@ private fun ShortcutKeySeparator() { } @Composable -private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) { +private fun ShortcutIconKey(key: ShortcutKey.Icon) { Icon( painter = when (key) { @@ -354,7 +368,7 @@ 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, ) } @@ -405,7 +419,7 @@ private fun Description(text: String) { .width(316.dp) .wrapContentSize(Alignment.Center), color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } @@ -473,3 +487,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, + ) +} 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..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>(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) @@ -92,7 +79,7 @@ constructor( fun onDialogDismissed() { _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive shortcutCustomizationInteractor.onCustomizationRequested(null) - shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + clearSelectedKeyCombination() } fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { @@ -112,7 +99,6 @@ constructor( suspend fun onSetShortcut() { val result = shortcutCustomizationInteractor.confirmAndSetShortcutCurrentlyBeingCustomized() - _shortcutCustomizationUiState.update { uiState -> when (result) { ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive @@ -158,6 +144,10 @@ constructor( } } + fun clearSelectedKeyCombination() { + shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + } + private fun getUiStateWithErrorMessage( uiState: ShortcutCustomizationUiState, errorMessage: String, @@ -180,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<ShortcutKey>): 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<ShortcutKey>) = + keys.filter { it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() } + companion object { private val SUPPORTED_MODIFIERS = listOf( 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<Trigger, InputGestureData> = 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<InputGestureData>(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<InputGestureData>(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<InputManager> { - whenever(getInputDevice(anyInt())).thenAnswer { invocation -> + on { getInputGesture(any()) } + .then { + val trigger = it.getArgument<Trigger>(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) |