diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8c1ab5cfbd..3c4e9f7e6d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,7 +21,7 @@ jobs: uses: android-actions/setup-android@v2 - name: Unit tests - run: bash ./gradlew testDebugUnitTest + run: bash ./gradlew testFreeDebugUnitTest style: name: Code style check diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 2ced70cf71..cac3214aa2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -27,7 +27,7 @@ jobs: uses: android-actions/setup-android@v2 - name: Unit tests - run: bash ./gradlew testDebugUnitTest + run: bash ./gradlew testFreeDebugUnitTest style: name: Code style check diff --git a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt index 8516e81376..7eaab4142d 100644 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt +++ b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt @@ -16,12 +16,14 @@ import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.KeyMap import io.github.sds100.keymapper.mappings.keymaps.KeyMapAction import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.Result @@ -109,6 +111,7 @@ class KeyMapController( val parallelTriggerActionPerformers = mutableMapOf() val parallelTriggerModifierKeyIndices = mutableListOf>() + val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf() // Only process key maps that can be triggered val validKeyMaps = value.filter { keyMap -> @@ -123,6 +126,10 @@ class KeyMapController( keyMap.trigger.keys .mapNotNull { it as? KeyCodeTriggerKey } .forEachIndexed { keyIndex, key -> + if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && key.consumeEvent) { + triggerKeysThatSendRepeatedKeyEvents.add(key) + } + if (keyMap.trigger.mode == TriggerMode.Sequence && key.clickType == ClickType.LONG_PRESS ) { @@ -378,6 +385,8 @@ class KeyMapController( this.parallelTriggerActionPerformers = parallelTriggerActionPerformers this.sequenceTriggerActionPerformers = sequenceTriggerActionPerformers + this.triggerKeysThatSendRepeatedKeyEvents = triggerKeysThatSendRepeatedKeyEvents + reset() } @@ -505,6 +514,18 @@ class KeyMapController( */ private val parallelTriggerLongPressJobs: SparseArrayCompat = SparseArrayCompat() + /** + * Keys that are detected through an input method will potentially send multiple DOWN key events + * with incremented repeatCounts, such as DPAD buttons. These repeated DOWN key events must + * all be consumed and ignored because the UP key event is only sent once at the end. The action + * must not be executed for each repeat. The user may potentially have many hundreds + * of trigger keys so to reduce latency this set caches which keys + * will be affected by this behavior. + * + * NOTE: This only contains the trigger keys that are flagged to consume the key event. + */ + private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() + private var parallelTriggerActionPerformers: Map = emptyMap() private var sequenceTriggerActionPerformers: Map = @@ -548,6 +569,8 @@ class KeyMapController( PreferenceDefaults.FORCE_VIBRATE, ) + private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + init { coroutineScope.launch { useCase.allKeyMapList.collectLatest { keyMapList -> @@ -557,25 +580,51 @@ class KeyMapController( } } + fun onMotionEvent(event: MyMotionEvent): Boolean { + if (!detectKeyMaps) return false + + // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad + // Some controllers send motion events as well as key events when DPAD buttons + // are pressed, while others just send key events. + // The motion events must be consumed but this means the following key events are also + // consumed so one must rely on converting these motion events oneself. + + val convertedKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) + + var consume = false + + for (keyEvent in convertedKeyEvents) { + if (onKeyEventPostFilter(keyEvent)) { + consume = true + } + } + + return consume + } + /** * @return whether to consume the [KeyEvent]. */ - fun onKeyEvent( - keyCode: Int, - action: Int, - metaState: Int, - scanCode: Int = 0, - device: InputDeviceInfo?, - ): Boolean { + fun onKeyEvent(keyEvent: MyKeyEvent): Boolean { if (!detectKeyMaps) return false + if (dpadMotionEventTracker.onKeyEvent(keyEvent)) { + return true + } + + val device = keyEvent.device + if (device != null) { if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { return false } } - metaStateFromKeyEvent = metaState + return onKeyEventPostFilter(keyEvent) + } + + private fun onKeyEventPostFilter(keyEvent: MyKeyEvent): Boolean { + metaStateFromKeyEvent = keyEvent.metaState // remove the metastate from any modifier keys that remapped and are pressed down for (it in parallelTriggerModifierKeyIndices) { @@ -589,24 +638,30 @@ class KeyMapController( if (parallelTriggerEventsAwaitingRelease[triggerIndex][eventIndex]) { metaStateFromKeyEvent = - metaStateFromKeyEvent.minusFlag(KeyEventUtils.modifierKeycodeToMetaState(key.keyCode)) + metaStateFromKeyEvent.minusFlag(InputEventUtils.modifierKeycodeToMetaState(key.keyCode)) } } val event = - if (device != null && device.isExternal) { - Event(keyCode, null, device.descriptor) + if (keyEvent.device != null && keyEvent.device.isExternal) { + Event(keyEvent.keyCode, null, keyEvent.device.descriptor, keyEvent.repeatCount) } else { Event( - keyCode, - null, - null, + keyCode = keyEvent.keyCode, + clickType = null, + descriptor = null, + repeatCount = keyEvent.repeatCount, ) } - when (action) { - KeyEvent.ACTION_DOWN -> return onKeyDown(event, device?.id ?: 0, scanCode) - KeyEvent.ACTION_UP -> return onKeyUp(event, device?.id ?: 0, scanCode) + when (keyEvent.action) { + KeyEvent.ACTION_DOWN -> return onKeyDown( + event, + keyEvent.device?.id ?: 0, + keyEvent.scanCode, + ) + + KeyEvent.ACTION_UP -> return onKeyUp(event, keyEvent.device?.id ?: 0, keyEvent.scanCode) } return false @@ -616,6 +671,20 @@ class KeyMapController( * @return whether to consume the [KeyEvent]. */ private fun onKeyDown(event: Event, deviceId: Int, scanCode: Int): Boolean { + // Must come before saving the event down time because + // there is no corresponding up key event for key events with a repeat count > 0 + if (event.repeatCount > 0) { + val matchingTriggerKey = triggerKeysThatSendRepeatedKeyEvents.any { + it.matchesEvent(event.withShortPress) || + it.matchesEvent(event.withLongPress) || + it.matchesEvent(event.withDoublePress) + } + + if (matchingTriggerKey) { + return true + } + } + eventDownTimeMap[event] = currentTime var consumeEvent = false @@ -790,7 +859,7 @@ class KeyMapController( if (isModifierKey(actionKeyCode)) { val actionMetaState = - KeyEventUtils.modifierKeycodeToMetaState(actionKeyCode) + InputEventUtils.modifierKeycodeToMetaState(actionKeyCode) metaStateFromActions = metaStateFromActions.withFlag(actionMetaState) } @@ -1172,7 +1241,7 @@ class KeyMapController( actionMap[actionKey]?.let { action -> if (action.data is ActionData.InputKeyEvent && isModifierKey(action.data.keyCode)) { val actionMetaState = - KeyEventUtils.modifierKeycodeToMetaState(action.data.keyCode) + InputEventUtils.modifierKeycodeToMetaState(action.data.keyCode) metaStateFromActionsToRemove = metaStateFromActionsToRemove.withFlag(actionMetaState) @@ -1430,8 +1499,7 @@ class KeyMapController( return detectedTriggerIndexes.isNotEmpty() } - private fun encodeActionList(actions: List): IntArray = - actions.map { getActionKey(it) }.toIntArray() + private fun encodeActionList(actions: List): IntArray = actions.map { getActionKey(it) }.toIntArray() /** * @return the key for the action in [actionMap]. Returns -1 if the [action] can't be found. @@ -1535,17 +1603,13 @@ class KeyMapController( } } - private fun longPressDelay(trigger: Trigger): Long = - trigger.longPressDelay?.toLong() ?: defaultLongPressDelay.value + private fun longPressDelay(trigger: Trigger): Long = trigger.longPressDelay?.toLong() ?: defaultLongPressDelay.value - private fun doublePressTimeout(trigger: Trigger): Long = - trigger.doublePressDelay?.toLong() ?: defaultDoublePressDelay.value + private fun doublePressTimeout(trigger: Trigger): Long = trigger.doublePressDelay?.toLong() ?: defaultDoublePressDelay.value - private fun vibrateDuration(trigger: Trigger): Long = - trigger.vibrateDuration?.toLong() ?: defaultVibrateDuration.value + private fun vibrateDuration(trigger: Trigger): Long = trigger.vibrateDuration?.toLong() ?: defaultVibrateDuration.value - private fun sequenceTriggerTimeout(trigger: Trigger): Long = - trigger.sequenceTriggerTimeout?.toLong() ?: defaultSequenceTriggerTimeout.value + private fun sequenceTriggerTimeout(trigger: Trigger): Long = trigger.sequenceTriggerTimeout?.toLong() ?: defaultSequenceTriggerTimeout.value private fun setActionMapAndOptions(actions: Set) { var key = 0 @@ -1594,6 +1658,7 @@ class KeyMapController( * null if not an external device */ val descriptor: String?, + val repeatCount: Int, ) private data class TriggerKeyLocation(val triggerIndex: Int, val keyIndex: Int) diff --git a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt index 8f46a6afcd..0507cd4838 100644 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt +++ b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt @@ -16,7 +16,8 @@ class ConfigTriggerViewModel( createKeyMapShortcut: CreateKeyMapShortcutUseCase, displayKeyMap: DisplayKeyMapUseCase, resourceProvider: ResourceProvider, - private val purchasingManager: PurchasingManager, + purchasingManager: PurchasingManager, + setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, ) : BaseConfigTriggerViewModel( coroutineScope, onboarding, @@ -24,5 +25,6 @@ class ConfigTriggerViewModel( recordTrigger, createKeyMapShortcut, displayKeyMap, + setupGuiKeyboardUseCase, resourceProvider, ) diff --git a/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayService.aidl b/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayService.aidl index 7dc44f7899..21c461d278 100644 --- a/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayService.aidl +++ b/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayService.aidl @@ -1,29 +1,37 @@ package io.github.sds100.keymapper.api; import android.view.KeyEvent; +import android.view.MotionEvent; import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback; interface IKeyEventRelayService { /** * Send a key event to the target package that is registered with * a callback. + * + * callbackID parameter was added in Key Mapper 2.8. */ - boolean sendKeyEvent(in KeyEvent event, in String targetPackageName); + boolean sendKeyEvent(in KeyEvent event, in String targetPackageName, in String callbackId); /** * Register a callback to receive key events from this relay service. The service * checks the process uid of the caller to this method and only permits certain applications * from connecting. + * + * id parameter was added in Key Mapper 2.8. */ - void registerCallback(IKeyEventRelayServiceCallback client); + void registerCallback(IKeyEventRelayServiceCallback client, String id); /** * Unregister all the callbacks associated with the calling package from this relay service. + * + * callbackID parameter was added in Key Mapper 2.8. */ - void unregisterAllCallbacks(); + void unregisterCallback(String callbackId); /** - * Unregister a callback to receive key events from this relay service. + * Send a motion event to the target package that is registered with + * a callback. */ - void unregisterCallback(IKeyEventRelayServiceCallback client); + boolean sendMotionEvent(in MotionEvent event, in String targetPackageName, in String callbackId); } \ No newline at end of file diff --git a/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayServiceCallback.aidl b/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayServiceCallback.aidl index 24fc46d25c..1086741930 100644 --- a/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayServiceCallback.aidl +++ b/app/src/main/aidl/io/github/sds100/keymapper/api/IKeyEventRelayServiceCallback.aidl @@ -1,7 +1,9 @@ package io.github.sds100.keymapper.api; import android.view.KeyEvent; +import android.view.MotionEvent; interface IKeyEventRelayServiceCallback { - boolean onKeyEvent(in KeyEvent event, in String sourcePackageName); + boolean onKeyEvent(in KeyEvent event); + boolean onMotionEvent(in MotionEvent event); } \ No newline at end of file diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 1b4bf36252..a73eac8a3b 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1 +1,3 @@ -New trigger! 🎉 You can now trigger your key maps from the Bixby button, Power button, a button on your headset, or any other way your device can launch the assistant. \ No newline at end of file +New trigger! 🎉 You can now remap the DPAD buttons on your game controller! + +Joysticks and triggers are coming soon... 😉 \ No newline at end of file diff --git a/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt index efc0b158db..464c135d5d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper import android.content.res.Configuration import android.os.Bundle +import android.view.KeyEvent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil @@ -11,6 +12,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import io.github.sds100.keymapper.Constants.PACKAGE_NAME import io.github.sds100.keymapper.databinding.ActivityMainBinding +import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerController import io.github.sds100.keymapper.system.permissions.RequestPermissionDelegate import io.github.sds100.keymapper.util.launchRepeatOnLifecycle import io.github.sds100.keymapper.util.ui.showPopups @@ -40,6 +42,9 @@ abstract class BaseMainActivity : AppCompatActivity() { get() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK private lateinit var requestPermissionDelegate: RequestPermissionDelegate + private val recordTriggerController: RecordTriggerController by lazy { + (applicationContext as KeyMapperApp).recordTriggerController + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -101,4 +106,17 @@ abstract class BaseMainActivity : AppCompatActivity() { viewModel.previousNightMode = currentNightMode super.onDestroy() } + + // Use this method rather than onKeyDown and onKeyUp so that we process + // the key events before any other Views. onKeyDown are called after being sent to the Views. + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val consume = recordTriggerController.onRecordKeyFromActivity(event) + + return if (consume) { + true + } else { + // IMPORTANT! return super so that the back navigation button still works. + super.dispatchKeyEvent(event) + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt index 7950a5dad0..a75720a4ac 100644 --- a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt +++ b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt @@ -30,7 +30,7 @@ import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.accessibility.MyAccessibilityService import io.github.sds100.keymapper.system.apps.DisplayAppsUseCase import io.github.sds100.keymapper.system.apps.DisplayAppsUseCaseImpl -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeMessengerImpl +import io.github.sds100.keymapper.system.inputmethod.ImeInputEventInjectorImpl import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCaseImpl import io.github.sds100.keymapper.system.inputmethod.ToggleCompatibleImeUseCaseImpl @@ -40,10 +40,9 @@ import io.github.sds100.keymapper.system.inputmethod.ToggleCompatibleImeUseCaseI */ object UseCases { - fun displayPackages(ctx: Context): DisplayAppsUseCase = - DisplayAppsUseCaseImpl( - ServiceLocator.packageManagerAdapter(ctx), - ) + fun displayPackages(ctx: Context): DisplayAppsUseCase = DisplayAppsUseCaseImpl( + ServiceLocator.packageManagerAdapter(ctx), + ) fun displayKeyMap(ctx: Context): DisplayKeyMapUseCase = DisplayKeyMapUseCaseImpl( ServiceLocator.permissionAdapter(ctx), @@ -59,16 +58,15 @@ object UseCases { ServiceLocator.settingsRepository(ctx), ) - fun displaySimpleMapping(ctx: Context): DisplaySimpleMappingUseCase = - DisplaySimpleMappingUseCaseImpl( - ServiceLocator.packageManagerAdapter(ctx), - ServiceLocator.permissionAdapter(ctx), - ServiceLocator.inputMethodAdapter(ctx), - ServiceLocator.settingsRepository(ctx), - ServiceLocator.accessibilityServiceAdapter(ctx), - getActionError(ctx), - getConstraintError(ctx), - ) + fun displaySimpleMapping(ctx: Context): DisplaySimpleMappingUseCase = DisplaySimpleMappingUseCaseImpl( + ServiceLocator.packageManagerAdapter(ctx), + ServiceLocator.permissionAdapter(ctx), + ServiceLocator.inputMethodAdapter(ctx), + ServiceLocator.settingsRepository(ctx), + ServiceLocator.accessibilityServiceAdapter(ctx), + getActionError(ctx), + getConstraintError(ctx), + ) fun getActionError(ctx: Context) = GetActionErrorUseCaseImpl( ServiceLocator.packageManagerAdapter(ctx), @@ -102,33 +100,27 @@ object UseCases { ServiceLocator.resourceProvider(ctx), ) - fun isActionSupported(ctx: Context) = - IsActionSupportedUseCaseImpl(ServiceLocator.systemFeatureAdapter(ctx)) + fun isActionSupported(ctx: Context) = IsActionSupportedUseCaseImpl(ServiceLocator.systemFeatureAdapter(ctx)) - fun fingerprintGesturesSupported(ctx: Context) = - AreFingerprintGesturesSupportedUseCaseImpl(ServiceLocator.settingsRepository(ctx)) + fun fingerprintGesturesSupported(ctx: Context) = AreFingerprintGesturesSupportedUseCaseImpl(ServiceLocator.settingsRepository(ctx)) - fun pauseMappings(ctx: Context) = - PauseMappingsUseCaseImpl( - ServiceLocator.settingsRepository(ctx), - ServiceLocator.mediaAdapter(ctx), - ) + fun pauseMappings(ctx: Context) = PauseMappingsUseCaseImpl( + ServiceLocator.settingsRepository(ctx), + ServiceLocator.mediaAdapter(ctx), + ) - fun showImePicker(ctx: Context): ShowInputMethodPickerUseCase = - ShowInputMethodPickerUseCaseImpl( - ServiceLocator.inputMethodAdapter(ctx), - ) + fun showImePicker(ctx: Context): ShowInputMethodPickerUseCase = ShowInputMethodPickerUseCaseImpl( + ServiceLocator.inputMethodAdapter(ctx), + ) - fun controlAccessibilityService(ctx: Context): ControlAccessibilityServiceUseCase = - ControlAccessibilityServiceUseCaseImpl( - ServiceLocator.accessibilityServiceAdapter(ctx), - ServiceLocator.permissionAdapter(ctx), - ) + fun controlAccessibilityService(ctx: Context): ControlAccessibilityServiceUseCase = ControlAccessibilityServiceUseCaseImpl( + ServiceLocator.accessibilityServiceAdapter(ctx), + ServiceLocator.permissionAdapter(ctx), + ) - fun toggleCompatibleIme(ctx: Context) = - ToggleCompatibleImeUseCaseImpl( - ServiceLocator.inputMethodAdapter(ctx), - ) + fun toggleCompatibleIme(ctx: Context) = ToggleCompatibleImeUseCaseImpl( + ServiceLocator.inputMethodAdapter(ctx), + ) fun detectConstraints(service: MyAccessibilityService) = DetectConstraintsUseCaseImpl( service, @@ -147,39 +139,38 @@ object UseCases { ctx: Context, service: IAccessibilityService, keyEventRelayService: KeyEventRelayServiceWrapper, - ) = - PerformActionsUseCaseImpl( - (ctx.applicationContext as KeyMapperApp).appCoroutineScope, - service, - ServiceLocator.inputMethodAdapter(ctx), - ServiceLocator.fileAdapter(ctx), - ServiceLocator.suAdapter(ctx), - Shell, - ServiceLocator.intentAdapter(ctx), - getActionError(ctx), - keyMapperImeMessenger(ctx, keyEventRelayService), - ShizukuInputEventInjector(), - ServiceLocator.packageManagerAdapter(ctx), - ServiceLocator.appShortcutAdapter(ctx), - ServiceLocator.popupMessageAdapter(ctx), - ServiceLocator.devicesAdapter(ctx), - ServiceLocator.phoneAdapter(ctx), - ServiceLocator.audioAdapter(ctx), - ServiceLocator.cameraAdapter(ctx), - ServiceLocator.displayAdapter(ctx), - ServiceLocator.lockScreenAdapter(ctx), - ServiceLocator.mediaAdapter(ctx), - ServiceLocator.airplaneModeAdapter(ctx), - ServiceLocator.networkAdapter(ctx), - ServiceLocator.bluetoothAdapter(ctx), - ServiceLocator.nfcAdapter(ctx), - ServiceLocator.openUrlAdapter(ctx), - ServiceLocator.resourceProvider(ctx), - ServiceLocator.settingsRepository(ctx), - ServiceLocator.soundsManager(ctx), - ServiceLocator.permissionAdapter(ctx), - ServiceLocator.notificationReceiverAdapter(ctx), - ) + ) = PerformActionsUseCaseImpl( + (ctx.applicationContext as KeyMapperApp).appCoroutineScope, + service, + ServiceLocator.inputMethodAdapter(ctx), + ServiceLocator.fileAdapter(ctx), + ServiceLocator.suAdapter(ctx), + Shell, + ServiceLocator.intentAdapter(ctx), + getActionError(ctx), + keyMapperImeMessenger(ctx, keyEventRelayService), + ShizukuInputEventInjector(), + ServiceLocator.packageManagerAdapter(ctx), + ServiceLocator.appShortcutAdapter(ctx), + ServiceLocator.popupMessageAdapter(ctx), + ServiceLocator.devicesAdapter(ctx), + ServiceLocator.phoneAdapter(ctx), + ServiceLocator.audioAdapter(ctx), + ServiceLocator.cameraAdapter(ctx), + ServiceLocator.displayAdapter(ctx), + ServiceLocator.lockScreenAdapter(ctx), + ServiceLocator.mediaAdapter(ctx), + ServiceLocator.airplaneModeAdapter(ctx), + ServiceLocator.networkAdapter(ctx), + ServiceLocator.bluetoothAdapter(ctx), + ServiceLocator.nfcAdapter(ctx), + ServiceLocator.openUrlAdapter(ctx), + ServiceLocator.resourceProvider(ctx), + ServiceLocator.settingsRepository(ctx), + ServiceLocator.soundsManager(ctx), + ServiceLocator.permissionAdapter(ctx), + ServiceLocator.notificationReceiverAdapter(ctx), + ) fun detectMappings(ctx: Context) = DetectMappingUseCaseImpl( ServiceLocator.vibratorAdapter(ctx), @@ -203,8 +194,6 @@ object UseCases { service, ShizukuInputEventInjector(), ServiceLocator.permissionAdapter(ctx), - ServiceLocator.phoneAdapter(ctx), - ServiceLocator.inputMethodAdapter(ctx), ) fun detectFingerprintMaps(ctx: Context) = DetectFingerprintMapsUseCaseImpl( @@ -213,12 +202,11 @@ object UseCases { detectMappings(ctx), ) - fun rerouteKeyEvents(ctx: Context, keyEventRelayService: KeyEventRelayServiceWrapper) = - RerouteKeyEventsUseCaseImpl( - ServiceLocator.inputMethodAdapter(ctx), - keyMapperImeMessenger(ctx, keyEventRelayService), - ServiceLocator.settingsRepository(ctx), - ) + fun rerouteKeyEvents(ctx: Context, keyEventRelayService: KeyEventRelayServiceWrapper) = RerouteKeyEventsUseCaseImpl( + ServiceLocator.inputMethodAdapter(ctx), + keyMapperImeMessenger(ctx, keyEventRelayService), + ServiceLocator.settingsRepository(ctx), + ) fun createAction(ctx: Context) = CreateActionUseCaseImpl( ServiceLocator.inputMethodAdapter(ctx), @@ -227,10 +215,9 @@ object UseCases { private fun keyMapperImeMessenger( ctx: Context, keyEventRelayService: KeyEventRelayServiceWrapper, - ) = - KeyMapperImeMessengerImpl( - ctx, - keyEventRelayService, - ServiceLocator.inputMethodAdapter(ctx), - ) + ) = ImeInputEventInjectorImpl( + ctx, + keyEventRelayService, + ServiceLocator.inputMethodAdapter(ctx), + ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/BaseActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/BaseActionUiHelper.kt index 42bbb4570e..a2119e4fe5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/BaseActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/BaseActionUiHelper.kt @@ -8,8 +8,8 @@ import io.github.sds100.keymapper.mappings.Mapping import io.github.sds100.keymapper.system.camera.CameraLensUtils import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.display.OrientationUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.intents.IntentTarget -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils import io.github.sds100.keymapper.system.volume.DndModeUtils import io.github.sds100.keymapper.system.volume.RingerModeUtils import io.github.sds100.keymapper.system.volume.VolumeStreamUtils @@ -31,435 +31,434 @@ abstract class BaseActionUiHelper, A : Action>( ResourceProvider by resourceProvider, DisplayActionUseCase by displayActionUseCase { - override fun getTitle(action: ActionData, showDeviceDescriptors: Boolean): String = - when (action) { - is ActionData.App -> - getAppName(action.packageName).handle( - onSuccess = { getString(R.string.description_open_app, it) }, - onError = { getString(R.string.description_open_app, action.packageName) }, - ) + override fun getTitle(action: ActionData, showDeviceDescriptors: Boolean): String = when (action) { + is ActionData.App -> + getAppName(action.packageName).handle( + onSuccess = { getString(R.string.description_open_app, it) }, + onError = { getString(R.string.description_open_app, action.packageName) }, + ) - is ActionData.AppShortcut -> action.shortcutTitle + is ActionData.AppShortcut -> action.shortcutTitle - is ActionData.InputKeyEvent -> { - val keyCodeString = if (action.keyCode > KeyEvent.getMaxKeyCode()) { - "Key Code ${action.keyCode}" - } else { - KeyEvent.keyCodeToString(action.keyCode) - } + is ActionData.InputKeyEvent -> { + val keyCodeString = if (action.keyCode > KeyEvent.getMaxKeyCode()) { + "Key Code ${action.keyCode}" + } else { + KeyEvent.keyCodeToString(action.keyCode) + } - // only a key code can be inputted through the shell - if (action.useShell) { - getString(R.string.description_keyevent_through_shell, keyCodeString) - } else { - val metaStateString = buildString { - KeyEventUtils.MODIFIER_LABELS.entries.forEach { - val modifier = it.key - val labelRes = it.value - - if (action.metaState.hasFlag(modifier)) { - append("${getString(labelRes)} + ") - } - } - } + // only a key code can be inputted through the shell + if (action.useShell) { + getString(R.string.description_keyevent_through_shell, keyCodeString) + } else { + val metaStateString = buildString { + InputEventUtils.MODIFIER_LABELS.entries.forEach { + val modifier = it.key + val labelRes = it.value - if (action.device != null) { - val name = if (action.device.name.isBlank()) { - getString(R.string.unknown_device_name) - } else { - action.device.name + if (action.metaState.hasFlag(modifier)) { + append("${getString(labelRes)} + ") } + } + } - val nameToShow = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - action.device.descriptor, - name, - ) - } else { - name - } + if (action.device != null) { + val name = if (action.device.name.isBlank()) { + getString(R.string.unknown_device_name) + } else { + action.device.name + } - getString( - R.string.description_keyevent_from_device, - arrayOf(metaStateString, keyCodeString, nameToShow), + val nameToShow = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + action.device.descriptor, + name, ) } else { - getString( - R.string.description_keyevent, - args = arrayOf(metaStateString, keyCodeString), - ) + name } - } - } - - is ActionData.DoNotDisturb.Enable -> { - val dndModeString = getString(DndModeUtils.getLabel(action.dndMode)) - getString( - R.string.action_enable_dnd_mode_formatted, - dndModeString, - ) - } - is ActionData.DoNotDisturb.Toggle -> { - val dndModeString = getString(DndModeUtils.getLabel(action.dndMode)) - getString( - R.string.action_toggle_dnd_mode_formatted, - dndModeString, - ) + getString( + R.string.description_keyevent_from_device, + arrayOf(metaStateString, keyCodeString, nameToShow), + ) + } else { + getString( + R.string.description_keyevent, + args = arrayOf(metaStateString, keyCodeString), + ) + } } + } - ActionData.DoNotDisturb.Disable -> getString(R.string.action_disable_dnd_mode) - - is ActionData.Volume.SetRingerMode -> { - val ringerModeString = getString(RingerModeUtils.getLabel(action.ringerMode)) - - getString(R.string.action_change_ringer_mode_formatted, ringerModeString) - } + is ActionData.DoNotDisturb.Enable -> { + val dndModeString = getString(DndModeUtils.getLabel(action.dndMode)) + getString( + R.string.action_enable_dnd_mode_formatted, + dndModeString, + ) + } - is ActionData.Volume -> { - var hasShowVolumeUiFlag = false - val string: String + is ActionData.DoNotDisturb.Toggle -> { + val dndModeString = getString(DndModeUtils.getLabel(action.dndMode)) + getString( + R.string.action_toggle_dnd_mode_formatted, + dndModeString, + ) + } - when (action) { - is ActionData.Volume.Stream -> { - val streamString = getString( - VolumeStreamUtils.getLabel(action.volumeStream), - ) + ActionData.DoNotDisturb.Disable -> getString(R.string.action_disable_dnd_mode) - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } + is ActionData.Volume.SetRingerMode -> { + val ringerModeString = getString(RingerModeUtils.getLabel(action.ringerMode)) - string = when (action) { - is ActionData.Volume.Stream.Decrease -> getString( - R.string.action_decrease_stream_formatted, - streamString, - ) + getString(R.string.action_change_ringer_mode_formatted, ringerModeString) + } - is ActionData.Volume.Stream.Increase -> getString( - R.string.action_increase_stream_formatted, - streamString, - ) - } - } + is ActionData.Volume -> { + var hasShowVolumeUiFlag = false + val string: String - is ActionData.Volume.Down -> { - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } + when (action) { + is ActionData.Volume.Stream -> { + val streamString = getString( + VolumeStreamUtils.getLabel(action.volumeStream), + ) - string = getString(R.string.action_volume_down) + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - is ActionData.Volume.Mute -> { - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } + string = when (action) { + is ActionData.Volume.Stream.Decrease -> getString( + R.string.action_decrease_stream_formatted, + streamString, + ) - string = getString(R.string.action_volume_mute) + is ActionData.Volume.Stream.Increase -> getString( + R.string.action_increase_stream_formatted, + streamString, + ) } + } - is ActionData.Volume.ToggleMute -> { - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } - - string = getString(R.string.action_toggle_mute) + is ActionData.Volume.Down -> { + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - is ActionData.Volume.UnMute -> { - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } + string = getString(R.string.action_volume_down) + } - string = getString(R.string.action_volume_unmute) + is ActionData.Volume.Mute -> { + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - is ActionData.Volume.Up -> { - if (action.showVolumeUi) { - hasShowVolumeUiFlag = true - } + string = getString(R.string.action_volume_mute) + } - string = getString(R.string.action_volume_up) + is ActionData.Volume.ToggleMute -> { + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - ActionData.Volume.CycleRingerMode -> { - string = getString(R.string.action_cycle_ringer_mode) - } + string = getString(R.string.action_toggle_mute) + } - ActionData.Volume.CycleVibrateRing -> { - string = getString(R.string.action_cycle_vibrate_ring) + is ActionData.Volume.UnMute -> { + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - is ActionData.Volume.SetRingerMode -> { - val ringerModeString = - getString(RingerModeUtils.getLabel(action.ringerMode)) + string = getString(R.string.action_volume_unmute) + } - string = getString( - R.string.action_change_ringer_mode_formatted, - ringerModeString, - ) + is ActionData.Volume.Up -> { + if (action.showVolumeUi) { + hasShowVolumeUiFlag = true } - ActionData.Volume.ShowDialog -> { - string = getString(R.string.action_volume_show_dialog) - } + string = getString(R.string.action_volume_up) } - if (hasShowVolumeUiFlag) { - val midDot = getString(R.string.middot) - "$string $midDot ${getString(R.string.flag_show_volume_dialog)}" - } else { - string + ActionData.Volume.CycleRingerMode -> { + string = getString(R.string.action_cycle_ringer_mode) } - } - - is ActionData.ControlMediaForApp -> - getAppName(action.packageName).handle( - onSuccess = { appName -> - val resId = when (action) { - is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package_formatted - is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package_formatted - is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package_formatted - is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package_formatted - is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package_formatted - is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted - is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted - } - getString(resId, appName) - }, - onError = { - val resId = when (action) { - is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package - is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package - is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package - is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package - is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package - is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package - is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package - } + ActionData.Volume.CycleVibrateRing -> { + string = getString(R.string.action_cycle_vibrate_ring) + } - getString(resId) - }, - ) + is ActionData.Volume.SetRingerMode -> { + val ringerModeString = + getString(RingerModeUtils.getLabel(action.ringerMode)) - is ActionData.Flashlight -> { - val resId = when (action) { - is ActionData.Flashlight.Toggle -> R.string.action_toggle_flashlight_formatted - is ActionData.Flashlight.Enable -> R.string.action_enable_flashlight_formatted - is ActionData.Flashlight.Disable -> R.string.action_disable_flashlight_formatted + string = getString( + R.string.action_change_ringer_mode_formatted, + ringerModeString, + ) } - val lensString = getString(CameraLensUtils.getLabel(action.lens)) + ActionData.Volume.ShowDialog -> { + string = getString(R.string.action_volume_show_dialog) + } + } - getString(resId, lensString) + if (hasShowVolumeUiFlag) { + val midDot = getString(R.string.middot) + "$string $midDot ${getString(R.string.flag_show_volume_dialog)}" + } else { + string } + } + + is ActionData.ControlMediaForApp -> + getAppName(action.packageName).handle( + onSuccess = { appName -> + val resId = when (action) { + is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package_formatted + is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package_formatted + is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package_formatted + is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package_formatted + is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package_formatted + is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted + is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted + } - is ActionData.SwitchKeyboard -> getInputMethodLabel(action.imeId).handle( - onSuccess = { getString(R.string.action_switch_keyboard_formatted, it) }, + getString(resId, appName) + }, onError = { - getString( - R.string.action_switch_keyboard_formatted, - action.savedImeName, - ) + val resId = when (action) { + is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package + is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package + is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package + is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package + is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package + is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package + is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package + } + + getString(resId) }, ) - is ActionData.Intent -> { - val resId = when (action.target) { - IntentTarget.ACTIVITY -> R.string.action_title_intent_start_activity - IntentTarget.BROADCAST_RECEIVER -> R.string.action_title_intent_send_broadcast - IntentTarget.SERVICE -> R.string.action_title_intent_start_service - } - - getString(resId, action.description) + is ActionData.Flashlight -> { + val resId = when (action) { + is ActionData.Flashlight.Toggle -> R.string.action_toggle_flashlight_formatted + is ActionData.Flashlight.Enable -> R.string.action_enable_flashlight_formatted + is ActionData.Flashlight.Disable -> R.string.action_disable_flashlight_formatted } - is ActionData.PhoneCall -> getString(R.string.description_phone_call, action.number) + val lensString = getString(CameraLensUtils.getLabel(action.lens)) - is ActionData.TapScreen -> if (action.description.isNullOrBlank()) { - getString( - R.string.description_tap_coordinate_default, - arrayOf(action.x, action.y), - ) - } else { - getString( - R.string.description_tap_coordinate_with_description, - arrayOf(action.x, action.y, action.description), - ) - } + getString(resId, lensString) + } - is ActionData.SwipeScreen -> if (action.description.isNullOrBlank()) { + is ActionData.SwitchKeyboard -> getInputMethodLabel(action.imeId).handle( + onSuccess = { getString(R.string.action_switch_keyboard_formatted, it) }, + onError = { getString( - R.string.description_swipe_coordinate_default, - arrayOf( - action.fingerCount, - action.xStart, - action.yStart, - action.xEnd, - action.yEnd, - action.duration, - ), - ) - } else { - getString( - R.string.description_swipe_coordinate_with_description, - arrayOf( - action.fingerCount, - action.xStart, - action.yStart, - action.xEnd, - action.yEnd, - action.duration, - action.description, - ), + R.string.action_switch_keyboard_formatted, + action.savedImeName, ) + }, + ) + + is ActionData.Intent -> { + val resId = when (action.target) { + IntentTarget.ACTIVITY -> R.string.action_title_intent_start_activity + IntentTarget.BROADCAST_RECEIVER -> R.string.action_title_intent_send_broadcast + IntentTarget.SERVICE -> R.string.action_title_intent_start_service } - is ActionData.PinchScreen -> if (action.description.isNullOrBlank()) { - val pinchTypeDisplayName = if (action.pinchType == PinchScreenType.PINCH_IN) { - getString(R.string.hint_coordinate_type_pinch_in) - } else { - getString(R.string.hint_coordinate_type_pinch_out) - } + getString(resId, action.description) + } - getString( - R.string.description_pinch_coordinate_default, - arrayOf( - pinchTypeDisplayName, - action.fingerCount, - action.x, - action.y, - action.distance, - action.duration, - ), - ) + is ActionData.PhoneCall -> getString(R.string.description_phone_call, action.number) + + is ActionData.TapScreen -> if (action.description.isNullOrBlank()) { + getString( + R.string.description_tap_coordinate_default, + arrayOf(action.x, action.y), + ) + } else { + getString( + R.string.description_tap_coordinate_with_description, + arrayOf(action.x, action.y, action.description), + ) + } + + is ActionData.SwipeScreen -> if (action.description.isNullOrBlank()) { + getString( + R.string.description_swipe_coordinate_default, + arrayOf( + action.fingerCount, + action.xStart, + action.yStart, + action.xEnd, + action.yEnd, + action.duration, + ), + ) + } else { + getString( + R.string.description_swipe_coordinate_with_description, + arrayOf( + action.fingerCount, + action.xStart, + action.yStart, + action.xEnd, + action.yEnd, + action.duration, + action.description, + ), + ) + } + + is ActionData.PinchScreen -> if (action.description.isNullOrBlank()) { + val pinchTypeDisplayName = if (action.pinchType == PinchScreenType.PINCH_IN) { + getString(R.string.hint_coordinate_type_pinch_in) } else { - val pinchTypeDisplayName = if (action.pinchType == PinchScreenType.PINCH_IN) { - getString(R.string.hint_coordinate_type_pinch_in) - } else { - getString(R.string.hint_coordinate_type_pinch_out) - } + getString(R.string.hint_coordinate_type_pinch_out) + } - getString( - R.string.description_pinch_coordinate_with_description, - arrayOf( - pinchTypeDisplayName, - action.fingerCount, - action.x, - action.y, - action.distance, - action.duration, - action.description, - ), - ) + getString( + R.string.description_pinch_coordinate_default, + arrayOf( + pinchTypeDisplayName, + action.fingerCount, + action.x, + action.y, + action.distance, + action.duration, + ), + ) + } else { + val pinchTypeDisplayName = if (action.pinchType == PinchScreenType.PINCH_IN) { + getString(R.string.hint_coordinate_type_pinch_in) + } else { + getString(R.string.hint_coordinate_type_pinch_out) } - is ActionData.Text -> getString(R.string.description_text_block, action.text) - is ActionData.Url -> getString(R.string.description_url, action.url) - is ActionData.Sound -> getString(R.string.description_sound, action.soundDescription) + getString( + R.string.description_pinch_coordinate_with_description, + arrayOf( + pinchTypeDisplayName, + action.fingerCount, + action.x, + action.y, + action.distance, + action.duration, + action.description, + ), + ) + } - ActionData.AirplaneMode.Disable -> getString(R.string.action_disable_airplane_mode) - ActionData.AirplaneMode.Enable -> getString(R.string.action_enable_airplane_mode) - ActionData.AirplaneMode.Toggle -> getString(R.string.action_toggle_airplane_mode) + is ActionData.Text -> getString(R.string.description_text_block, action.text) + is ActionData.Url -> getString(R.string.description_url, action.url) + is ActionData.Sound -> getString(R.string.description_sound, action.soundDescription) - ActionData.Bluetooth.Disable -> getString(R.string.action_disable_bluetooth) - ActionData.Bluetooth.Enable -> getString(R.string.action_enable_bluetooth) - ActionData.Bluetooth.Toggle -> getString(R.string.action_toggle_bluetooth) + ActionData.AirplaneMode.Disable -> getString(R.string.action_disable_airplane_mode) + ActionData.AirplaneMode.Enable -> getString(R.string.action_enable_airplane_mode) + ActionData.AirplaneMode.Toggle -> getString(R.string.action_toggle_airplane_mode) - ActionData.Brightness.Decrease -> getString(R.string.action_decrease_brightness) - ActionData.Brightness.DisableAuto -> getString(R.string.action_disable_auto_brightness) - ActionData.Brightness.EnableAuto -> getString(R.string.action_enable_auto_brightness) - ActionData.Brightness.Increase -> getString(R.string.action_increase_brightness) - ActionData.Brightness.ToggleAuto -> getString(R.string.action_toggle_auto_brightness) + ActionData.Bluetooth.Disable -> getString(R.string.action_disable_bluetooth) + ActionData.Bluetooth.Enable -> getString(R.string.action_enable_bluetooth) + ActionData.Bluetooth.Toggle -> getString(R.string.action_toggle_bluetooth) - ActionData.ConsumeKeyEvent -> getString(R.string.action_consume_keyevent) + ActionData.Brightness.Decrease -> getString(R.string.action_decrease_brightness) + ActionData.Brightness.DisableAuto -> getString(R.string.action_disable_auto_brightness) + ActionData.Brightness.EnableAuto -> getString(R.string.action_enable_auto_brightness) + ActionData.Brightness.Increase -> getString(R.string.action_increase_brightness) + ActionData.Brightness.ToggleAuto -> getString(R.string.action_toggle_auto_brightness) - ActionData.ControlMedia.FastForward -> getString(R.string.action_fast_forward) - ActionData.ControlMedia.NextTrack -> getString(R.string.action_next_track) - ActionData.ControlMedia.Pause -> getString(R.string.action_pause_media) - ActionData.ControlMedia.Play -> getString(R.string.action_play_media) - ActionData.ControlMedia.PlayPause -> getString(R.string.action_play_pause_media) - ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) - ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) + ActionData.ConsumeKeyEvent -> getString(R.string.action_consume_keyevent) - ActionData.CopyText -> getString(R.string.action_text_copy) - ActionData.CutText -> getString(R.string.action_text_cut) - ActionData.PasteText -> getString(R.string.action_text_paste) + ActionData.ControlMedia.FastForward -> getString(R.string.action_fast_forward) + ActionData.ControlMedia.NextTrack -> getString(R.string.action_next_track) + ActionData.ControlMedia.Pause -> getString(R.string.action_pause_media) + ActionData.ControlMedia.Play -> getString(R.string.action_play_media) + ActionData.ControlMedia.PlayPause -> getString(R.string.action_play_pause_media) + ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) + ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) - ActionData.DeviceAssistant -> getString(R.string.action_open_device_assistant) + ActionData.CopyText -> getString(R.string.action_text_copy) + ActionData.CutText -> getString(R.string.action_text_cut) + ActionData.PasteText -> getString(R.string.action_text_paste) - ActionData.GoBack -> getString(R.string.action_go_back) - ActionData.GoHome -> getString(R.string.action_go_home) - ActionData.GoLastApp -> getString(R.string.action_go_last_app) - ActionData.OpenMenu -> getString(R.string.action_open_menu) - ActionData.OpenRecents -> getString(R.string.action_open_recents) + ActionData.DeviceAssistant -> getString(R.string.action_open_device_assistant) - ActionData.HideKeyboard -> getString(R.string.action_hide_keyboard) - ActionData.LockDevice -> getString(R.string.action_lock_device) + ActionData.GoBack -> getString(R.string.action_go_back) + ActionData.GoHome -> getString(R.string.action_go_home) + ActionData.GoLastApp -> getString(R.string.action_go_last_app) + ActionData.OpenMenu -> getString(R.string.action_open_menu) + ActionData.OpenRecents -> getString(R.string.action_open_recents) - ActionData.MobileData.Disable -> getString(R.string.action_disable_mobile_data) - ActionData.MobileData.Enable -> getString(R.string.action_enable_mobile_data) - ActionData.MobileData.Toggle -> getString(R.string.action_toggle_mobile_data) + ActionData.HideKeyboard -> getString(R.string.action_hide_keyboard) + ActionData.LockDevice -> getString(R.string.action_lock_device) - ActionData.MoveCursorToEnd -> getString(R.string.action_move_to_end_of_text) + ActionData.MobileData.Disable -> getString(R.string.action_disable_mobile_data) + ActionData.MobileData.Enable -> getString(R.string.action_enable_mobile_data) + ActionData.MobileData.Toggle -> getString(R.string.action_toggle_mobile_data) - ActionData.Nfc.Disable -> getString(R.string.action_nfc_disable) - ActionData.Nfc.Enable -> getString(R.string.action_nfc_enable) - ActionData.Nfc.Toggle -> getString(R.string.action_nfc_toggle) + ActionData.MoveCursorToEnd -> getString(R.string.action_move_to_end_of_text) - ActionData.OpenCamera -> getString(R.string.action_open_camera) - ActionData.OpenSettings -> getString(R.string.action_open_settings) + ActionData.Nfc.Disable -> getString(R.string.action_nfc_disable) + ActionData.Nfc.Enable -> getString(R.string.action_nfc_enable) + ActionData.Nfc.Toggle -> getString(R.string.action_nfc_toggle) - is ActionData.Rotation.CycleRotations -> { - val orientationStrings = action.orientations.map { - getString(OrientationUtils.getLabel(it)) - } + ActionData.OpenCamera -> getString(R.string.action_open_camera) + ActionData.OpenSettings -> getString(R.string.action_open_settings) - getString( - R.string.action_cycle_rotations_formatted, - orientationStrings.joinToString(), - ) + is ActionData.Rotation.CycleRotations -> { + val orientationStrings = action.orientations.map { + getString(OrientationUtils.getLabel(it)) } - ActionData.Rotation.DisableAuto -> getString(R.string.action_disable_auto_rotate) - ActionData.Rotation.EnableAuto -> getString(R.string.action_enable_auto_rotate) - ActionData.Rotation.Landscape -> getString(R.string.action_landscape_mode) - ActionData.Rotation.Portrait -> getString(R.string.action_portrait_mode) - ActionData.Rotation.SwitchOrientation -> getString(R.string.action_switch_orientation) - ActionData.Rotation.ToggleAuto -> getString(R.string.action_toggle_auto_rotate) - - ActionData.ScreenOnOff -> getString(R.string.action_power_on_off_device) - ActionData.Screenshot -> getString(R.string.action_screenshot) - ActionData.SecureLock -> getString(R.string.action_secure_lock_device) - ActionData.SelectWordAtCursor -> getString(R.string.action_select_word_at_cursor) - ActionData.ShowKeyboard -> getString(R.string.action_show_keyboard) - ActionData.ShowKeyboardPicker -> getString(R.string.action_show_keyboard_picker) - ActionData.ShowPowerMenu -> getString(R.string.action_show_power_menu) - - ActionData.StatusBar.Collapse -> getString(R.string.action_collapse_status_bar) - ActionData.StatusBar.ExpandNotifications -> getString(R.string.action_expand_notification_drawer) - ActionData.StatusBar.ExpandQuickSettings -> getString(R.string.action_expand_quick_settings) - ActionData.StatusBar.ToggleNotifications -> getString(R.string.action_toggle_notification_drawer) - ActionData.StatusBar.ToggleQuickSettings -> getString(R.string.action_toggle_quick_settings) - - ActionData.ToggleKeyboard -> getString(R.string.action_toggle_keyboard) - ActionData.ToggleSplitScreen -> getString(R.string.action_toggle_split_screen) - ActionData.VoiceAssistant -> getString(R.string.action_open_assistant) - - ActionData.Wifi.Disable -> getString(R.string.action_disable_wifi) - ActionData.Wifi.Enable -> getString(R.string.action_enable_wifi) - ActionData.Wifi.Toggle -> getString(R.string.action_toggle_wifi) - ActionData.DismissAllNotifications -> getString(R.string.action_dismiss_all_notifications) - ActionData.DismissLastNotification -> getString(R.string.action_dismiss_most_recent_notification) - - ActionData.AnswerCall -> getString(R.string.action_answer_call) - ActionData.EndCall -> getString(R.string.action_end_call) - - ActionData.DeviceControls -> getString(R.string.action_device_controls) + getString( + R.string.action_cycle_rotations_formatted, + orientationStrings.joinToString(), + ) } + ActionData.Rotation.DisableAuto -> getString(R.string.action_disable_auto_rotate) + ActionData.Rotation.EnableAuto -> getString(R.string.action_enable_auto_rotate) + ActionData.Rotation.Landscape -> getString(R.string.action_landscape_mode) + ActionData.Rotation.Portrait -> getString(R.string.action_portrait_mode) + ActionData.Rotation.SwitchOrientation -> getString(R.string.action_switch_orientation) + ActionData.Rotation.ToggleAuto -> getString(R.string.action_toggle_auto_rotate) + + ActionData.ScreenOnOff -> getString(R.string.action_power_on_off_device) + ActionData.Screenshot -> getString(R.string.action_screenshot) + ActionData.SecureLock -> getString(R.string.action_secure_lock_device) + ActionData.SelectWordAtCursor -> getString(R.string.action_select_word_at_cursor) + ActionData.ShowKeyboard -> getString(R.string.action_show_keyboard) + ActionData.ShowKeyboardPicker -> getString(R.string.action_show_keyboard_picker) + ActionData.ShowPowerMenu -> getString(R.string.action_show_power_menu) + + ActionData.StatusBar.Collapse -> getString(R.string.action_collapse_status_bar) + ActionData.StatusBar.ExpandNotifications -> getString(R.string.action_expand_notification_drawer) + ActionData.StatusBar.ExpandQuickSettings -> getString(R.string.action_expand_quick_settings) + ActionData.StatusBar.ToggleNotifications -> getString(R.string.action_toggle_notification_drawer) + ActionData.StatusBar.ToggleQuickSettings -> getString(R.string.action_toggle_quick_settings) + + ActionData.ToggleKeyboard -> getString(R.string.action_toggle_keyboard) + ActionData.ToggleSplitScreen -> getString(R.string.action_toggle_split_screen) + ActionData.VoiceAssistant -> getString(R.string.action_open_assistant) + + ActionData.Wifi.Disable -> getString(R.string.action_disable_wifi) + ActionData.Wifi.Enable -> getString(R.string.action_enable_wifi) + ActionData.Wifi.Toggle -> getString(R.string.action_toggle_wifi) + ActionData.DismissAllNotifications -> getString(R.string.action_dismiss_all_notifications) + ActionData.DismissLastNotification -> getString(R.string.action_dismiss_most_recent_notification) + + ActionData.AnswerCall -> getString(R.string.action_answer_call) + ActionData.EndCall -> getString(R.string.action_end_call) + + ActionData.DeviceControls -> getString(R.string.action_device_controls) + } + override fun getIcon(action: ActionData): IconInfo? = when (action) { is ActionData.InputKeyEvent -> null diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt index f8d8370dd9..9d81cf00ef 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt @@ -120,7 +120,7 @@ class ConfigActionsViewModel>( ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigActionsViewModel, popupViewModel = this@ConfigActionsViewModel, - neverShowDndTriggerErrorAgain = { displayActionUseCase.neverShowDndTriggerErrorAgain() }, + neverShowDndTriggerErrorAgain = { displayActionUseCase.neverShowDndTriggerError() }, fixError = { displayActionUseCase.fixError(it) }, ) } @@ -171,11 +171,10 @@ class ConfigActionsViewModel>( testActionUseCase.invoke(actionData).onFailure { error -> if (error is Error.AccessibilityServiceDisabled) { - ViewModelHelper.handleAccessibilityServiceStoppedSnackBar( + ViewModelHelper.handleAccessibilityServiceStoppedDialog( resourceProvider = this, popupViewModel = this, startService = displayActionUseCase::startAccessibilityService, - message = R.string.dialog_message_enable_accessibility_service_to_test_action, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index b714887e09..77591ca788 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -9,7 +9,6 @@ import io.github.sds100.keymapper.actions.sound.SoundsManager import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.shizuku.InputEventInjector import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.accessibility.ServiceAdapter @@ -23,12 +22,13 @@ import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils +import io.github.sds100.keymapper.system.inputevents.InputEventInjector +import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeMessenger import io.github.sds100.keymapper.system.intents.IntentAdapter import io.github.sds100.keymapper.system.intents.IntentTarget -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils import io.github.sds100.keymapper.system.lock.LockScreenAdapter import io.github.sds100.keymapper.system.media.MediaAdapter import io.github.sds100.keymapper.system.navigation.OpenMenuHelper @@ -63,7 +63,10 @@ import io.github.sds100.keymapper.util.ui.ResourceProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import splitties.bitflags.withFlag import timber.log.Timber @@ -81,7 +84,7 @@ class PerformActionsUseCaseImpl( private val shellAdapter: ShellAdapter, private val intentAdapter: IntentAdapter, private val getActionError: GetActionErrorUseCase, - private val keyMapperImeMessenger: KeyMapperImeMessenger, + private val imeInputEventInjector: ImeInputEventInjector, private val shizukuInputEventInjector: InputEventInjector, private val packageManagerAdapter: PackageManagerAdapter, private val appShortcutAdapter: AppShortcutAdapter, @@ -114,6 +117,13 @@ class PerformActionsUseCaseImpl( ) } + /** + * Cache this so we aren't checking every time a key event must be inputted. + */ + private val inputKeyEventsWithShizuku: StateFlow = + permissionAdapter.isGrantedFlow(Permission.SHIZUKU) + .stateIn(coroutineScope, SharingStarted.Eagerly, false) + override fun perform( action: ActionData, inputEventType: InputEventType, @@ -148,7 +158,7 @@ class PerformActionsUseCaseImpl( ) result = when { - permissionAdapter.isGranted(Permission.SHIZUKU) -> { + inputKeyEventsWithShizuku.value -> { shizukuInputEventInjector.inputKeyEvent(model) Success(Unit) } @@ -156,7 +166,7 @@ class PerformActionsUseCaseImpl( action.useShell -> suAdapter.execute("input keyevent ${model.keyCode}") else -> { - keyMapperImeMessenger.inputKeyEvent(model) + imeInputEventInjector.inputKeyEvent(model) Success(Unit) } @@ -319,7 +329,7 @@ class PerformActionsUseCaseImpl( } is ActionData.Text -> { - keyMapperImeMessenger.inputText(action.text) + imeInputEventInjector.inputText(action.text) result = Success(Unit) } @@ -596,10 +606,10 @@ class PerformActionsUseCaseImpl( metaState = KeyEvent.META_CTRL_ON, ) - if (permissionAdapter.isGranted(Permission.SHIZUKU)) { + if (inputKeyEventsWithShizuku.value) { shizukuInputEventInjector.inputKeyEvent(keyModel) } else { - keyMapperImeMessenger.inputKeyEvent(keyModel) + imeInputEventInjector.inputKeyEvent(keyModel) } result = Success(Unit) @@ -828,7 +838,7 @@ class PerformActionsUseCaseImpl( if (action.device?.descriptor == null) { // automatically select a game controller as the input device for game controller key events - if (KeyEventUtils.isGamepadKeyCode(action.keyCode)) { + if (InputEventUtils.isGamepadKeyCode(action.keyCode)) { deviceAdapter.connectedInputDevices.value.ifIsData { inputDevices -> val device = inputDevices.find { it.isGameController } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ChooseKeyCodeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ChooseKeyCodeViewModel.kt index de8e767aaf..b779b50237 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ChooseKeyCodeViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ChooseKeyCodeViewModel.kt @@ -4,7 +4,7 @@ import android.view.KeyEvent import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.filterByQuery import io.github.sds100.keymapper.util.ui.DefaultSimpleListItem @@ -33,7 +33,7 @@ class ChooseKeyCodeViewModel : ViewModel() { private val allListItems = flow { withContext(Dispatchers.Default) { - KeyEventUtils.getKeyCodes().sorted().map { keyCode -> + InputEventUtils.getKeyCodes().sorted().map { keyCode -> DefaultSimpleListItem( id = keyCode.toString(), title = "$keyCode \t\t ${KeyEvent.keyCodeToString(keyCode)}", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt index e0a7218694..e6734d0921 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -9,7 +9,7 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success @@ -212,7 +212,7 @@ class ConfigKeyEventActionViewModel( onError = { "" }, ) - val modifierListItems = KeyEventUtils.MODIFIER_LABELS.map { (modifier, label) -> + val modifierListItems = InputEventUtils.MODIFIER_LABELS.map { (modifier, label) -> CheckBoxListItem( id = modifier.toString(), label = getString(label), @@ -264,8 +264,7 @@ class ConfigKeyEventActionViewModel( private val resourceProvider: ResourceProvider, ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T = - ConfigKeyEventActionViewModel(useCase, resourceProvider) as T + override fun create(modelClass: Class): T = ConfigKeyEventActionViewModel(useCase, resourceProvider) as T } private data class KeyEventState( diff --git a/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt index f5007fc9ca..78e359b1b7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt @@ -7,6 +7,7 @@ import android.os.DeadObjectException import android.os.IBinder import android.os.IBinder.DeathRecipient import android.view.KeyEvent +import android.view.MotionEvent import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper import timber.log.Timber import java.util.concurrent.ConcurrentHashMap @@ -29,47 +30,75 @@ class KeyEventRelayService : Service() { companion object { const val ACTION_REBIND_RELAY_SERVICE = "io.github.sds100.keymapper.ACTION_REBIND_RELAY_SERVICE" + + const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" + const val CALLBACK_ID_INPUT_METHOD = "input_method" + + /** + * Used when a client registers a callback without specifying an ID. + */ + private const val CALLBACK_ID_DEFAULT = "default" } val permittedPackages = KeyMapperImeHelper.KEY_MAPPER_IME_PACKAGE_LIST private val binderInterface: IKeyEventRelayService = object : IKeyEventRelayService.Stub() { - override fun sendKeyEvent(event: KeyEvent?, targetPackageName: String?): Boolean { - Timber.d("KeyEventRelayService: onKeyEvent ${event?.keyCode}") - - synchronized(callbackLock) { - if (!clientConnections.containsKey(targetPackageName)) { - return false + override fun sendKeyEvent( + event: KeyEvent?, + targetPackageName: String?, + callbackId: String?, + ): Boolean { + event ?: return false + targetPackageName ?: return false + + val key = ClientKey(targetPackageName, callbackId ?: CALLBACK_ID_DEFAULT) + + try { + val connection = clientConnections[key] ?: return false + var consumeKeyEvent = false + + if (connection.callback.onKeyEvent(event)) { + consumeKeyEvent = true } - try { - // Get the process ID of the app that called this service. - val sourcePackageName = getCallerPackageName() ?: return false - - if (!permittedPackages.contains(sourcePackageName)) { - Timber.d("An unrecognized package $sourcePackageName tried to send a key event.") - - return false - } - - var consumeKeyEvent = false + return consumeKeyEvent + } catch (e: DeadObjectException) { + // If the application is no longer connected then delete the callback. + synchronized(callbackLock) { + removeConnection(key) + } + return false + } + } - for (connection in clientConnections[targetPackageName]!!) { - if (connection.callback.onKeyEvent(event, targetPackageName)) { - consumeKeyEvent = true - } - } + override fun sendMotionEvent( + event: MotionEvent?, + targetPackageName: String?, + callbackId: String?, + ): Boolean { + event ?: return false + targetPackageName ?: return false + val key = ClientKey(targetPackageName, callbackId ?: CALLBACK_ID_DEFAULT) + + try { + val connection = clientConnections[key] ?: return false + var consumeMotionEvent = false + + if (connection.callback.onMotionEvent(event)) { + consumeMotionEvent = true + } - return consumeKeyEvent - } catch (e: DeadObjectException) { - // If the application is no longer connected then delete the callback. - clientConnections.remove(targetPackageName) - return false + return consumeMotionEvent + } catch (e: DeadObjectException) { + // If the application is no longer connected then delete the callback. + synchronized(callbackLock) { + removeConnection(key) } + return false } } - override fun registerCallback(client: IKeyEventRelayServiceCallback?) { + override fun registerCallback(client: IKeyEventRelayServiceCallback?, id: String?) { val sourcePackageName = getCallerPackageName() ?: return if (client == null || !permittedPackages.contains(sourcePackageName)) { @@ -79,46 +108,38 @@ class KeyEventRelayService : Service() { synchronized(callbackLock) { Timber.d("Package $sourcePackageName registered a key event relay callback.") - val connection = ClientConnection(sourcePackageName, client) - if (clientConnections.containsKey(sourcePackageName)) { - clientConnections[sourcePackageName] = - clientConnections[sourcePackageName]!!.plus(connection) - } else { - clientConnections[sourcePackageName] = arrayOf(connection) + val key = ClientKey(sourcePackageName, id ?: CALLBACK_ID_DEFAULT) + + // Handle disconnecting the client. Unlink the death recipient. + if (clientConnections.containsKey(key)) { + removeConnection(key) } + val connection = ClientConnection(key, client) + clientConnections[key] = connection client.asBinder().linkToDeath(connection, 0) } } - override fun unregisterAllCallbacks() { - synchronized(callbackLock) { - val sourcePackageName = getCallerPackageName() ?: return + override fun unregisterCallback(callbackId: String?) { + val sourcePackageName = getCallerPackageName() ?: return - Timber.d("Package $sourcePackageName unregistered all key event relay callback.") + // The callback id is not passed on older versions of the API. + if (callbackId == null) { + Timber.d("Package $sourcePackageName unregistered all key event relay callbacks.") - if (clientConnections.containsKey(sourcePackageName)) { - for (connection in clientConnections[sourcePackageName]!!) { - connection.callback.asBinder().unlinkToDeath(connection, 0) - } + removeAllConnections(sourcePackageName) + } else { + Timber.d("Package $sourcePackageName unregistered a key event relay callback.") - clientConnections.remove(sourcePackageName) - } + removeConnection(ClientKey(sourcePackageName, callbackId)) } } - - override fun unregisterCallback(client: IKeyEventRelayServiceCallback?) { - client ?: return - val sourcePackageName = getCallerPackageName() ?: return - - Timber.d("Package $sourcePackageName unregistered a key event relay callback.") - removeConnection(sourcePackageName, client) - } } private val callbackLock: Any = Any() - private var clientConnections: ConcurrentHashMap> = + private var clientConnections: ConcurrentHashMap = ConcurrentHashMap() override fun onCreate() { @@ -143,39 +164,43 @@ class KeyEventRelayService : Service() { override fun onBind(intent: Intent?): IBinder? = binderInterface.asBinder() + /** + * IMPORTANT! This takes about 1ms to execute so do not use it when latency is critical. + */ private fun getCallerPackageName(): String? { val sourceUid = Binder.getCallingUid() return applicationContext.packageManager.getNameForUid(sourceUid) } - private fun removeConnection(packageName: String, callback: IKeyEventRelayServiceCallback) { - if (clientConnections.containsKey(packageName)) { - val newConnections = mutableListOf() + private fun removeConnection(key: ClientKey) { + val connection = clientConnections.remove(key) ?: return - // Unlink the death recipient from the connection to remove and - // delete it from the list of connections for the package. - for (connection in clientConnections[packageName]!!) { - if (connection.callback == callback) { - connection.callback.asBinder().unlinkToDeath(connection, 0) - continue - } + // Unlink the death recipient from the connection to remove and + // delete it from the list of connections for the package. + connection.callback.asBinder().unlinkToDeath(connection, 0) + } - newConnections.add(connection) + private fun removeAllConnections(packageName: String) { + synchronized(callbackLock) { + for (key in clientConnections.keys()) { + if (key.packageName == packageName) { + removeConnection(key) + } } - - clientConnections[packageName] = newConnections.toTypedArray() } } private inner class ClientConnection( - private val clientPackageName: String, + private val clientKey: ClientKey, val callback: IKeyEventRelayServiceCallback, ) : DeathRecipient { override fun binderDied() { - Timber.d("Client binder died: $clientPackageName") + Timber.d("Client binder died: $clientKey") synchronized(callbackLock) { - removeConnection(clientPackageName, callback) + removeConnection(clientKey) } } } + + data class ClientKey(val packageName: String, val callbackId: String) } diff --git a/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt index 434aec69b1..c65f15673b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt @@ -8,6 +8,7 @@ import android.os.DeadObjectException import android.os.IBinder import android.os.RemoteException import android.view.KeyEvent +import android.view.MotionEvent /** * This handles connecting to the relay service and exposes an interface @@ -16,6 +17,7 @@ import android.view.KeyEvent */ class KeyEventRelayServiceWrapperImpl( private val ctx: Context, + private val id: String, private val callback: IKeyEventRelayServiceCallback, ) : KeyEventRelayServiceWrapper { @@ -30,7 +32,7 @@ class KeyEventRelayServiceWrapperImpl( ) { synchronized(keyEventRelayServiceLock) { keyEventRelayService = IKeyEventRelayService.Stub.asInterface(service) - keyEventRelayService?.registerCallback(callback) + keyEventRelayService?.registerCallback(callback, id) } } @@ -54,20 +56,36 @@ class KeyEventRelayServiceWrapperImpl( } override fun sendKeyEvent( - event: KeyEvent?, - targetPackageName: String?, + event: KeyEvent, + targetPackageName: String, + callbackId: String, ): Boolean { - synchronized(keyEventRelayServiceLock) { - if (keyEventRelayService == null) { - return false - } + if (keyEventRelayService == null) { + return false + } - try { - return keyEventRelayService!!.sendKeyEvent(event, targetPackageName) - } catch (e: DeadObjectException) { - keyEventRelayService = null - return false - } + try { + return keyEventRelayService!!.sendKeyEvent(event, targetPackageName, callbackId) + } catch (e: DeadObjectException) { + keyEventRelayService = null + return false + } + } + + override fun sendMotionEvent( + event: MotionEvent, + targetPackageName: String, + callbackId: String, + ): Boolean { + if (keyEventRelayService == null) { + return false + } + + try { + return keyEventRelayService!!.sendMotionEvent(event, targetPackageName, callbackId) + } catch (e: DeadObjectException) { + keyEventRelayService = null + return false } } @@ -92,7 +110,7 @@ class KeyEventRelayServiceWrapperImpl( // because the connection is already broken at that point and it // will fail. try { - keyEventRelayService?.unregisterCallback(callback) + keyEventRelayService?.unregisterCallback(id) ctx.unbindService(serviceConnection) } catch (e: RemoteException) { // do nothing @@ -104,5 +122,6 @@ class KeyEventRelayServiceWrapperImpl( } interface KeyEventRelayServiceWrapper { - fun sendKeyEvent(event: KeyEvent?, targetPackageName: String?): Boolean + fun sendKeyEvent(event: KeyEvent, targetPackageName: String, callbackId: String): Boolean + fun sendMotionEvent(event: MotionEvent, targetPackageName: String, callbackId: String): Boolean } diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt index d4b067d4b5..e180da9289 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt +++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt @@ -32,6 +32,7 @@ object ComposeColors { val inversePrimaryLight = Color(0xFFA8C7FF) val redLight = Color(0xffd32f2f) val onRedLight = Color(0xFFFFFFFF) + val greenLight = Color(0xff4CAF50) val primaryDark = Color(0xFFA8C7FF) val onPrimaryDark = Color(0xFF002F66) @@ -61,4 +62,5 @@ object ComposeColors { val inverseOnSurfaceDark = Color(0xFF1A1B1F) val redDark = Color(0xffff7961) val onRedDark = Color(0xFFFFFFFF) + val greenDark = Color(0xff80e27e) } diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt index 55545a9a3d..b074c91249 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt +++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt @@ -13,16 +13,19 @@ import androidx.compose.ui.graphics.Color data class ComposeCustomColors( val red: Color = Color.Unspecified, val onRed: Color = Color.Unspecified, + val green: Color = Color.Unspecified, ) { companion object { val LightPalette = ComposeCustomColors( red = ComposeColors.redLight, onRed = ComposeColors.onRedLight, + green = ComposeColors.greenLight, ) val DarkPalette = ComposeCustomColors( red = ComposeColors.redDark, onRed = ComposeColors.onRedDark, + green = ComposeColors.greenDark, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt index 2ff5b25225..5f32f164fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt @@ -125,7 +125,7 @@ class ConfigConstraintsViewModel( ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigConstraintsViewModel, popupViewModel = this@ConfigConstraintsViewModel, - neverShowDndTriggerErrorAgain = { displayUseCase.neverShowDndTriggerErrorAgain() }, + neverShowDndTriggerErrorAgain = { displayUseCase.neverShowDndTriggerError() }, fixError = { displayUseCase.fixError(it) }, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 9bed05c66f..3e63ee4ed1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -67,5 +67,12 @@ object Keys { val shownShizukuPermissionPrompt = booleanPreferencesKey("key_shown_shizuku_permission_prompt") val savedWifiSSIDs = stringSetPreferencesKey("key_saved_wifi_ssids") - val neverShowDndError = booleanPreferencesKey("key_never_show_dnd_error") + val neverShowDndAccessError = booleanPreferencesKey("key_never_show_dnd_error") + val neverShowTriggerKeyboardIconExplanation = + booleanPreferencesKey("key_never_show_keyboard_icon_explanation") + + val neverShowDpadImeTriggerError = + booleanPreferencesKey("key_never_show_dpad_ime_trigger_error") + val neverShowNoKeysRecordedError = + booleanPreferencesKey("key_never_show_no_keys_recorded_error") } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt index 69d9c2ecb5..e26132c25a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt @@ -39,5 +39,6 @@ data class KeyCodeTriggerKeyEntity( const val DEVICE_ID_ANY_DEVICE = "io.github.sds100.keymapper.ANY_DEVICE" const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 + const val FLAG_DETECTION_SOURCE_INPUT_METHOD = 2 } } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index 211cb574b4..d5f2888ba5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -24,7 +24,6 @@ sealed class TriggerKeyEntity : Parcelable { const val NAME_UID = "uid" // Click types - const val UNDETERMINED = -1 const val SHORT_PRESS = 0 const val LONG_PRESS = 1 const val DOUBLE_PRESS = 2 diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepository.kt index afc70a2ea6..d9aaf956fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepository.kt @@ -9,4 +9,5 @@ import kotlinx.coroutines.flow.Flow interface PreferenceRepository { fun get(key: Preferences.Key): Flow fun set(key: Preferences.Key, value: T?) + fun deleteAll() } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt index 887f37062b..ad833ca997 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt @@ -35,8 +35,7 @@ class SettingsPreferenceRepository( private val dataStore = ctx.dataStore - override fun get(key: Preferences.Key): Flow = - dataStore.data.map { it[key] }.distinctUntilChanged() + override fun get(key: Preferences.Key): Flow = dataStore.data.map { it[key] }.distinctUntilChanged() override fun set(key: Preferences.Key, value: T?) { coroutineScope.launch { @@ -49,4 +48,10 @@ class SettingsPreferenceRepository( } } } + + override fun deleteAll() { + coroutineScope.launch { + dataStore.edit { it.clear() } + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt index ec1f4ba102..819834ecf6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt @@ -10,10 +10,17 @@ import android.view.ViewGroup import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.bottomappbar.BottomAppBar.FAB_ALIGNMENT_MODE_CENTER @@ -21,8 +28,10 @@ import com.google.android.material.bottomappbar.BottomAppBar.FAB_ALIGNMENT_MODE_ import com.google.android.material.tabs.TabLayoutMediator import io.github.sds100.keymapper.R import io.github.sds100.keymapper.backup.BackupUtils +import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.databinding.FragmentHomeBinding import io.github.sds100.keymapper.fixError +import io.github.sds100.keymapper.mappings.keymaps.trigger.DpadTriggerSetupBottomSheet import io.github.sds100.keymapper.success import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.url.UrlUtils @@ -105,6 +114,34 @@ class HomeFragment : Fragment() { FragmentHomeBinding.inflate(inflater, container, false).apply { lifecycleOwner = viewLifecycleOwner _binding = this + + @OptIn(ExperimentalMaterial3Api::class) + composeView.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + KeyMapperTheme { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val state by homeViewModel.keymapListViewModel.setupGuiKeyboardState.collectAsStateWithLifecycle() + + if (homeViewModel.keymapListViewModel.showDpadTriggerSetupBottomSheet) { + DpadTriggerSetupBottomSheet( + modifier = Modifier.systemBarsPadding(), + onDismissRequest = { + homeViewModel.keymapListViewModel.showDpadTriggerSetupBottomSheet = + false + }, + guiKeyboardState = state, + onEnableKeyboardClick = homeViewModel.keymapListViewModel::onEnableGuiKeyboardClick, + onChooseKeyboardClick = homeViewModel.keymapListViewModel::onChooseGuiKeyboardClick, + onNeverShowAgainClick = homeViewModel.keymapListViewModel::onNeverShowSetupDpadClick, + sheetState = sheetState, + ) + } + } + } + } return this.root } } diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt index b3f6f3d6c1..9004fb00f4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt @@ -10,6 +10,7 @@ import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapListVie import io.github.sds100.keymapper.mappings.fingerprintmaps.ListFingerprintMapsUseCase import io.github.sds100.keymapper.mappings.keymaps.KeyMapListViewModel import io.github.sds100.keymapper.mappings.keymaps.ListKeyMapsUseCase +import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.system.accessibility.ServiceState import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase @@ -60,6 +61,7 @@ class HomeViewModel( private val showImePicker: ShowInputMethodPickerUseCase, private val onboarding: OnboardingUseCase, resourceProvider: ResourceProvider, + private val setupGuiKeyboard: SetupGuiKeyboardUseCase, ) : ViewModel(), ResourceProvider by resourceProvider, PopupViewModel by PopupViewModelImpl(), @@ -92,6 +94,7 @@ class HomeViewModel( listKeyMaps, resourceProvider, multiSelectProvider, + setupGuiKeyboard, ) } @@ -257,32 +260,9 @@ class HomeViewModel( val closeKeyMapper = _closeKeyMapper.asSharedFlow() init { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch { backupRestore.onAutomaticBackupResult.collectLatest { result -> - when (result) { - is Success -> { - showPopup( - "successful_automatic_backup_result", - PopupUi.SnackBar(getString(R.string.toast_automatic_backup_successful)), - ) - } - - is Error -> { - val response = showPopup( - "automatic_backup_error", - PopupUi.Dialog( - title = getString(R.string.toast_automatic_backup_failed), - message = result.getFullMessage(this@HomeViewModel), - positiveButtonText = getString(R.string.pos_ok), - neutralButtonText = getString(R.string.neutral_go_to_settings), - ), - ) ?: return@collectLatest - - if (response == DialogResponse.NEUTRAL) { - navigate("settings", NavDestination.Settings) - } - } - } + onAutomaticBackupResult(result) } } @@ -292,25 +272,80 @@ class HomeViewModel( ) { showWhatsNew, showQuickStartGuideHint -> if (showWhatsNew) { - val dialog = PopupUi.Dialog( - title = getString(R.string.whats_new), - message = onboarding.getWhatsNewText(), - positiveButtonText = getString(R.string.pos_ok), - neutralButtonText = getString(R.string.neutral_changelog), + showWhatsNewDialog() + } + + _showQuickStartGuideHint.value = showQuickStartGuideHint + }.launchIn(viewModelScope) + + viewModelScope.launch { + if (setupGuiKeyboard.isInstalled.first() && !setupGuiKeyboard.isCompatibleVersion.first()) { + showUpgradeGuiKeyboardDialog() + } + } + } + + private suspend fun showWhatsNewDialog() { + val dialog = PopupUi.Dialog( + title = getString(R.string.whats_new), + message = onboarding.getWhatsNewText(), + positiveButtonText = getString(R.string.pos_ok), + neutralButtonText = getString(R.string.neutral_changelog), + ) + + // don't return if they dismiss the dialog because this is common behaviour. + val response = showPopup("whats-new", dialog) + + if (response == DialogResponse.NEUTRAL) { + showPopup("url_changelog", PopupUi.OpenUrl(getString(R.string.url_changelog))) + } + + onboarding.showedWhatsNew() + } + + private suspend fun onAutomaticBackupResult(result: Result<*>) { + when (result) { + is Success -> { + showPopup( + "successful_automatic_backup_result", + PopupUi.SnackBar(getString(R.string.toast_automatic_backup_successful)), ) + } - // don't return if they dismiss the dialog because this is common behaviour. - val response = showPopup("whats-new", dialog) + is Error -> { + val response = showPopup( + "automatic_backup_error", + PopupUi.Dialog( + title = getString(R.string.toast_automatic_backup_failed), + message = result.getFullMessage(this), + positiveButtonText = getString(R.string.pos_ok), + neutralButtonText = getString(R.string.neutral_go_to_settings), + ), + ) ?: return if (response == DialogResponse.NEUTRAL) { - showPopup("url_changelog", PopupUi.OpenUrl(getString(R.string.url_changelog))) + navigate("settings", NavDestination.Settings) } - - onboarding.showedWhatsNew() } + } + } - _showQuickStartGuideHint.value = showQuickStartGuideHint - }.flowOn(Dispatchers.Default).launchIn(viewModelScope) + private suspend fun showUpgradeGuiKeyboardDialog() { + val dialog = PopupUi.Dialog( + title = getString(R.string.dialog_upgrade_gui_keyboard_title), + message = getString(R.string.dialog_upgrade_gui_keyboard_message), + positiveButtonText = getString(R.string.dialog_upgrade_gui_keyboard_positive), + negativeButtonText = getString(R.string.dialog_upgrade_gui_keyboard_neutral), + ) + + val response = showPopup("upgrade_gui_keyboard", dialog) + + if (response == DialogResponse.POSITIVE) { + showPopup( + "gui_keyboard_play_store", + PopupUi.OpenUrl(getString(R.string.url_play_store_keymapper_gui_keyboard)), + ) + } } fun approvedQuickStartGuideTapTarget() { @@ -513,6 +548,7 @@ class HomeViewModel( private val showImePicker: ShowInputMethodPickerUseCase, private val onboarding: OnboardingUseCase, private val resourceProvider: ResourceProvider, + private val setupGuiKeyboard: SetupGuiKeyboardUseCase, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T = HomeViewModel( @@ -524,6 +560,7 @@ class HomeViewModel( showImePicker, onboarding, resourceProvider, + setupGuiKeyboard, ) as T } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/DisplayMappingUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/DisplayMappingUseCase.kt index e123ccd888..50a991bffc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/DisplayMappingUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/DisplayMappingUseCase.kt @@ -84,8 +84,8 @@ class DisplaySimpleMappingUseCaseImpl( override fun restartAccessibilityService(): Boolean = accessibilityServiceAdapter.restart() - override fun neverShowDndTriggerErrorAgain() { - preferenceRepository.set(Keys.neverShowDndError, true) + override fun neverShowDndTriggerError() { + preferenceRepository.set(Keys.neverShowDndAccessError, true) } } @@ -101,7 +101,7 @@ interface DisplayActionUseCase : GetActionErrorUseCase { fun getAppIcon(packageName: String): Result fun getInputMethodLabel(imeId: String): Result suspend fun fixError(error: Error) - fun neverShowDndTriggerErrorAgain() + fun neverShowDndTriggerError() fun startAccessibilityService(): Boolean fun restartAccessibilityService(): Boolean } @@ -110,6 +110,6 @@ interface DisplayConstraintUseCase : GetConstraintErrorUseCase { fun getAppName(packageName: String): Result fun getAppIcon(packageName: String): Result fun getInputMethodLabel(imeId: String): Result - fun neverShowDndTriggerErrorAgain() + fun neverShowDndTriggerError() suspend fun fixError(error: Error) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/fingerprintmaps/FingerprintMapListViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/fingerprintmaps/FingerprintMapListViewModel.kt index f9b0fb51c6..e8da0b8740 100755 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/fingerprintmaps/FingerprintMapListViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/fingerprintmaps/FingerprintMapListViewModel.kt @@ -152,7 +152,7 @@ class FingerprintMapListViewModel( ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@FingerprintMapListViewModel, popupViewModel = this@FingerprintMapListViewModel, - neverShowDndTriggerErrorAgain = { useCase.neverShowDndTriggerErrorAgain() }, + neverShowDndTriggerErrorAgain = { useCase.neverShowDndTriggerError() }, fixError = { useCase.fixError(it) }, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt index 83ee68fe9f..7d4f672c58 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt @@ -12,13 +12,14 @@ import io.github.sds100.keymapper.mappings.ConfigMappingUseCase import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.Defaultable import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull @@ -75,6 +76,7 @@ class ConfigKeyMapUseCaseImpl( override fun addKeyCodeTriggerKey( keyCode: Int, device: TriggerKeyDevice, + detectionSource: KeyEventDetectionSource, ) = editTrigger { trigger -> val clickType = when (trigger.mode) { is TriggerMode.Parallel -> trigger.mode.clickType @@ -93,7 +95,7 @@ class ConfigKeyMapUseCaseImpl( var consumeKeyEvent = true // Issue #753 - if (KeyEventUtils.isModifierKey(keyCode)) { + if (InputEventUtils.isModifierKey(keyCode)) { consumeKeyEvent = false } @@ -102,6 +104,7 @@ class ConfigKeyMapUseCaseImpl( device = device, clickType = clickType, consumeEvent = consumeKeyEvent, + detectionSource = detectionSource, ) val newKeys = trigger.keys.plus(triggerKey) @@ -291,11 +294,9 @@ class ConfigKeyMapUseCaseImpl( override fun setVibrateEnabled(enabled: Boolean) = editTrigger { it.copy(vibrate = enabled) } - override fun setVibrationDuration(duration: Defaultable) = - editTrigger { it.copy(vibrateDuration = duration.nullIfDefault()) } + override fun setVibrationDuration(duration: Defaultable) = editTrigger { it.copy(vibrateDuration = duration.nullIfDefault()) } - override fun setLongPressDelay(delay: Defaultable) = - editTrigger { it.copy(longPressDelay = delay.nullIfDefault()) } + override fun setLongPressDelay(delay: Defaultable) = editTrigger { it.copy(longPressDelay = delay.nullIfDefault()) } override fun setDoublePressDelay(delay: Defaultable) { editTrigger { it.copy(doublePressDelay = delay.nullIfDefault()) } @@ -372,48 +373,46 @@ class ConfigKeyMapUseCaseImpl( } } - override fun setActionRepeatEnabled(uid: String, repeat: Boolean) = - setActionOption(uid) { it.copy(repeat = repeat) } + override fun setActionRepeatEnabled(uid: String, repeat: Boolean) = setActionOption(uid) { it.copy(repeat = repeat) } - override fun setActionRepeatRate(uid: String, repeatRate: Int?) = - setActionOption(uid) { it.copy(repeatRate = repeatRate) } + override fun setActionRepeatRate(uid: String, repeatRate: Int?) = setActionOption(uid) { it.copy(repeatRate = repeatRate) } - override fun setActionRepeatDelay(uid: String, repeatDelay: Int?) = - setActionOption(uid) { it.copy(repeatDelay = repeatDelay) } + override fun setActionRepeatDelay(uid: String, repeatDelay: Int?) = setActionOption(uid) { it.copy(repeatDelay = repeatDelay) } - override fun setActionRepeatLimit(uid: String, repeatLimit: Int?) = - setActionOption(uid) { it.copy(repeatLimit = repeatLimit) } + override fun setActionRepeatLimit(uid: String, repeatLimit: Int?) = setActionOption(uid) { it.copy(repeatLimit = repeatLimit) } - override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = - setActionOption(uid) { it.copy(holdDown = holdDown) } + override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = setActionOption(uid) { it.copy(holdDown = holdDown) } - override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int?) = - setActionOption(uid) { it.copy(holdDownDuration = holdDownDuration) } + override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int?) = setActionOption(uid) { it.copy(holdDownDuration = holdDownDuration) } - override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } + override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } - override fun setActionStopRepeatingWhenLimitReached(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } + override fun setActionStopRepeatingWhenLimitReached(uid: String) = setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } - override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } + override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } - override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = - setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } + override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } - override fun setActionMultiplier(uid: String, multiplier: Int?) = - setActionOption(uid) { it.copy(multiplier = multiplier) } + override fun setActionMultiplier(uid: String, multiplier: Int?) = setActionOption(uid) { it.copy(multiplier = multiplier) } - override fun setDelayBeforeNextAction(uid: String, delay: Int?) = - setActionOption(uid) { it.copy(delayBeforeNextAction = delay) } + override fun setDelayBeforeNextAction(uid: String, delay: Int?) = setActionOption(uid) { it.copy(delayBeforeNextAction = delay) } override fun createAction(data: ActionData): KeyMapAction { var holdDown = false var repeat = false if (data is ActionData.InputKeyEvent) { - if (KeyEventUtils.isModifierKey(data.keyCode)) { + val trigger = mapping.value.dataOrNull()?.trigger + + val containsDpadKey: Boolean = if (trigger == null) { + false + } else { + trigger.keys + .mapNotNull { it as? KeyCodeTriggerKey } + .any { InputEventUtils.isDpadKeyCode(it.keyCode) } + } + + if (InputEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { holdDown = true repeat = false } else { @@ -517,7 +516,12 @@ class ConfigKeyMapUseCaseImpl( interface ConfigKeyMapUseCase : ConfigMappingUseCase { // trigger - fun addKeyCodeTriggerKey(keyCode: Int, device: TriggerKeyDevice) + fun addKeyCodeTriggerKey( + keyCode: Int, + device: TriggerKeyDevice, + detectionSource: KeyEventDetectionSource, + ) + fun addAssistantTriggerKey(type: AssistantTriggerType) fun removeTriggerKey(uid: String) fun getTriggerKey(uid: String): TriggerKey? diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt index 8575a29006..707c8af5de 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt @@ -14,6 +14,7 @@ import io.github.sds100.keymapper.mappings.ConfigMappingViewModel import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerKeyViewModel import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.purchasing.PurchasingManager import io.github.sds100.keymapper.ui.utils.getJsonSerializable @@ -40,6 +41,7 @@ class ConfigKeyMapViewModel( createActionUseCase: CreateActionUseCase, resourceProvider: ResourceProvider, purchasingManager: PurchasingManager, + setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, ) : ViewModel(), ConfigMappingViewModel, ResourceProvider by resourceProvider { @@ -73,6 +75,7 @@ class ConfigKeyMapViewModel( displayMapping, resourceProvider, purchasingManager, + setupGuiKeyboardUseCase, ) override val configConstraintsViewModel = ConfigConstraintsViewModel( @@ -134,21 +137,22 @@ class ConfigKeyMapViewModel( private val createActionUseCase: CreateActionUseCase, private val resourceProvider: ResourceProvider, private val purchasingManager: PurchasingManager, + private val setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class) = - ConfigKeyMapViewModel( - config, - testAction, - onboard, - recordTrigger, - createKeyMapShortcut, - displayMapping, - createActionUseCase, - resourceProvider, - purchasingManager, - ) as T + override fun create(modelClass: Class) = ConfigKeyMapViewModel( + config, + testAction, + onboard, + recordTrigger, + createKeyMapShortcut, + displayMapping, + createActionUseCase, + resourceProvider, + purchasingManager, + setupGuiKeyboardUseCase, + ) as T } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt index 9b95283341..d368d0ddf5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt @@ -10,6 +10,7 @@ import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.purchasing.PurchasingManager +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.system.permissions.Permission @@ -17,6 +18,7 @@ import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.util.valueIfFailure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -28,7 +30,7 @@ class DisplayKeyMapUseCaseImpl( private val permissionAdapter: PermissionAdapter, private val inputMethodAdapter: InputMethodAdapter, displaySimpleMappingUseCase: DisplaySimpleMappingUseCase, - private val preferenceRepository: PreferenceRepository, + private val preferences: PreferenceRepository, private val purchasingManager: PurchasingManager, ) : DisplayKeyMapUseCase, DisplaySimpleMappingUseCase by displaySimpleMappingUseCase { @@ -41,17 +43,47 @@ class DisplayKeyMapUseCaseImpl( private val keyMapperImeHelper: KeyMapperImeHelper = KeyMapperImeHelper(inputMethodAdapter) + private val showDpadImeSetupError: Flow = + preferences.get(Keys.neverShowDpadImeTriggerError).map { neverShow -> + if (neverShow == null) { + true + } else { + !neverShow + } + } + override val invalidateTriggerErrors: Flow = merge( permissionAdapter.onPermissionsUpdate, - preferenceRepository.get(Keys.neverShowDndError).map { }.drop(1), + preferences.get(Keys.neverShowDndAccessError).map { }.drop(1), purchasingManager.onCompleteProductPurchase.map { }, + inputMethodAdapter.chosenIme.map { }, + showDpadImeSetupError.map { }, ) + override val showTriggerKeyboardIconExplanation: Flow = + preferences.get(Keys.neverShowTriggerKeyboardIconExplanation).map { neverShow -> + if (neverShow == null) { + true + } else { + !neverShow + } + } + + override fun neverShowTriggerKeyboardIconExplanation() { + preferences.set(Keys.neverShowTriggerKeyboardIconExplanation, true) + } + + override fun neverShowDpadImeSetupError() { + preferences.set(Keys.neverShowDpadImeTriggerError, true) + } + override suspend fun getTriggerErrors(keyMap: KeyMap): List { val trigger = keyMap.trigger val errors = mutableListOf() + val isKeyMapperImeChosen = keyMapperImeHelper.isCompatibleImeChosen() + // can only detect volume button presses during a phone call with an input method service - if (!keyMapperImeHelper.isCompatibleImeChosen() && keyMap.requiresImeKeyEventForwarding()) { + if (!isKeyMapperImeChosen && keyMap.requiresImeKeyEventForwarding()) { errors.add(TriggerError.CANT_DETECT_IN_PHONE_CALL) } @@ -74,9 +106,9 @@ class DisplayKeyMapUseCaseImpl( errors.add(TriggerError.SCREEN_OFF_ROOT_DENIED) } - val containsAssistantTrigger = keyMap.trigger.keys.any { it is AssistantTriggerKey } + val containsAssistantTrigger = trigger.keys.any { it is AssistantTriggerKey } val containsDeviceAssistantTrigger = - keyMap.trigger.keys.any { it is AssistantTriggerKey && it.requiresDeviceAssistant() } + trigger.keys.any { it is AssistantTriggerKey && it.requiresDeviceAssistant() } val isAssistantTriggerPurchased = purchasingManager.isPurchased(ProductId.ASSISTANT_TRIGGER).valueIfFailure { false } @@ -94,6 +126,14 @@ class DisplayKeyMapUseCaseImpl( errors.add(TriggerError.ASSISTANT_NOT_SELECTED) } + val containsDpadKey = trigger.keys + .mapNotNull { it as? KeyCodeTriggerKey } + .any { InputEventUtils.isDpadKeyCode(it.keyCode) } + + if (showDpadImeSetupError.first() && !isKeyMapperImeChosen && containsDpadKey) { + errors.add(TriggerError.DPAD_IME_NOT_SELECTED) + } + return errors } } @@ -101,4 +141,8 @@ class DisplayKeyMapUseCaseImpl( interface DisplayKeyMapUseCase : DisplaySimpleMappingUseCase { val invalidateTriggerErrors: Flow suspend fun getTriggerErrors(keyMap: KeyMap): List + val showTriggerKeyboardIconExplanation: Flow + fun neverShowTriggerKeyboardIconExplanation() + + fun neverShowDpadImeSetupError() } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt index 931e880d87..1dd7c5a6a2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt @@ -6,13 +6,14 @@ import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.ui.ChipUi @@ -99,39 +100,44 @@ class KeyMapListItemCreator( ) } - private fun getTriggerChipError(error: TriggerError): ChipUi.Error = - when (error) { - TriggerError.DND_ACCESS_DENIED -> - ChipUi.Error( - id = TriggerError.DND_ACCESS_DENIED.toString(), - text = getString(R.string.trigger_error_dnd_access_denied_short), - error = Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY), - ) - - TriggerError.SCREEN_OFF_ROOT_DENIED -> ChipUi.Error( - id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(), - text = getString(R.string.trigger_error_screen_off_root_permission_denied_short), - error = Error.PermissionDenied(Permission.ROOT), + private fun getTriggerChipError(error: TriggerError): ChipUi.Error = when (error) { + TriggerError.DND_ACCESS_DENIED -> + ChipUi.Error( + id = TriggerError.DND_ACCESS_DENIED.toString(), + text = getString(R.string.trigger_error_dnd_access_denied_short), + error = Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY), ) - TriggerError.CANT_DETECT_IN_PHONE_CALL -> ChipUi.Error( - id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(), - text = getString(R.string.trigger_error_cant_detect_in_phone_call), - error = Error.CantDetectKeyEventsInPhoneCall, - ) + TriggerError.SCREEN_OFF_ROOT_DENIED -> ChipUi.Error( + id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(), + text = getString(R.string.trigger_error_screen_off_root_permission_denied_short), + error = Error.PermissionDenied(Permission.ROOT), + ) - TriggerError.ASSISTANT_NOT_SELECTED -> ChipUi.Error( - id = error.toString(), - text = getString(R.string.trigger_error_assistant_activity_not_chosen), - error = Error.PermissionDenied(Permission.DEVICE_ASSISTANT), - ) + TriggerError.CANT_DETECT_IN_PHONE_CALL -> ChipUi.Error( + id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(), + text = getString(R.string.trigger_error_cant_detect_in_phone_call), + error = Error.CantDetectKeyEventsInPhoneCall, + ) - TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> ChipUi.Error( - id = error.toString(), - text = getString(R.string.trigger_error_assistant_not_purchased), - error = Error.ProductNotPurchased(ProductId.ASSISTANT_TRIGGER), - ) - } + TriggerError.ASSISTANT_NOT_SELECTED -> ChipUi.Error( + id = error.toString(), + text = getString(R.string.trigger_error_assistant_activity_not_chosen), + error = Error.PermissionDenied(Permission.DEVICE_ASSISTANT), + ) + + TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> ChipUi.Error( + id = error.toString(), + text = getString(R.string.trigger_error_assistant_not_purchased), + error = Error.ProductNotPurchased(ProductId.ASSISTANT_TRIGGER), + ) + + TriggerError.DPAD_IME_NOT_SELECTED -> ChipUi.Error( + id = error.toString(), + text = getString(R.string.trigger_error_dpad_ime_not_selected_short), + error = Error.DpadTriggerImeNotSelected, + ) + } private fun StringBuilder.appendKeyCodeTriggerKeyName( key: KeyCodeTriggerKey, @@ -143,7 +149,7 @@ class KeyMapListItemCreator( else -> Unit } - append(KeyEventUtils.keyCodeToString(key.keyCode)) + append(InputEventUtils.keyCodeToString(key.keyCode)) val deviceName = when (key.device) { is TriggerKeyDevice.Internal -> getString(R.string.this_device) @@ -162,6 +168,10 @@ class KeyMapListItemCreator( append(" (") + if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + append("${getString(R.string.flag_detect_from_input_method)} $midDot ") + } + append(deviceName) if (!key.consumeEvent) { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt index a3cbf794bb..723bfd1c23 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt @@ -1,5 +1,10 @@ package io.github.sds100.keymapper.mappings.keymaps +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardState +import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State @@ -19,6 +24,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -26,6 +33,7 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch open class KeyMapListViewModel( @@ -33,6 +41,7 @@ open class KeyMapListViewModel( private val useCase: ListKeyMapsUseCase, resourceProvider: ResourceProvider, private val multiSelectProvider: MultiSelectProvider, + private val setupGuiKeyboard: SetupGuiKeyboardUseCase, ) : PopupViewModel by PopupViewModelImpl(), ResourceProvider by resourceProvider, NavigationViewModel by NavigationViewModelImpl() { @@ -42,6 +51,19 @@ open class KeyMapListViewModel( private val _state = MutableStateFlow>>(State.Loading) val state = _state.asStateFlow() + val setupGuiKeyboardState: StateFlow = combine( + setupGuiKeyboard.isInstalled, + setupGuiKeyboard.isEnabled, + setupGuiKeyboard.isChosen, + ) { isInstalled, isEnabled, isChosen -> + SetupGuiKeyboardState( + isInstalled, + isEnabled, + isChosen, + ) + }.stateIn(coroutineScope, SharingStarted.Lazily, SetupGuiKeyboardState.DEFAULT) + var showDpadTriggerSetupBottomSheet: Boolean by mutableStateOf(false) + init { val keyMapStateListFlow = MutableStateFlow>>(State.Loading) @@ -79,6 +101,30 @@ open class KeyMapListViewModel( } } + coroutineScope.launch { + useCase.invalidateTriggerErrors.drop(1).collectLatest { + /* + Don't get the key maps from the repository because there can be a race condition + when restoring key maps. This happens because when the activity is resumed the + key maps in the repository are being updated and this flow is collected + at the same time. + */ + rebuildUiState.emit(rebuildUiState.first()) + } + } + + coroutineScope.launch { + useCase.invalidateConstraintErrors.drop(1).collectLatest { + /* + Don't get the key maps from the repository because there can be a race condition + when restoring key maps. This happens because when the activity is resumed the + key maps in the repository are being updated and this flow is collected + at the same time. + */ + rebuildUiState.emit(rebuildUiState.first()) + } + } + coroutineScope.launch(Dispatchers.Default) { combine( keyMapStateListFlow, @@ -156,24 +202,44 @@ open class KeyMapListViewModel( } } + fun onEnableGuiKeyboardClick() { + setupGuiKeyboard.enableInputMethod() + } + + fun onChooseGuiKeyboardClick() { + setupGuiKeyboard.chooseInputMethod() + } + + fun onNeverShowSetupDpadClick() { + useCase.neverShowDpadImeSetupError() + } + private fun onFixError(error: Error) { coroutineScope.launch { - if (error == Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) { - coroutineScope.launch { - ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( + when (error) { + Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY) -> { + coroutineScope.launch { + ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( + resourceProvider = this@KeyMapListViewModel, + popupViewModel = this@KeyMapListViewModel, + neverShowDndTriggerErrorAgain = { useCase.neverShowDndTriggerError() }, + fixError = { useCase.fixError(it) }, + ) + } + } + + Error.DpadTriggerImeNotSelected -> { + showDpadTriggerSetupBottomSheet = true + } + + else -> { + ViewModelHelper.showFixErrorDialog( resourceProvider = this@KeyMapListViewModel, popupViewModel = this@KeyMapListViewModel, - neverShowDndTriggerErrorAgain = { useCase.neverShowDndTriggerErrorAgain() }, - fixError = { useCase.fixError(it) }, - ) - } - } else { - ViewModelHelper.showFixErrorDialog( - resourceProvider = this@KeyMapListViewModel, - popupViewModel = this@KeyMapListViewModel, - error, - ) { - useCase.fixError(error) + error, + ) { + useCase.fixError(error) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt index 77b16b3995..e95600d22b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt @@ -10,18 +10,14 @@ import io.github.sds100.keymapper.mappings.DetectMappingUseCase import io.github.sds100.keymapper.mappings.keymaps.KeyMap import io.github.sds100.keymapper.mappings.keymaps.KeyMapEntityMapper import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository -import io.github.sds100.keymapper.shizuku.InputEventInjector import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.display.DisplayAdapter +import io.github.sds100.keymapper.system.inputevents.InputEventInjector +import io.github.sds100.keymapper.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeMessenger import io.github.sds100.keymapper.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.phone.CallState -import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter import io.github.sds100.keymapper.util.InputEventType @@ -45,12 +41,10 @@ class DetectKeyMapsUseCaseImpl( private val suAdapter: SuAdapter, private val displayAdapter: DisplayAdapter, private val volumeAdapter: VolumeAdapter, - private val keyMapperImeMessenger: KeyMapperImeMessenger, + private val imeInputEventInjector: ImeInputEventInjector, private val accessibilityService: IAccessibilityService, private val shizukuInputEventInjector: InputEventInjector, private val permissionAdapter: PermissionAdapter, - private val phoneAdapter: PhoneAdapter, - private val inputMethodAdapter: InputMethodAdapter, ) : DetectKeyMapsUseCase, DetectMappingUseCase by detectMappingUseCase { @@ -97,23 +91,6 @@ class DetectKeyMapsUseCaseImpl( override val currentTime: Long get() = SystemClock.elapsedRealtime() - override val acceptKeyEventsFromIme: Boolean - get() { - if (!keyMapperImeHelper.isCompatibleImeChosen()) { - return false - } - - if (permissionAdapter.isGranted(Permission.READ_PHONE_STATE)) { - val callState = phoneAdapter.getCallState() - - return callState == CallState.IN_PHONE_CALL || callState == CallState.RINGING - } - - return false - } - - private val keyMapperImeHelper: KeyMapperImeHelper = KeyMapperImeHelper(inputMethodAdapter) - private val openMenuHelper = OpenMenuHelper( suAdapter, accessibilityService, @@ -156,7 +133,7 @@ class DetectKeyMapsUseCaseImpl( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> keyMapperImeMessenger.inputKeyEvent( + else -> imeInputEventInjector.inputKeyEvent( InputKeyModel( keyCode, inputEventType, @@ -181,8 +158,6 @@ interface DetectKeyMapsUseCase : DetectMappingUseCase { val defaultDoublePressDelay: Flow val defaultSequenceTriggerTimeout: Flow - val acceptKeyEventsFromIme: Boolean - val currentTime: Long fun imitateButtonPress( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt index 2ebd0ff17e..beb9f826eb 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt @@ -3,7 +3,8 @@ package io.github.sds100.keymapper.mappings.keymaps.detection import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.valueOrNull @@ -21,11 +22,7 @@ import timber.log.Timber class DetectScreenOffKeyEventsController( private val suAdapter: SuAdapter, private val devicesAdapter: DevicesAdapter, - private val onKeyEvent: suspend ( - keyCode: Int, - action: Int, - device: InputDeviceInfo, - ) -> Unit, + private val onKeyEvent: suspend (event: MyKeyEvent) -> Unit, ) { companion object { @@ -69,11 +66,12 @@ class DetectScreenOffKeyEventsController( var line: String? while (inputStream.bufferedReader().readLine() - .also { line = it } != null && isActive + .also { line = it } != null && + isActive ) { line ?: continue - KeyEventUtils.GET_EVENT_LABEL_TO_KEYCODE.forEach { (label, keyCode) -> + InputEventUtils.GET_EVENT_LABEL_TO_KEYCODE.forEach { (label, keyCode) -> if (line!!.contains(label)) { val deviceLocation = deviceLocationRegex.find(line!!)?.value ?: return@forEach @@ -85,17 +83,27 @@ class DetectScreenOffKeyEventsController( when (actionString) { "UP" -> { onKeyEvent.invoke( - keyCode, - KeyEvent.ACTION_UP, - device, + MyKeyEvent( + keyCode = keyCode, + action = KeyEvent.ACTION_UP, + device = device, + scanCode = 0, + metaState = 0, + repeatCount = 0, + ), ) } "DOWN" -> { onKeyEvent.invoke( - keyCode, - KeyEvent.ACTION_DOWN, - device, + MyKeyEvent( + keyCode = keyCode, + action = KeyEvent.ACTION_DOWN, + device = device, + scanCode = 0, + metaState = 0, + repeatCount = 0, + ), ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt new file mode 100644 index 0000000000..0162c06913 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt @@ -0,0 +1,134 @@ +package io.github.sds100.keymapper.mappings.keymaps.detection + +import android.view.KeyEvent +import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent + +/** + * See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad + * Some controllers send motion events as well as key events when DPAD buttons + * are pressed, while others just send key events. + * The motion events must be consumed but this means the following key events are also + * consumed so one must rely on converting these motion events oneself. + */ +class DpadMotionEventTracker { + companion object { + private const val DPAD_DOWN = 1 + private const val DPAD_UP = 2 + private const val DPAD_LEFT = 4 + private const val DPAD_RIGHT = 8 + } + + val dpadState: HashMap = hashMapOf() + + /** + * When moving the joysticks on a controller while pressing a DPAD button at the same time, + * DPAD key events can be interleaved in the DPAD motion events. We don't want to register + * these as multiple clicks so consume DPAD key events if the motion events say the DPAD + * is already pressed. + * + * @return whether to consume the key event. + */ + fun onKeyEvent(event: MyKeyEvent): Boolean { + event.device ?: return false + + if (!InputEventUtils.isDpadKeyCode(event.keyCode)) { + return false + } + + val dpadFlag = when (event.keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> DPAD_DOWN + KeyEvent.KEYCODE_DPAD_UP -> DPAD_UP + KeyEvent.KEYCODE_DPAD_LEFT -> DPAD_LEFT + KeyEvent.KEYCODE_DPAD_RIGHT -> DPAD_RIGHT + else -> return false + } + + val dpadState = dpadState[event.device.descriptor] ?: return false + + if (dpadState == 0) { + return false + } + + return dpadState and dpadFlag == dpadFlag + } + + /** + * The equivalent DPAD key events if any DPAD buttons changed in the motion event. + * There is a chance that one motion event will be sent if multiple axes change at the same time + * hence why it returns an array. + * + * @return An array of key events. Empty if no DPAD buttons changed. + */ + fun convertMotionEvent(event: MyMotionEvent): List { + val oldState = dpadState[event.device.descriptor] ?: 0 + val newState = eventToDpadState(event) + val diff = oldState xor newState + + dpadState[event.device.descriptor] = newState + + // If no dpad keys changed then return null + if (diff == 0) { + return emptyList() + } + + val keyCodes = mutableListOf() + + if (diff and DPAD_DOWN == DPAD_DOWN) { + keyCodes.add(KeyEvent.KEYCODE_DPAD_DOWN) + } + + if (diff and DPAD_UP == DPAD_UP) { + keyCodes.add(KeyEvent.KEYCODE_DPAD_UP) + } + + if (diff and DPAD_LEFT == DPAD_LEFT) { + keyCodes.add(KeyEvent.KEYCODE_DPAD_LEFT) + } + + if (diff and DPAD_RIGHT == DPAD_RIGHT) { + keyCodes.add(KeyEvent.KEYCODE_DPAD_RIGHT) + } + + // If the new state contains the dpad press then it has just been pressed down. + val action = if (newState and diff == diff) { + KeyEvent.ACTION_DOWN + } else { + KeyEvent.ACTION_UP + } + + return keyCodes.map { + MyKeyEvent( + it, + action, + metaState = event.metaState, + scanCode = 0, + device = event.device, + repeatCount = 0, + ) + } + } + + fun reset() { + dpadState.clear() + } + + private fun eventToDpadState(event: MyMotionEvent): Int { + var state = 0 + + if (event.axisHatX == -1.0f) { + state = DPAD_LEFT + } else if (event.axisHatX == 1.0f) { + state = DPAD_RIGHT + } + + if (event.axisHatY == -1.0f) { + state = state or DPAD_UP + } else if (event.axisHatY == 1.0f) { + state = state or DPAD_DOWN + } + + return state + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt index 9243c103c1..f642e72f9e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt @@ -5,7 +5,7 @@ import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.actions.RepeatMode import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.mappings.keymaps.KeyMapAction -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.InputEventType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -113,7 +113,7 @@ class ParallelTriggerActionPerformer( return@forEachIndexed } - if (action.data is ActionData.InputKeyEvent && KeyEventUtils.isModifierKey(action.data.keyCode)) { + if (action.data is ActionData.InputKeyEvent && InputEventUtils.isModifierKey(action.data.keyCode)) { return@forEachIndexed } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt index e9e6f11bcf..d2feae2313 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt @@ -13,12 +13,13 @@ import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.mappings.keymaps.KeyMap import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.mapData +import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.NavigationViewModel import io.github.sds100.keymapper.util.ui.NavigationViewModelImpl import io.github.sds100.keymapper.util.ui.PopupUi @@ -62,6 +63,7 @@ abstract class BaseConfigTriggerViewModel( private val recordTrigger: RecordTriggerUseCase, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, + private val setupGuiKeyboard: SetupGuiKeyboardUseCase, resourceProvider: ResourceProvider, ) : ResourceProvider by resourceProvider, PopupViewModel by PopupViewModelImpl(), @@ -84,7 +86,7 @@ abstract class BaseConfigTriggerViewModel( val recordTriggerState: StateFlow = recordTrigger.state.stateIn( coroutineScope, SharingStarted.Lazily, - RecordTriggerState.Stopped, + RecordTriggerState.Idle, ) val triggerModeButtonsEnabled: StateFlow = config.mapping.map { state -> @@ -204,6 +206,27 @@ abstract class BaseConfigTriggerViewModel( val fixAppKilling = _fixAppKilling.asSharedFlow() var showAdvancedTriggersBottomSheet: Boolean by mutableStateOf(false) + var showDpadTriggerSetupBottomSheet: Boolean by mutableStateOf(false) + var showNoKeysRecordedBottomSheet: Boolean by mutableStateOf(false) + + val setupGuiKeyboardState: StateFlow = combine( + setupGuiKeyboard.isInstalled, + setupGuiKeyboard.isEnabled, + setupGuiKeyboard.isChosen, + ) { isInstalled, isEnabled, isChosen -> + SetupGuiKeyboardState( + isInstalled, + isEnabled, + isChosen, + ) + }.stateIn(coroutineScope, SharingStarted.Lazily, SetupGuiKeyboardState.DEFAULT) + + /** + * Check whether the user stopped the trigger recording countdown. This + * distinction is important so that the bottom sheet describing what to do + * when no buttons are recorded is not shown. + */ + private var isRecordingCompletionUserInitiated: Boolean = false init { val rebuildErrorList = MutableSharedFlow>(replay = 1) @@ -234,25 +257,11 @@ abstract class BaseConfigTriggerViewModel( } } - recordTrigger.onRecordKey.onEach { - if (it.keyCode == KeyEvent.KEYCODE_CAPS_LOCK) { - val dialog = PopupUi.Ok( - message = getString(R.string.dialog_message_enable_physical_keyboard_caps_lock_a_keyboard_layout), - ) - - showPopup("caps_lock_message", dialog) - } - - if (it.keyCode == KeyEvent.KEYCODE_BACK) { - val dialog = PopupUi.Ok( - message = getString(R.string.dialog_message_screen_pinning_warning), - ) - - showPopup("screen_pinning_message", dialog) + coroutineScope.launch { + recordTrigger.onRecordKey.collectLatest { + onRecordTriggerKey(it) } - - config.addKeyCodeTriggerKey(it.keyCode, it.device) - }.launchIn(coroutineScope) + } coroutineScope.launch { config.mapping @@ -260,63 +269,128 @@ abstract class BaseConfigTriggerViewModel( .distinctUntilChanged() .drop(1) .collectLatest { mode -> - if (mode is TriggerMode.Parallel) { - if (onboarding.shownParallelTriggerOrderExplanation) return@collectLatest + onTriggerModeChanged(mode) + } + } - val dialog = PopupUi.Ok( - message = getString(R.string.dialog_message_parallel_trigger_order), - ) + recordTrigger.state.onEach { state -> + if (state is RecordTriggerState.Completed && + state.recordedKeys.isEmpty() && + onboarding.showNoKeysDetectedBottomSheet.first() && + !isRecordingCompletionUserInitiated + ) { + showNoKeysRecordedBottomSheet = true + } - showPopup("parallel_trigger_order", dialog) ?: return@collectLatest + // reset this field when recording has completed + isRecordingCompletionUserInitiated = false + }.launchIn(coroutineScope) + } - onboarding.shownParallelTriggerOrderExplanation = true - } + private suspend fun onTriggerModeChanged(mode: TriggerMode) { + if (mode is TriggerMode.Parallel) { + if (onboarding.shownParallelTriggerOrderExplanation) { + return + } + + val dialog = PopupUi.Ok( + message = getString(R.string.dialog_message_parallel_trigger_order), + ) - if (mode is TriggerMode.Sequence) { - if (onboarding.shownSequenceTriggerExplanation) return@collectLatest + showPopup("parallel_trigger_order", dialog) ?: return - val dialog = PopupUi.Ok( - message = getString(R.string.dialog_message_sequence_trigger_explanation), - ) + onboarding.shownParallelTriggerOrderExplanation = true + } - showPopup("sequence_trigger_explanation", dialog) - ?: return@collectLatest + if (mode is TriggerMode.Sequence) { + if (onboarding.shownSequenceTriggerExplanation) { + return + } - onboarding.shownSequenceTriggerExplanation = true - } - } + val dialog = PopupUi.Ok( + message = getString(R.string.dialog_message_sequence_trigger_explanation), + ) + + showPopup("sequence_trigger_explanation", dialog) + ?: return + + onboarding.shownSequenceTriggerExplanation = true } } - private fun buildTriggerErrorListItems(triggerErrors: List): List = - triggerErrors.map { error -> - when (error) { - TriggerError.DND_ACCESS_DENIED -> TextListItem.Error( - id = error.toString(), - text = getString(R.string.trigger_error_dnd_access_denied), - ) + private suspend fun onRecordTriggerKey(key: RecordedKey) { + // Add the trigger key before showing the dialog so it doesn't + // need to be dismissed before it is added. + config.addKeyCodeTriggerKey(key.keyCode, key.device, key.detectionSource) - TriggerError.SCREEN_OFF_ROOT_DENIED -> TextListItem.Error( - id = error.toString(), - text = getString(R.string.trigger_error_screen_off_root_permission_denied), - ) + if (key.keyCode == KeyEvent.KEYCODE_CAPS_LOCK) { + val dialog = PopupUi.Ok( + message = getString(R.string.dialog_message_enable_physical_keyboard_caps_lock_a_keyboard_layout), + ) - TriggerError.CANT_DETECT_IN_PHONE_CALL -> TextListItem.Error( - id = error.toString(), - text = getString(R.string.trigger_error_cant_detect_in_phone_call), - ) + showPopup("caps_lock_message", dialog) + } - TriggerError.ASSISTANT_NOT_SELECTED -> TextListItem.Error( - id = error.toString(), - text = getString(R.string.trigger_error_assistant_activity_not_chosen), - ) + if (key.keyCode == KeyEvent.KEYCODE_BACK) { + val dialog = PopupUi.Ok( + message = getString(R.string.dialog_message_screen_pinning_warning), + ) - TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> TextListItem.Error( - id = error.toString(), - text = getString(R.string.trigger_error_assistant_not_purchased), - ) + showPopup("screen_pinning_message", dialog) + } + + // Issue #491. Some key codes can only be detected through an input method. This will + // be shown to the user by showing a keyboard icon next to the trigger key name so + // explain this to the user. + if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && displayKeyMap.showTriggerKeyboardIconExplanation.first()) { + val dialog = PopupUi.Dialog( + title = getString(R.string.dialog_title_keyboard_icon_means_ime_detection), + message = getString(R.string.dialog_message_keyboard_icon_means_ime_detection), + negativeButtonText = getString(R.string.neg_dont_show_again), + positiveButtonText = getString(R.string.pos_ok), + ) + + val response = showPopup("keyboard_icon_explanation", dialog) + + if (response == DialogResponse.NEGATIVE) { + displayKeyMap.neverShowTriggerKeyboardIconExplanation() } } + } + + private fun buildTriggerErrorListItems(triggerErrors: List): List = triggerErrors.map { error -> + when (error) { + TriggerError.DND_ACCESS_DENIED -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_dnd_access_denied), + ) + + TriggerError.SCREEN_OFF_ROOT_DENIED -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_screen_off_root_permission_denied), + ) + + TriggerError.CANT_DETECT_IN_PHONE_CALL -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_cant_detect_in_phone_call), + ) + + TriggerError.ASSISTANT_NOT_SELECTED -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_assistant_activity_not_chosen), + ) + + TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_assistant_not_purchased), + ) + + TriggerError.DPAD_IME_NOT_SELECTED -> TextListItem.Error( + id = error.toString(), + text = getString(R.string.trigger_error_dpad_ime_not_selected), + ) + } + } fun onParallelRadioButtonCheckedChange(isChecked: Boolean) { if (isChecked) { @@ -394,16 +468,21 @@ abstract class BaseConfigTriggerViewModel( val recordTriggerState = recordTrigger.state.firstOrNull() ?: return@launch val result = when (recordTriggerState) { - is RecordTriggerState.CountingDown -> recordTrigger.stopRecording() - RecordTriggerState.Stopped -> recordTrigger.startRecording() + is RecordTriggerState.CountingDown -> { + isRecordingCompletionUserInitiated = true + recordTrigger.stopRecording() + } + + is RecordTriggerState.Completed, + RecordTriggerState.Idle, + -> recordTrigger.startRecording() } if (result is Error.AccessibilityServiceDisabled) { - ViewModelHelper.handleAccessibilityServiceStoppedSnackBar( + ViewModelHelper.handleAccessibilityServiceStoppedDialog( resourceProvider = this@BaseConfigTriggerViewModel, popupViewModel = this@BaseConfigTriggerViewModel, startService = displayKeyMap::startAccessibilityService, - message = R.string.dialog_message_enable_accessibility_service_to_record_trigger, ) } @@ -418,12 +497,6 @@ abstract class BaseConfigTriggerViewModel( } } - fun stopRecordingTrigger() { - coroutineScope.launch { - recordTrigger.stopRecording() - } - } - fun onTriggerErrorClick(listItemId: String) { coroutineScope.launch { when (TriggerError.valueOf(listItemId)) { @@ -431,7 +504,7 @@ abstract class BaseConfigTriggerViewModel( ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@BaseConfigTriggerViewModel, popupViewModel = this@BaseConfigTriggerViewModel, - neverShowDndTriggerErrorAgain = { displayKeyMap.neverShowDndTriggerErrorAgain() }, + neverShowDndTriggerErrorAgain = { displayKeyMap.neverShowDndTriggerError() }, fixError = { displayKeyMap.fixError(it) }, ) } @@ -452,6 +525,10 @@ abstract class BaseConfigTriggerViewModel( TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> { showAdvancedTriggersBottomSheet = true } + + TriggerError.DPAD_IME_NOT_SELECTED -> { + showDpadTriggerSetupBottomSheet = true + } } } } @@ -459,31 +536,30 @@ abstract class BaseConfigTriggerViewModel( private fun createListItems( trigger: Trigger, showDeviceDescriptors: Boolean, - ): List = - trigger.keys.mapIndexed { index, key -> - val clickTypeString = when (key.clickType) { - ClickType.SHORT_PRESS -> null - ClickType.LONG_PRESS -> getString(R.string.clicktype_long_press) - ClickType.DOUBLE_PRESS -> getString(R.string.clicktype_double_press) - } - - val linkDrawable = when { - trigger.mode is TriggerMode.Parallel && index < trigger.keys.lastIndex -> TriggerKeyLinkType.PLUS - trigger.mode is TriggerMode.Sequence && index < trigger.keys.lastIndex -> TriggerKeyLinkType.ARROW - else -> TriggerKeyLinkType.HIDDEN - } + ): List = trigger.keys.mapIndexed { index, key -> + val clickTypeString = when (key.clickType) { + ClickType.SHORT_PRESS -> null + ClickType.LONG_PRESS -> getString(R.string.clicktype_long_press) + ClickType.DOUBLE_PRESS -> getString(R.string.clicktype_double_press) + } - TriggerKeyListItem( - id = key.uid, - name = getTriggerKeyName(key), - clickTypeString = clickTypeString, - extraInfo = getTriggerKeyExtraInfo(key, showDeviceDescriptors), - linkType = linkDrawable, - isDragDropEnabled = trigger.keys.size > 1, - isChooseDeviceButtonVisible = key is KeyCodeTriggerKey, - ) + val linkDrawable = when { + trigger.mode is TriggerMode.Parallel && index < trigger.keys.lastIndex -> TriggerKeyLinkType.PLUS + trigger.mode is TriggerMode.Sequence && index < trigger.keys.lastIndex -> TriggerKeyLinkType.ARROW + else -> TriggerKeyLinkType.HIDDEN } + TriggerKeyListItem( + id = key.uid, + name = getTriggerKeyName(key), + clickTypeString = clickTypeString, + extraInfo = getTriggerKeyExtraInfo(key, showDeviceDescriptors), + linkType = linkDrawable, + isDragDropEnabled = trigger.keys.size > 1, + isChooseDeviceButtonVisible = key is KeyCodeTriggerKey, + ) + } + private fun getTriggerKeyExtraInfo(key: TriggerKey, showDeviceDescriptors: Boolean): String? { if (key !is KeyCodeTriggerKey) { return null @@ -491,22 +567,31 @@ abstract class BaseConfigTriggerViewModel( return buildString { append(getTriggerKeyDeviceName(key.device, showDeviceDescriptors)) + val midDot = getString(R.string.middot) if (!key.consumeEvent) { - val midDot = getString(R.string.middot) append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") } } } - private fun getTriggerKeyName(key: TriggerKey): String = when (key) { - is AssistantTriggerKey -> when (key.type) { - AssistantTriggerType.ANY -> getString(R.string.assistant_any_trigger_name) - AssistantTriggerType.VOICE -> getString(R.string.assistant_voice_trigger_name) - AssistantTriggerType.DEVICE -> getString(R.string.assistant_device_trigger_name) - } + private fun getTriggerKeyName(key: TriggerKey): String { + return when (key) { + is AssistantTriggerKey -> when (key.type) { + AssistantTriggerType.ANY -> getString(R.string.assistant_any_trigger_name) + AssistantTriggerType.VOICE -> getString(R.string.assistant_voice_trigger_name) + AssistantTriggerType.DEVICE -> getString(R.string.assistant_device_trigger_name) + } + + is KeyCodeTriggerKey -> buildString { + append(InputEventUtils.keyCodeToString(key.keyCode)) - is KeyCodeTriggerKey -> KeyEventUtils.keyCodeToString(key.keyCode) + if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + val midDot = getString(R.string.middot) + append(" $midDot ${getString(R.string.flag_detect_from_input_method)}") + } + } + } } private fun getTriggerKeyDeviceName( @@ -526,4 +611,20 @@ abstract class BaseConfigTriggerViewModel( } } } + + fun onEnableGuiKeyboardClick() { + setupGuiKeyboard.enableInputMethod() + } + + fun onChooseGuiKeyboardClick() { + setupGuiKeyboard.chooseInputMethod() + } + + fun onNeverShowSetupDpadClick() { + displayKeyMap.neverShowDpadImeSetupError() + } + + fun onNeverShowNoKeysRecordedClick() { + onboarding.neverShowNoKeysRecordedBottomSheet() + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt index 5c5bdcd926..78e15ab996 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt @@ -15,6 +15,7 @@ data class KeyCodeTriggerKey( val device: TriggerKeyDevice, override val clickType: ClickType, override val consumeEvent: Boolean = true, + val detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ) : TriggerKey() { override fun toString(): String { @@ -27,25 +28,42 @@ data class KeyCodeTriggerKey( } companion object { - fun fromEntity(entity: KeyCodeTriggerKeyEntity): TriggerKey = KeyCodeTriggerKey( - uid = entity.uid, - keyCode = entity.keyCode, - device = when (entity.deviceId) { + fun fromEntity(entity: KeyCodeTriggerKeyEntity): TriggerKey { + val device = when (entity.deviceId) { KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> TriggerKeyDevice.Internal KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> TriggerKeyDevice.Any else -> TriggerKeyDevice.External( entity.deviceId, entity.deviceName ?: "", ) - }, - clickType = when (entity.clickType) { + } + + val clickType = when (entity.clickType) { TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS else -> ClickType.SHORT_PRESS - }, - consumeEvent = !entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT), - ) + } + + val consumeEvent = + !entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + + val detectionSource = + if (entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD)) { + KeyEventDetectionSource.INPUT_METHOD + } else { + KeyEventDetectionSource.ACCESSIBILITY_SERVICE + } + + return KeyCodeTriggerKey( + uid = entity.uid, + keyCode = entity.keyCode, + device = device, + clickType = clickType, + consumeEvent = consumeEvent, + detectionSource = detectionSource, + ) + } fun toEntity(key: KeyCodeTriggerKey): KeyCodeTriggerKeyEntity { val deviceId = when (key.device) { @@ -72,6 +90,10 @@ data class KeyCodeTriggerKey( flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) } + if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) + } + return KeyCodeTriggerKeyEntity( keyCode = key.keyCode, deviceId = deviceId, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt new file mode 100644 index 0000000000..b1624e368f --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt @@ -0,0 +1,6 @@ +package io.github.sds100.keymapper.mappings.keymaps.trigger + +enum class KeyEventDetectionSource { + ACCESSIBILITY_SERVICE, + INPUT_METHOD, +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt index 1a62fb1585..5f37e9b6d1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt @@ -85,7 +85,7 @@ private fun RecordTriggerButtonRow( AdvancedTriggersButton( modifier = Modifier.weight(1f), - isEnabled = recordTriggerState is RecordTriggerState.Stopped, + isEnabled = recordTriggerState !is RecordTriggerState.CountingDown, onClick = onAdvancedTriggersClick, ) } @@ -106,7 +106,7 @@ private fun RecordTriggerButton( is RecordTriggerState.CountingDown -> stringResource(R.string.button_recording_trigger_countdown, state.timeLeft) - RecordTriggerState.Stopped -> + else -> stringResource(R.string.button_record_trigger) } @@ -172,7 +172,7 @@ private fun PreviewStopped() { Surface { RecordTriggerButtonRow( modifier = Modifier.fillMaxWidth(), - recordTriggerState = RecordTriggerState.Stopped, + recordTriggerState = RecordTriggerState.Idle, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt index 3b76a98a68..2a0c34f462 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt @@ -4,6 +4,8 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger * Created by sds100 on 04/03/2021. */ sealed class RecordTriggerState { + data object Idle : RecordTriggerState() + data class CountingDown( /** * The time left in seconds @@ -11,5 +13,5 @@ sealed class RecordTriggerState { val timeLeft: Int, ) : RecordTriggerState() - object Stopped : RecordTriggerState() + data class Completed(val recordedKeys: List) : RecordTriggerState() } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt index 2b9eb15288..866999a264 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt @@ -1,14 +1,21 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger +import android.view.KeyEvent import io.github.sds100.keymapper.system.accessibility.ServiceAdapter +import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.devices.InputDeviceUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.ServiceEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch /** * Created by sds100 on 04/03/2021. @@ -17,28 +24,17 @@ class RecordTriggerController( private val coroutineScope: CoroutineScope, private val serviceAdapter: ServiceAdapter, ) : RecordTriggerUseCase { - override val state = MutableStateFlow(RecordTriggerState.Stopped) + override val state = MutableStateFlow(RecordTriggerState.Idle) - override val onRecordKey = serviceAdapter.eventReceiver.mapNotNull { event -> - when (event) { - is ServiceEvent.RecordedTriggerKey -> { - val device = if (event.device != null && event.device.isExternal) { - TriggerKeyDevice.External(event.device.descriptor, event.device.name) - } else { - TriggerKeyDevice.Internal - } - - RecordedKey(event.keyCode, device) - } - - else -> null - } - } + private val recordedKeys: MutableList = mutableListOf() + override val onRecordKey: MutableSharedFlow = MutableSharedFlow() init { serviceAdapter.eventReceiver.onEach { event -> when (event) { - is ServiceEvent.OnStoppedRecordingTrigger -> state.value = RecordTriggerState.Stopped + is ServiceEvent.OnStoppedRecordingTrigger -> + state.value = + RecordTriggerState.Completed(recordedKeys) is ServiceEvent.OnIncrementRecordTriggerTimer -> state.value = @@ -47,13 +43,82 @@ class RecordTriggerController( else -> Unit } }.launchIn(coroutineScope) + + serviceAdapter.eventReceiver + .mapNotNull { + if (it is ServiceEvent.RecordedTriggerKey) { + it + } else { + null + } + } + .map { createRecordedKeyEvent(it.keyCode, it.device, it.detectionSource) } + .onEach { key -> + recordedKeys.add(key) + onRecordKey.emit(key) + } + .launchIn(coroutineScope) + } + + override suspend fun startRecording(): Result<*> { + recordedKeys.clear() + return serviceAdapter.send(ServiceEvent.StartRecordingTrigger) } - override suspend fun startRecording(): Result<*> = - serviceAdapter.send(ServiceEvent.StartRecordingTrigger) + override suspend fun stopRecording(): Result<*> { + return serviceAdapter.send(ServiceEvent.StopRecordingTrigger) + } - override suspend fun stopRecording(): Result<*> = - serviceAdapter.send(ServiceEvent.StopRecordingTrigger) + /** + * Process key events from the activity so that DPAD buttons can be recorded + * even when the Key Mapper IME is not being used. + * @return Whether the key event is consumed. + */ + fun onRecordKeyFromActivity(event: KeyEvent): Boolean { + // Only consume the key event if the app is recording a trigger. + if (state.value !is RecordTriggerState.CountingDown) { + return false + } + + if (!InputEventUtils.isDpadKeyCode(event.keyCode)) { + return false + } + + // If it is the up key event then consume it and record the key. Do not record the key + // on the down event because the down key event may be consumed by a View and so + // onKeyDown will not be called in the activity. This can be reproduced by navigating + // to the TriggerFragment by touch and then pressing a DPAD button will cause the View + // consume the key event and get focus. + if (event.action == KeyEvent.ACTION_UP) { + val device = InputDeviceUtils.createInputDeviceInfo(event.device) + val recordedKey = createRecordedKeyEvent( + event.keyCode, + device, + KeyEventDetectionSource.INPUT_METHOD, + ) + + recordedKeys.add(recordedKey) + coroutineScope.launch { + onRecordKey.emit(recordedKey) + } + } + + return true + } + + private fun createRecordedKeyEvent( + keyCode: Int, + device: InputDeviceInfo?, + detectionSource: KeyEventDetectionSource, + ): RecordedKey { + val triggerKeyDevice = if (device != null && device.isExternal) { + TriggerKeyDevice.External(device.descriptor, device.name) + } else { + TriggerKeyDevice.Internal + } + + return RecordedKey(keyCode, triggerKeyDevice, detectionSource) + } } interface RecordTriggerUseCase { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt index 24e8f74312..a6eb4e92d9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt @@ -6,4 +6,5 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger data class RecordedKey( val keyCode: Int, val device: TriggerKeyDevice, + val detectionSource: KeyEventDetectionSource, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt new file mode 100644 index 0000000000..c083051554 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt @@ -0,0 +1,336 @@ +package io.github.sds100.keymapper.mappings.keymaps.trigger + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DpadTriggerSetupBottomSheet( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, + guiKeyboardState: SetupGuiKeyboardState, + onEnableKeyboardClick: () -> Unit = {}, + onChooseKeyboardClick: () -> Unit = {}, + onNeverShowAgainClick: () -> Unit = {}, + sheetState: SheetState, +) { + SetupGuiKeyboardBottomSheet( + modifier, + guiKeyboardState = guiKeyboardState, + sheetState = sheetState, + onDismissRequest = onDismissRequest, + onEnableKeyboardClick = onEnableKeyboardClick, + onChooseKeyboardClick = onChooseKeyboardClick, + onNeverShowAgainClick = onNeverShowAgainClick, + title = stringResource(R.string.dpad_trigger_setup_bottom_sheet_title), + text = stringResource(R.string.dpad_trigger_setup_bottom_sheet_text), + setupCompleteText = stringResource(R.string.dpad_trigger_setup_setup_complete_text), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NoKeysRecordedBottomSheet( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, + viewModel: ConfigTriggerViewModel, + sheetState: SheetState, +) { + val state by viewModel.setupGuiKeyboardState.collectAsStateWithLifecycle() + + SetupGuiKeyboardBottomSheet( + modifier = modifier, + guiKeyboardState = state, + sheetState = sheetState, + onDismissRequest = onDismissRequest, + onEnableKeyboardClick = viewModel::onEnableGuiKeyboardClick, + onChooseKeyboardClick = viewModel::onChooseGuiKeyboardClick, + onNeverShowAgainClick = viewModel::onNeverShowNoKeysRecordedClick, + title = stringResource(R.string.no_keys_recorded_bottom_sheet_title), + text = stringResource(R.string.no_keys_recorded_bottom_sheet_text), + setupCompleteText = stringResource(R.string.no_keys_recorded_setup_complete_text), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SetupGuiKeyboardBottomSheet( + modifier: Modifier = Modifier, + guiKeyboardState: SetupGuiKeyboardState, + sheetState: SheetState, + onDismissRequest: () -> Unit, + onEnableKeyboardClick: () -> Unit = {}, + onChooseKeyboardClick: () -> Unit = {}, + onNeverShowAgainClick: () -> Unit = {}, + title: String, + text: String, + setupCompleteText: String, +) { + val scope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current + val scrollState = rememberScrollState() + + ModalBottomSheet( + modifier = modifier, + onDismissRequest = onDismissRequest, + sheetState = sheetState, + // Hide drag handle because other bottom sheets don't have it + dragHandle = {}, + ) { + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + textAlign = TextAlign.Center, + text = title, + style = MaterialTheme.typography.headlineMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = text, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + val guiKeyboardUrl = stringResource(R.string.url_play_store_keymapper_gui_keyboard) + StepRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + isEnabled = !guiKeyboardState.isKeyboardInstalled, + rowText = stringResource(R.string.setup_gui_keyboard_install_keyboard_text), + buttonTextEnabled = stringResource(R.string.setup_gui_keyboard_install_keyboard_button), + buttonTextDisabled = stringResource(R.string.setup_gui_keyboard_install_keyboard_button_disabled), + onButtonClick = { + uriHandler.openUri(guiKeyboardUrl) + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + StepRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + isEnabled = !guiKeyboardState.isKeyboardEnabled, + rowText = stringResource(R.string.setup_gui_keyboard_enable_keyboard_text), + buttonTextEnabled = stringResource(R.string.setup_gui_keyboard_enable_keyboard_button), + buttonTextDisabled = stringResource(R.string.setup_gui_keyboard_enable_keyboard_button_disabled), + onButtonClick = onEnableKeyboardClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + StepRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + isEnabled = !guiKeyboardState.isKeyboardChosen, + rowText = stringResource(R.string.setup_gui_keyboard_choose_keyboard_text), + buttonTextEnabled = stringResource(R.string.setup_gui_keyboard_choose_keyboard_button), + buttonTextDisabled = stringResource(R.string.setup_gui_keyboard_choose_keyboard_button_disabled), + onButtonClick = onChooseKeyboardClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (guiKeyboardState.isKeyboardInstalled && guiKeyboardState.isKeyboardEnabled && guiKeyboardState.isKeyboardChosen) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = setupCompleteText, + style = MaterialTheme.typography.bodyLarge, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + onClick = { + onNeverShowAgainClick() + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.pos_never_show_again)) + } + + Button( + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.pos_done)) + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@Composable +private fun StepRow( + modifier: Modifier = Modifier, + isEnabled: Boolean, + rowText: String, + buttonTextEnabled: String, + buttonTextDisabled: String, + onButtonClick: () -> Unit, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = rowText, + fontWeight = FontWeight.Medium, + ) + + FilledTonalButton( + onClick = onButtonClick, + enabled = isEnabled, + ) { + val text = if (isEnabled) { + buttonTextEnabled + } else { + buttonTextDisabled + } + + Text(text) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewDpad() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + SetupGuiKeyboardBottomSheet( + onDismissRequest = {}, + guiKeyboardState = SetupGuiKeyboardState( + isKeyboardInstalled = true, + isKeyboardEnabled = false, + isKeyboardChosen = false, + ), + sheetState = sheetState, + title = stringResource(R.string.dpad_trigger_setup_bottom_sheet_title), + text = stringResource(R.string.dpad_trigger_setup_bottom_sheet_text), + setupCompleteText = stringResource(R.string.dpad_trigger_setup_setup_complete_text), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewDpadComplete() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + SetupGuiKeyboardBottomSheet( + onDismissRequest = {}, + guiKeyboardState = SetupGuiKeyboardState( + isKeyboardInstalled = true, + isKeyboardEnabled = true, + isKeyboardChosen = true, + ), + sheetState = sheetState, + title = stringResource(R.string.dpad_trigger_setup_bottom_sheet_title), + text = stringResource(R.string.dpad_trigger_setup_bottom_sheet_text), + setupCompleteText = stringResource(R.string.dpad_trigger_setup_setup_complete_text), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewNoKeyRecordedComplete() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + SetupGuiKeyboardBottomSheet( + onDismissRequest = {}, + guiKeyboardState = SetupGuiKeyboardState( + isKeyboardInstalled = true, + isKeyboardEnabled = true, + isKeyboardChosen = true, + ), + sheetState = sheetState, + title = stringResource(R.string.no_keys_recorded_bottom_sheet_title), + text = stringResource(R.string.no_keys_recorded_bottom_sheet_text), + setupCompleteText = stringResource(R.string.no_keys_recorded_setup_complete_text), + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt new file mode 100644 index 0000000000..4170cdb35a --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt @@ -0,0 +1,15 @@ +package io.github.sds100.keymapper.mappings.keymaps.trigger + +data class SetupGuiKeyboardState( + val isKeyboardInstalled: Boolean, + val isKeyboardEnabled: Boolean, + val isKeyboardChosen: Boolean, +) { + companion object { + val DEFAULT = SetupGuiKeyboardState( + isKeyboardInstalled = false, + isKeyboardEnabled = false, + isKeyboardChosen = false, + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt new file mode 100644 index 0000000000..24f23e75ba --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt @@ -0,0 +1,66 @@ +package io.github.sds100.keymapper.mappings.keymaps.trigger + +import io.github.sds100.keymapper.system.apps.PackageInfo +import io.github.sds100.keymapper.system.apps.PackageManagerAdapter +import io.github.sds100.keymapper.system.inputmethod.ImeInfo +import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.onSuccess +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +class SetupGuiKeyboardUseCaseImpl( + private val inputMethodAdapter: InputMethodAdapter, + private val packageManager: PackageManagerAdapter, +) : SetupGuiKeyboardUseCase { + private val guiKeyboardPackage: Flow = + packageManager.installedPackages + .mapNotNull { it as? State.Data } + .map { packages -> packages.data.find { it.packageName == KeyMapperImeHelper.KEY_MAPPER_GUI_IME_PACKAGE } } + + override val isInstalled: Flow = guiKeyboardPackage.map { it != null } + + override val isEnabled: Flow = + getGuiKeyboardImeInfoFlow().map { it?.isEnabled ?: false } + + override val isCompatibleVersion: Flow = + guiKeyboardPackage.map { packageInfo -> + if (packageInfo == null) { + false + } else { + packageInfo.versionCode >= KeyMapperImeHelper.MIN_SUPPORTED_GUI_KEYBOARD_VERSION_CODE + } + } + + override fun enableInputMethod() { + inputMethodAdapter.getInfoByPackageName(KeyMapperImeHelper.KEY_MAPPER_GUI_IME_PACKAGE) + .onSuccess { + inputMethodAdapter.enableIme(it.id) + } + } + + override val isChosen: Flow = + getGuiKeyboardImeInfoFlow().map { it?.isChosen ?: false } + + override fun chooseInputMethod() { + inputMethodAdapter.showImePicker(fromForeground = true) + } + + private fun getGuiKeyboardImeInfoFlow(): Flow { + return inputMethodAdapter.inputMethods.map { list -> list.find { it.packageName == KeyMapperImeHelper.KEY_MAPPER_GUI_IME_PACKAGE } } + } +} + +interface SetupGuiKeyboardUseCase { + val isInstalled: Flow + + val isEnabled: Flow + fun enableInputMethod() + + val isChosen: Flow + fun chooseInputMethod() + + val isCompatibleVersion: Flow +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt index 0bc2f1f99c..0a201774ad 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt @@ -4,7 +4,7 @@ import io.github.sds100.keymapper.data.entities.Extra import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.getData import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.valueOrNull import kotlinx.serialization.Serializable import splitties.bitflags.hasFlag @@ -32,15 +32,12 @@ data class Trigger( fun isChangingVibrationDurationAllowed(): Boolean = vibrate || longPressDoubleVibration - fun isChangingLongPressDelayAllowed(): Boolean = - keys.any { key -> key.clickType == ClickType.LONG_PRESS } + fun isChangingLongPressDelayAllowed(): Boolean = keys.any { key -> key.clickType == ClickType.LONG_PRESS } - fun isChangingDoublePressDelayAllowed(): Boolean = - keys.any { key -> key.clickType == ClickType.DOUBLE_PRESS } + fun isChangingDoublePressDelayAllowed(): Boolean = keys.any { key -> key.clickType == ClickType.DOUBLE_PRESS } - fun isLongPressDoubleVibrationAllowed(): Boolean = - (keys.size == 1 || (mode is TriggerMode.Parallel)) && - keys.getOrNull(0)?.clickType == ClickType.LONG_PRESS + fun isLongPressDoubleVibrationAllowed(): Boolean = (keys.size == 1 || (mode is TriggerMode.Parallel)) && + keys.getOrNull(0)?.clickType == ClickType.LONG_PRESS /** * Must check that it is not empty otherwise it would be true from the "all" check. @@ -50,12 +47,11 @@ data class Trigger( fun isDetectingWhenScreenOffAllowed(): Boolean { return keys.isNotEmpty() && keys.all { - it is KeyCodeTriggerKey && KeyEventUtils.canDetectKeyWhenScreenOff(it.keyCode) + it is KeyCodeTriggerKey && InputEventUtils.canDetectKeyWhenScreenOff(it.keyCode) } } - fun isChangingSequenceTriggerTimeoutAllowed(): Boolean = - keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence + fun isChangingSequenceTriggerTimeoutAllowed(): Boolean = keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence } object TriggerEntityMapper { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt index 13a35842a5..edd3dc7025 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt @@ -15,4 +15,9 @@ enum class TriggerError { // This error appears when a key map has an assistant trigger but the user hasn't purchased // the product. ASSISTANT_TRIGGER_NOT_PURCHASED, + + /** + * A Key Mapper IME must be used for DPAD triggers to work. + */ + DPAD_IME_NOT_SELECTED, } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt index 9564b287e7..15d8dda920 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt @@ -4,9 +4,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.ItemTouchHelper import com.airbnb.epoxy.EpoxyController @@ -52,21 +57,49 @@ class TriggerFragment : RecyclerViewFragment>> get() = configTriggerViewModel.triggerKeyListItems - override fun bind(inflater: LayoutInflater, container: ViewGroup?) = - FragmentTriggerBinding.inflate(inflater, container, false).apply { - lifecycleOwner = viewLifecycleOwner - - composeViewRecordTriggerButtons.apply { - // Dispose of the Composition when the view's LifecycleOwner - // is destroyed - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - KeyMapperTheme { - RecordTriggerButtonRow(Modifier.fillMaxWidth(), configTriggerViewModel) + @OptIn(ExperimentalMaterial3Api::class) + override fun bind(inflater: LayoutInflater, container: ViewGroup?) = FragmentTriggerBinding.inflate(inflater, container, false).apply { + lifecycleOwner = viewLifecycleOwner + + composeViewRecordTriggerButtons.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + KeyMapperTheme { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val state by configTriggerViewModel.setupGuiKeyboardState.collectAsStateWithLifecycle() + + if (configTriggerViewModel.showDpadTriggerSetupBottomSheet) { + DpadTriggerSetupBottomSheet( + modifier = Modifier.systemBarsPadding(), + onDismissRequest = { + configTriggerViewModel.showDpadTriggerSetupBottomSheet = false + }, + guiKeyboardState = state, + onEnableKeyboardClick = configTriggerViewModel::onEnableGuiKeyboardClick, + onChooseKeyboardClick = configTriggerViewModel::onChooseGuiKeyboardClick, + onNeverShowAgainClick = configTriggerViewModel::onNeverShowSetupDpadClick, + sheetState = sheetState, + ) } + + if (configTriggerViewModel.showNoKeysRecordedBottomSheet) { + NoKeysRecordedBottomSheet( + modifier = Modifier.systemBarsPadding(), + onDismissRequest = { + configTriggerViewModel.showNoKeysRecordedBottomSheet = false + }, + viewModel = configTriggerViewModel, + sheetState = sheetState, + ) + } + + RecordTriggerButtonRow(Modifier.fillMaxWidth(), configTriggerViewModel) } } } + } override fun subscribeUi(binding: FragmentTriggerBinding) { binding.viewModel = configTriggerViewModel @@ -103,46 +136,37 @@ class TriggerFragment : RecyclerViewFragment() { - - override fun isDragEnabledForModel(model: TriggerKeyBindingModel_?): Boolean = - model?.model()?.isDragDropEnabled ?: false - - override fun onModelMoved( - fromPosition: Int, - toPosition: Int, - modelBeingMoved: TriggerKeyBindingModel_?, - itemView: View?, - ) { - configTriggerViewModel.onMoveTriggerKey(fromPosition, toPosition) - } + override fun getEmptyListPlaceHolderTextView(binding: FragmentTriggerBinding) = binding.emptyListPlaceHolder + + private fun FragmentTriggerBinding.enableTriggerKeyDragging(controller: EpoxyController): ItemTouchHelper = EpoxyTouchHelper.initDragging(controller) + .withRecyclerView(recyclerViewTriggerKeys) + .forVerticalList() + .withTarget(TriggerKeyBindingModel_::class.java) + .andCallbacks(object : EpoxyTouchHelper.DragCallbacks() { + + override fun isDragEnabledForModel(model: TriggerKeyBindingModel_?): Boolean = model?.model()?.isDragDropEnabled ?: false + + override fun onModelMoved( + fromPosition: Int, + toPosition: Int, + modelBeingMoved: TriggerKeyBindingModel_?, + itemView: View?, + ) { + configTriggerViewModel.onMoveTriggerKey(fromPosition, toPosition) + } - override fun onDragStarted( - model: TriggerKeyBindingModel_?, - itemView: View?, - adapterPosition: Int, - ) { - itemView?.findViewById(R.id.cardView)?.isDragged = true - } + override fun onDragStarted( + model: TriggerKeyBindingModel_?, + itemView: View?, + adapterPosition: Int, + ) { + itemView?.findViewById(R.id.cardView)?.isDragged = true + } - override fun onDragReleased(model: TriggerKeyBindingModel_?, itemView: View?) { - itemView?.findViewById(R.id.cardView)?.isDragged = false - } - }) + override fun onDragReleased(model: TriggerKeyBindingModel_?, itemView: View?) { + itemView?.findViewById(R.id.cardView)?.isDragged = false + } + }) private inner class TriggerKeyController : EpoxyController() { var modelList: List = listOf() diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt index 0c303265c7..815f65e9bd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger +import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.serialization.Serializable @Serializable @@ -20,4 +21,12 @@ sealed class TriggerKeyDevice { return true } } + + fun isSameDevice(device: InputDeviceInfo): Boolean { + if (this is External && device.isExternal) { + return device.descriptor == this.descriptor + } else { + return true + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt index fe3553a583..561fee2f77 100644 --- a/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt @@ -25,19 +25,19 @@ import kotlinx.coroutines.flow.map * Created by sds100 on 14/02/21. */ class OnboardingUseCaseImpl( - private val preferenceRepository: PreferenceRepository, + private val preferences: PreferenceRepository, private val fileAdapter: FileAdapter, private val leanbackAdapter: LeanbackAdapter, private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val packageManagerAdapter: PackageManagerAdapter, -) : PreferenceRepository by preferenceRepository, +) : PreferenceRepository by preferences, OnboardingUseCase { override var shownAppIntro by PrefDelegate(Keys.shownAppIntro, false) override suspend fun showInstallGuiKeyboardPrompt(action: ActionData): Boolean { - val acknowledged = preferenceRepository.get(Keys.acknowledgedGuiKeyboard).first() + val acknowledged = preferences.get(Keys.acknowledgedGuiKeyboard).first() val isGuiKeyboardInstalled = packageManagerAdapter.isAppInstalled(KeyMapperImeHelper.KEY_MAPPER_GUI_IME_PACKAGE) @@ -49,13 +49,12 @@ class OnboardingUseCaseImpl( action.canUseImeToPerform() } - override suspend fun showInstallShizukuPrompt(action: ActionData): Boolean = - !shizukuAdapter.isInstalled.value && - ShizukuUtils.isRecommendedForSdkVersion() && - action.canUseShizukuToPerform() + override suspend fun showInstallShizukuPrompt(action: ActionData): Boolean = !shizukuAdapter.isInstalled.value && + ShizukuUtils.isRecommendedForSdkVersion() && + action.canUseShizukuToPerform() override fun neverShowGuiKeyboardPromptsAgain() { - preferenceRepository.set(Keys.acknowledgedGuiKeyboard, true) + preferences.set(Keys.acknowledgedGuiKeyboard, true) } override var shownParallelTriggerOrderExplanation by PrefDelegate( @@ -74,10 +73,9 @@ class OnboardingUseCaseImpl( set(Keys.lastInstalledVersionCodeHomeScreen, Constants.VERSION_CODE) } - override fun getWhatsNewText(): String = - with(fileAdapter.openAsset("whats-new.txt").bufferedReader()) { - readText() - } + override fun getWhatsNewText(): String = with(fileAdapter.openAsset("whats-new.txt").bufferedReader()) { + readText() + } override var approvedAssistantTriggerFeaturePrompt by PrefDelegate( Keys.approvedAssistantTriggerFeaturePrompt, @@ -112,13 +110,13 @@ class OnboardingUseCaseImpl( } override fun shownQuickStartGuideHint() { - preferenceRepository.set(Keys.shownQuickStartGuideHint, true) + preferences.set(Keys.shownQuickStartGuideHint, true) } override fun isTvDevice(): Boolean = leanbackAdapter.isTvDevice() override val promptForShizukuPermission: Flow = combine( - preferenceRepository.get(Keys.shownShizukuPermissionPrompt), + preferences.get(Keys.shownShizukuPermissionPrompt), shizukuAdapter.isInstalled, permissionAdapter.isGrantedFlow(Permission.SHIZUKU), ) { @@ -131,6 +129,19 @@ class OnboardingUseCaseImpl( override val showShizukuAppIntroSlide: Boolean get() = shizukuAdapter.isInstalled.value + + override val showNoKeysDetectedBottomSheet: Flow = + preferences.get(Keys.neverShowNoKeysRecordedError).map { neverShow -> + if (neverShow == null) { + true + } else { + !neverShow + } + } + + override fun neverShowNoKeysRecordedBottomSheet() { + preferences.set(Keys.neverShowNoKeysRecordedError, true) + } } interface OnboardingUseCase { @@ -168,4 +179,7 @@ interface OnboardingUseCase { val promptForShizukuPermission: Flow val showShizukuAppIntroSlide: Boolean + + val showNoKeysDetectedBottomSheet: Flow + fun neverShowNoKeysRecordedBottomSheet() } diff --git a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt index 140cbd0774..e1a27137a7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.reroutekeyevents import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.util.InputEventType import kotlinx.coroutines.CoroutineScope @@ -31,28 +32,28 @@ class RerouteKeyEventsController( */ private var repeatJob: Job? = null - fun onKeyEvent( - keyCode: Int, - action: Int, - metaState: Int, - scanCode: Int = 0, - device: InputDeviceInfo?, - ): Boolean = when (action) { - KeyEvent.ACTION_DOWN -> onKeyDown( - keyCode, - device, - metaState, - scanCode, - ) - - KeyEvent.ACTION_UP -> onKeyUp( - keyCode, - device, - metaState, - scanCode, - ) + fun onKeyEvent(event: MyKeyEvent): Boolean { + if (!useCase.shouldRerouteKeyEvent(event.device?.descriptor)) { + return false + } - else -> false + return when (event.action) { + KeyEvent.ACTION_DOWN -> onKeyDown( + event.keyCode, + event.device, + event.metaState, + event.scanCode, + ) + + KeyEvent.ACTION_UP -> onKeyUp( + event.keyCode, + event.device, + event.metaState, + event.scanCode, + ) + + else -> false + } } /** @@ -64,10 +65,6 @@ class RerouteKeyEventsController( metaState: Int, scanCode: Int = 0, ): Boolean { - if (device != null && !useCase.shouldRerouteKeyEvent(device.descriptor)) { - return false - } - val inputKeyModel = InputKeyModel( keyCode = keyCode, inputType = InputEventType.DOWN, @@ -102,10 +99,6 @@ class RerouteKeyEventsController( metaState: Int, scanCode: Int = 0, ): Boolean { - if (device != null && !useCase.shouldRerouteKeyEvent(device.descriptor)) { - return false - } - repeatJob?.cancel() val inputKeyModel = InputKeyModel( diff --git a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt index ba0d9352b4..39f8257540 100644 --- a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt @@ -1,11 +1,12 @@ package io.github.sds100.keymapper.reroutekeyevents +import android.os.Build import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeMessenger import io.github.sds100.keymapper.util.firstBlocking import kotlinx.coroutines.flow.map @@ -20,7 +21,7 @@ import kotlinx.coroutines.flow.map */ class RerouteKeyEventsUseCaseImpl( private val inputMethodAdapter: InputMethodAdapter, - private val keyMapperImeMessenger: KeyMapperImeMessenger, + private val imeInputEventInjector: ImeInputEventInjector, private val preferenceRepository: PreferenceRepository, ) : RerouteKeyEventsUseCase { @@ -32,17 +33,26 @@ class RerouteKeyEventsUseCaseImpl( private val imeHelper by lazy { KeyMapperImeHelper(inputMethodAdapter) } - override fun shouldRerouteKeyEvent(descriptor: String): Boolean = - imeHelper.isCompatibleImeChosen() && - devicesToRerouteKeyEvents.firstBlocking().contains(descriptor) && - rerouteKeyEvents.firstBlocking() + override fun shouldRerouteKeyEvent(descriptor: String?): Boolean { + if (Build.VERSION.SDK_INT != Build.VERSION_CODES.R) { + return false + } + + return rerouteKeyEvents.firstBlocking() && + imeHelper.isCompatibleImeChosen() && + ( + descriptor != null && + devicesToRerouteKeyEvents.firstBlocking() + .contains(descriptor) + ) + } override fun inputKeyEvent(keyModel: InputKeyModel) { - keyMapperImeMessenger.inputKeyEvent(keyModel) + imeInputEventInjector.inputKeyEvent(keyModel) } } interface RerouteKeyEventsUseCase { - fun shouldRerouteKeyEvent(descriptor: String): Boolean + fun shouldRerouteKeyEvent(descriptor: String?): Boolean fun inputKeyEvent(keyModel: InputKeyModel) } diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt index f600934435..d817d1e4a8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.map * Created by sds100 on 14/02/2021. */ class ConfigSettingsUseCaseImpl( - private val preferenceRepository: PreferenceRepository, + private val preferences: PreferenceRepository, private val permissionAdapter: PermissionAdapter, private val inputMethodAdapter: InputMethodAdapter, private val soundsManager: SoundsManager, @@ -68,7 +68,7 @@ class ConfigSettingsUseCaseImpl( } override val rerouteKeyEvents: Flow = - preferenceRepository.get(Keys.rerouteKeyEvents).map { it ?: false } + preferences.get(Keys.rerouteKeyEvents).map { it ?: false } override val isCompatibleImeChosen: Flow = inputMethodAdapter.chosenIme.map { imeHelper.isCompatibleImeChosen() @@ -85,60 +85,56 @@ class ConfigSettingsUseCaseImpl( imeHelper.enableCompatibleInputMethods() } - override suspend fun chooseCompatibleIme(): Result = - imeHelper.chooseCompatibleInputMethod() + override suspend fun chooseCompatibleIme(): Result = imeHelper.chooseCompatibleInputMethod() - override suspend fun showImePicker(): Result<*> = - inputMethodAdapter.showImePicker(fromForeground = true) + override suspend fun showImePicker(): Result<*> = inputMethodAdapter.showImePicker(fromForeground = true) - override fun getPreference(key: Preferences.Key) = - preferenceRepository.get(key) + override fun getPreference(key: Preferences.Key) = preferences.get(key) - override fun setPreference(key: Preferences.Key, value: T?) = - preferenceRepository.set(key, value) + override fun setPreference(key: Preferences.Key, value: T?) = preferences.set(key, value) override val automaticBackupLocation = - preferenceRepository.get(Keys.automaticBackupLocation).map { it ?: "" } + preferences.get(Keys.automaticBackupLocation).map { it ?: "" } override fun setAutomaticBackupLocation(uri: String) { - preferenceRepository.set(Keys.automaticBackupLocation, uri) + preferences.set(Keys.automaticBackupLocation, uri) } override fun disableAutomaticBackup() { - preferenceRepository.set(Keys.automaticBackupLocation, null) + preferences.set(Keys.automaticBackupLocation, null) } override val defaultLongPressDelay: Flow = - preferenceRepository.get(Keys.defaultLongPressDelay) + preferences.get(Keys.defaultLongPressDelay) .map { it ?: PreferenceDefaults.LONG_PRESS_DELAY } override val defaultDoublePressDelay: Flow = - preferenceRepository.get(Keys.defaultDoublePressDelay) + preferences.get(Keys.defaultDoublePressDelay) .map { it ?: PreferenceDefaults.DOUBLE_PRESS_DELAY } override val defaultRepeatDelay: Flow = - preferenceRepository.get(Keys.defaultRepeatDelay) + preferences.get(Keys.defaultRepeatDelay) .map { it ?: PreferenceDefaults.REPEAT_DELAY } override val defaultSequenceTriggerTimeout: Flow = - preferenceRepository.get(Keys.defaultSequenceTriggerTimeout) + preferences.get(Keys.defaultSequenceTriggerTimeout) .map { it ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT } override val defaultVibrateDuration: Flow = - preferenceRepository.get(Keys.defaultVibrateDuration) + preferences.get(Keys.defaultVibrateDuration) .map { it ?: PreferenceDefaults.VIBRATION_DURATION } override val defaultRepeatRate: Flow = - preferenceRepository.get(Keys.defaultRepeatRate) + preferences.get(Keys.defaultRepeatRate) .map { it ?: PreferenceDefaults.REPEAT_RATE } override fun resetDefaultMappingOptions() { - preferenceRepository.set(Keys.defaultLongPressDelay, null) - preferenceRepository.set(Keys.defaultDoublePressDelay, null) - preferenceRepository.set(Keys.defaultRepeatDelay, null) - preferenceRepository.set(Keys.defaultSequenceTriggerTimeout, null) - preferenceRepository.set(Keys.defaultVibrateDuration, null) - preferenceRepository.set(Keys.defaultRepeatRate, null) + preferences.set(Keys.defaultLongPressDelay, null) + preferences.set(Keys.defaultDoublePressDelay, null) + preferences.set(Keys.defaultRepeatDelay, null) + preferences.set(Keys.defaultSequenceTriggerTimeout, null) + preferences.set(Keys.defaultVibrateDuration, null) + preferences.set(Keys.defaultRepeatRate, null) } override fun requestWriteSecureSettingsPermission() { @@ -161,8 +157,7 @@ class ConfigSettingsUseCaseImpl( permissionAdapter.request(Permission.POST_NOTIFICATIONS) } - override fun isNotificationsPermissionGranted(): Boolean = - permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) + override fun isNotificationsPermissionGranted(): Boolean = permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) override fun getSoundFiles(): List = soundsManager.soundFiles.value @@ -171,6 +166,10 @@ class ConfigSettingsUseCaseImpl( soundsManager.deleteSound(it) } } + + override fun resetAllSettings() { + preferences.deleteAll() + } } interface ConfigSettingsUseCase { @@ -211,4 +210,6 @@ interface ConfigSettingsUseCase { fun requestShizukuPermission() val connectedInputDevices: StateFlow>> + + fun resetAllSettings() } diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt index 97288a436d..a831881f79 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt @@ -341,6 +341,18 @@ class MainSettingsFragment : BaseSettingsFragment() { } } + Preference(requireContext()).apply { + setTitle(R.string.title_pref_reset_settings) + setSummary(R.string.summary_pref_reset_settings) + + setOnPreferenceClickListener { + viewModel.onResetAllSettingsClick() + true + } + + addPreference(this) + } + // write secure settings PreferenceCategory(requireContext()).apply { key = CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt index 4ca01df2cb..8bb9c59352 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.util.getFullMessage import io.github.sds100.keymapper.util.onFailure import io.github.sds100.keymapper.util.onSuccess import io.github.sds100.keymapper.util.otherwise +import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.MultiChoiceItem import io.github.sds100.keymapper.util.ui.PopupUi import io.github.sds100.keymapper.util.ui.PopupViewModel @@ -191,6 +192,23 @@ class SettingsViewModel( } } + fun onResetAllSettingsClick() { + val dialog = PopupUi.Dialog( + title = getString(R.string.dialog_title_reset_settings), + message = getString(R.string.dialog_message_reset_settings), + positiveButtonText = getString(R.string.pos_button_reset_settings), + negativeButtonText = getString(R.string.neg_cancel), + ) + + viewModelScope.launch { + val response = showPopup("reset_settings_dialog", dialog) + + if (response == DialogResponse.POSITIVE) { + useCase.resetAllSettings() + } + } + } + @Suppress("UNCHECKED_CAST") class Factory( private val configSettingsUseCase: ConfigSettingsUseCase, diff --git a/app/src/main/java/io/github/sds100/keymapper/shizuku/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt similarity index 93% rename from app/src/main/java/io/github/sds100/keymapper/shizuku/InputEventInjector.kt rename to app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt index dbc2de5bf6..6fabff179c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/shizuku/InputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt @@ -5,16 +5,13 @@ import android.content.Context import android.hardware.input.IInputManager import android.os.SystemClock import android.view.KeyEvent +import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.util.InputEventType import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper import timber.log.Timber -/** - * Created by sds100 on 21/04/2021. - */ - @SuppressLint("PrivateApi") class ShizukuInputEventInjector : InputEventInjector { @@ -58,7 +55,3 @@ class ShizukuInputEventInjector : InputEventInjector { } } } - -interface InputEventInjector { - fun inputKeyEvent(model: InputKeyModel) -} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt index b80d8c1343..eb3b809ba6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt @@ -182,7 +182,9 @@ class AccessibilityServiceAdapter( settingsIntent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS, + or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + // Add this flag so user only has to press back once. + or Intent.FLAG_ACTIVITY_NO_HISTORY, ) ctx.startActivity(settingsIntent) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index fb4267bae4..8ad509a8e0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.system.accessibility import android.accessibilityservice.AccessibilityServiceInfo import android.os.Build -import android.os.SystemClock import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -17,12 +16,15 @@ import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintGestureMap import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapId import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.mappings.keymaps.detection.DetectScreenOffKeyEventsController +import io.github.sds100.keymapper.mappings.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController import io.github.sds100.keymapper.mappings.keymaps.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsController import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsUseCase import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.util.ServiceEvent @@ -107,6 +109,8 @@ abstract class BaseAccessibilityServiceController( private var recordingTriggerJob: Job? = null private val recordingTrigger: Boolean get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true + private val recordDpadMotionEventTracker: DpadMotionEventTracker = + DpadMotionEventTracker() val isPaused: StateFlow = pauseMappingsUseCase.isPaused .stateIn(coroutineScope, SharingStarted.Eagerly, false) @@ -119,16 +123,11 @@ abstract class BaseAccessibilityServiceController( DetectScreenOffKeyEventsController( suAdapter, devicesAdapter, - ) { keyCode, action, device -> + ) { event -> if (!isPaused.value) { withContext(Dispatchers.Main.immediate) { - keyMapController.onKeyEvent( - keyCode, - action, - metaState = 0, - device = device, - ) + keyMapController.onKeyEvent(event) } } } @@ -294,24 +293,20 @@ abstract class BaseAccessibilityServiceController( } fun onKeyEvent( - keyCode: Int, - action: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, - eventTime: Long, + event: MyKeyEvent, + detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { - val detailedLogInfo = - "key code: $keyCode, time since event: ${SystemClock.uptimeMillis() - eventTime}ms, device name: ${device?.name}, descriptor: ${device?.descriptor}, device id: ${device?.id}, is external: ${device?.isExternal}, meta state: $metaState, scan code: $scanCode" + val detailedLogInfo = event.toString() if (recordingTrigger) { - if (action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded key ${KeyEvent.keyCodeToString(keyCode)}, $detailedLogInfo") + if (event.action == KeyEvent.ACTION_DOWN) { + Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") coroutineScope.launch { outputEvents.emit( ServiceEvent.RecordedTriggerKey( - keyCode, - device, + event.keyCode, + event.device, + detectionSource, ), ) } @@ -321,35 +316,23 @@ abstract class BaseAccessibilityServiceController( } if (isPaused.value) { - when (action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo") + when (event.action) { + KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") + KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") } } else { try { var consume: Boolean - consume = keyMapController.onKeyEvent( - keyCode, - action, - metaState, - scanCode, - device, - ) + consume = keyMapController.onKeyEvent(event) if (!consume) { - consume = rerouteKeyEventsController.onKeyEvent( - keyCode, - action, - metaState, - scanCode, - device, - ) + consume = rerouteKeyEventsController.onKeyEvent(event) } - when (action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(keyCode)} - consumed: $consume, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(keyCode)} - consumed: $consume, $detailedLogInfo") + when (event.action) { + KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - consumed: $consume, $detailedLogInfo") + KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - consumed: $consume, $detailedLogInfo") } return consume @@ -361,46 +344,71 @@ abstract class BaseAccessibilityServiceController( return false } - fun onKeyEventFromIme( - keyCode: Int, - action: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, - eventTime: Long, - ): Boolean { - if (!detectKeyMapsUseCase.acceptKeyEventsFromIme) { - Timber.d("Don't input key event from ime") - return false - } - + fun onKeyEventFromIme(event: MyKeyEvent): Boolean { /* Issue #850 If a volume key is sent while the phone is ringing or in a call then that key event must have been relayed by an input method and only an up event - is sent. This is a restriction in Android. So send a fake down key event as well. + is sent. This is a restriction in Android. So send a fake DOWN key event as well + before returning the UP key event. */ - if (action == KeyEvent.ACTION_UP && (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { + if (event.action == KeyEvent.ACTION_UP && (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { onKeyEvent( - keyCode, - KeyEvent.ACTION_DOWN, - device, - metaState, - 0, - eventTime, + event.copy(action = KeyEvent.ACTION_DOWN), + detectionSource = KeyEventDetectionSource.INPUT_METHOD, ) } return onKeyEvent( - keyCode, - action, - device, - metaState, - scanCode, - eventTime, + event, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, ) } + fun onMotionEventFromIme(event: MyMotionEvent): Boolean { + if (isPaused.value) { + return false + } + + if (recordingTrigger) { + val dpadKeyEvents = recordDpadMotionEventTracker.convertMotionEvent(event) + + var consume = false + + for (keyEvent in dpadKeyEvents) { + if (keyEvent.action == KeyEvent.ACTION_DOWN) { + Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") + + coroutineScope.launch { + outputEvents.emit( + ServiceEvent.RecordedTriggerKey( + keyEvent.keyCode, + keyEvent.device, + KeyEventDetectionSource.INPUT_METHOD, + ), + ) + } + } + + // Consume the key event if it is an DOWN or UP. + consume = true + } + + if (consume) { + return true + } + } + + try { + val consume = keyMapController.onMotionEvent(event) + + return consume + } catch (e: Exception) { + Timber.e(e) + return false + } + } + fun onAccessibilityEvent(event: AccessibilityEventModel?) { Timber.d("OnAccessibilityEvent $event") val focussedNode = accessibilityService.findFocussedNode(AccessibilityNodeInfo.FOCUS_INPUT) @@ -431,6 +439,7 @@ abstract class BaseAccessibilityServiceController( when (event) { is ServiceEvent.StartRecordingTrigger -> if (!recordingTrigger) { + recordDpadMotionEventTracker.reset() recordingTriggerJob = recordTriggerJob() } @@ -439,6 +448,7 @@ abstract class BaseAccessibilityServiceController( recordingTriggerJob?.cancel() recordingTriggerJob = null + recordDpadMotionEventTracker.reset() if (wasRecordingTrigger) { coroutineScope.launch { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index 1516ee2a81..3ae7b881f3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -10,6 +10,7 @@ import android.graphics.Path import android.graphics.Point import android.os.Build import android.view.KeyEvent +import android.view.MotionEvent import android.view.accessibility.AccessibilityEvent import androidx.core.content.getSystemService import androidx.core.os.bundleOf @@ -18,9 +19,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback +import io.github.sds100.keymapper.api.KeyEventRelayService import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapId +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.system.devices.InputDeviceUtils +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Inject import io.github.sds100.keymapper.util.InputEventType @@ -101,11 +106,10 @@ class MyAccessibilityService : } } - private val keyEventReceiverCallback: IKeyEventRelayServiceCallback = + private val relayServiceCallback: IKeyEventRelayServiceCallback = object : IKeyEventRelayServiceCallback.Stub() { - override fun onKeyEvent(event: KeyEvent?, sourcePackageName: String?): Boolean { + override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return false - sourcePackageName ?: return false val device = if (event.device == null) { null @@ -115,21 +119,37 @@ class MyAccessibilityService : if (controller != null) { return controller!!.onKeyEventFromIme( - event.keyCode, - event.action, - device, - event.metaState, - event.scanCode, - event.eventTime, + MyKeyEvent( + keyCode = event.keyCode, + action = event.action, + metaState = event.metaState, + scanCode = event.scanCode, + device = device, + repeatCount = event.repeatCount, + ), ) } return false } + + override fun onMotionEvent(event: MotionEvent?): Boolean { + event ?: return false + + if (controller != null) { + return controller!!.onMotionEventFromIme(MyMotionEvent.fromMotionEvent(event)) + } + + return false + } } private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl(this, keyEventReceiverCallback) + KeyEventRelayServiceWrapperImpl( + ctx = this, + id = KeyEventRelayService.CALLBACK_ID_ACCESSIBILITY_SERVICE, + callback = relayServiceCallback, + ) } private var controller: AccessibilityServiceController? = null @@ -249,12 +269,15 @@ class MyAccessibilityService : if (controller != null) { return controller!!.onKeyEvent( - event.keyCode, - event.action, - device, - event.metaState, - event.scanCode, - event.eventTime, + MyKeyEvent( + keyCode = event.keyCode, + action = event.action, + metaState = event.metaState, + scanCode = event.scanCode, + device = device, + repeatCount = event.repeatCount, + ), + KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt index 98059ce108..5e5507e206 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt @@ -18,6 +18,7 @@ import android.os.TransactionTooLargeException import android.provider.MediaStore import android.provider.Settings import androidx.core.content.ContextCompat +import androidx.core.content.pm.PackageInfoCompat import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State @@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import splitties.bitflags.withFlag +import java.io.IOException /** * Created by sds100 on 16/03/2021. @@ -283,6 +285,8 @@ class AndroidPackageManagerAdapter( .success() } catch (e: PackageManager.NameNotFoundException) { return Error.AppNotFound(packageName) + } catch (e: IOException) { + return Error.AppNotFound(packageName) } } @@ -294,6 +298,8 @@ class AndroidPackageManagerAdapter( .success() } catch (e: PackageManager.NameNotFoundException) { return Error.AppNotFound(packageName) + } catch (e: IOException) { + return Error.AppNotFound(packageName) } } @@ -344,18 +350,18 @@ class AndroidPackageManagerAdapter( val isLaunchable = launchIntent != null || leanbackLaunchIntent != null - val activityPackageInfo = packageManager.getPackageInfo( + val packageInfo = packageManager.getPackageInfo( packageName, - PackageManager.GET_ACTIVITIES, + PackageManager.GET_ACTIVITIES or PackageManager.GET_META_DATA, ) - if (activityPackageInfo == null) { + if (packageInfo == null) { return null } val activityModels = mutableListOf() - activityPackageInfo.activities.forEach { activity -> + packageInfo.activities.forEach { activity -> val model = ActivityInfo( activityName = activity.name, packageName = activity.packageName, @@ -369,6 +375,7 @@ class AndroidPackageManagerAdapter( activities = activityModels, isEnabled = applicationInfo.enabled, isLaunchable = isLaunchable, + versionCode = PackageInfoCompat.getLongVersionCode(packageInfo), ) } catch (e: PackageManager.NameNotFoundException) { return null diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt index 36639ef065..42686f9f7a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageInfo.kt @@ -12,4 +12,5 @@ data class PackageInfo( * Whether this package can be launched. */ val isLaunchable: Boolean, + val versionCode: Long, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageUtils.kt deleted file mode 100644 index 15abca7a36..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.sds100.keymapper.system.apps - -import android.content.Context - -/** - * Created by sds100 on 27/10/2018. - */ - -object PackageUtils { - - fun isAppInstalled(ctx: Context, packageName: String): Boolean { - try { - ctx.packageManager.getApplicationInfo(packageName, 0) - - return true - } catch (e: Exception) { - return false - } - } -} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt new file mode 100644 index 0000000000..97bbe5764a --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -0,0 +1,11 @@ +package io.github.sds100.keymapper.system.inputevents + +import io.github.sds100.keymapper.system.inputmethod.InputKeyModel + +/** + * Created by sds100 on 21/04/2021. + */ + +interface InputEventInjector { + fun inputKeyEvent(model: InputKeyModel) +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/keyevents/KeyEventUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/system/keyevents/KeyEventUtils.kt rename to app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index eb98c115f7..24a0429684 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/keyevents/KeyEventUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -1,6 +1,8 @@ -package io.github.sds100.keymapper.system.keyevents +package io.github.sds100.keymapper.system.inputevents import android.os.Build +import android.view.InputDevice +import android.view.InputEvent import android.view.KeyEvent import io.github.sds100.keymapper.R import splitties.bitflags.withFlag @@ -8,7 +10,7 @@ import splitties.bitflags.withFlag /** * Created by sds100 on 17/07/2018. */ -object KeyEventUtils { +object InputEventUtils { /** * Maps keys which aren't single characters like the Control keys to a string representation */ @@ -667,8 +669,7 @@ object KeyEventUtils { "KEY_SEARCH" to KeyEvent.KEYCODE_SEARCH, ) - fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = - GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } + fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } val MODIFIER_KEYCODES: Set get() = setOf( @@ -806,4 +807,18 @@ object KeyEventUtils { KeyEvent.META_SCROLL_LOCK_ON to R.string.meta_state_scroll_lock, KeyEvent.META_FUNCTION_ON to R.string.meta_state_function, ) + + fun isDpadKeyCode(code: Int): Boolean { + return code == KeyEvent.KEYCODE_DPAD_LEFT || + code == KeyEvent.KEYCODE_DPAD_RIGHT || + code == KeyEvent.KEYCODE_DPAD_UP || + code == KeyEvent.KEYCODE_DPAD_DOWN || + code == KeyEvent.KEYCODE_DPAD_UP_LEFT || + code == KeyEvent.KEYCODE_DPAD_UP_RIGHT || + code == KeyEvent.KEYCODE_DPAD_DOWN_LEFT || + code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + } + + fun isDpadDevice(event: InputEvent): Boolean = // Check that input comes from a device with directional pads. + event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt new file mode 100644 index 0000000000..64384acaac --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.system.inputevents + +import io.github.sds100.keymapper.system.devices.InputDeviceInfo + +data class MyKeyEvent( + val keyCode: Int, + val action: Int, + val metaState: Int, + val scanCode: Int, + val device: InputDeviceInfo?, + val repeatCount: Int, +) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt new file mode 100644 index 0000000000..2b80add05f --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt @@ -0,0 +1,29 @@ +package io.github.sds100.keymapper.system.inputevents + +import android.view.MotionEvent +import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.devices.InputDeviceUtils + +/** + * This is our own abstraction over MotionEvent so that it is easier to write tests and read + * values without relying on the Android SDK. + */ +data class MyMotionEvent( + val metaState: Int, + val device: InputDeviceInfo, + val axisHatX: Float, + val axisHatY: Float, + val isDpad: Boolean, +) { + companion object { + fun fromMotionEvent(event: MotionEvent): MyMotionEvent { + return MyMotionEvent( + metaState = event.metaState, + device = InputDeviceUtils.createInputDeviceInfo(event.device), + axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), + axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), + isDpad = InputEventUtils.isDpadDevice(event), + ) + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt index 05dd9252aa..4701c96ee4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt @@ -65,7 +65,7 @@ class AndroidInputMethodAdapter( MutableStateFlow(initialValues) } - val broadcastReceiver = object : BroadcastReceiver() { + private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { intent ?: return context ?: return @@ -189,8 +189,7 @@ class AndroidInputMethodAdapter( } } - private fun enableImeWithoutUserInput(imeId: String): Result<*> = - suAdapter.execute("ime enable $imeId") + private fun enableImeWithoutUserInput(imeId: String): Result<*> = suAdapter.execute("ime enable $imeId") override suspend fun chooseImeWithoutUserInput(imeId: String): Result { getInfoById(imeId).onSuccess { @@ -237,13 +236,12 @@ class AndroidInputMethodAdapter( override fun getInfoById(imeId: String): Result { val info = - inputMethods.value.find { it.id == imeId } ?: return Error.InputMethodNotFound(imeId) + getInputMethods().find { it.id == imeId } ?: return Error.InputMethodNotFound(imeId) return Success(info) } - override fun getInfoByPackageName(packageName: String): Result = - getImeId(packageName).then { getInfoById(it) } + override fun getInfoByPackageName(packageName: String): Result = getImeId(packageName).then { getInfoById(it) } /** * Example: @@ -291,8 +289,7 @@ class AndroidInputMethodAdapter( return getInfoById(chosenImeId).valueOrNull() } - private fun getChosenImeId(): String = - Settings.Secure.getString(ctx.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD) + private fun getChosenImeId(): String = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD) private fun getImeId(packageName: String): Result { val imeId = inputMethodManager.inputMethodList.find { it.packageName == packageName }?.id diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt similarity index 81% rename from app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt rename to app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt index 61ec51ce61..298dfc076d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt @@ -6,8 +6,9 @@ import android.os.Build import android.os.SystemClock import android.view.KeyCharacterMap import android.view.KeyEvent +import io.github.sds100.keymapper.api.KeyEventRelayService import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper -import io.github.sds100.keymapper.shizuku.InputEventInjector +import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.util.InputEventType import timber.log.Timber @@ -19,11 +20,11 @@ import timber.log.Timber * This class handles communicating with the Key Mapper input method services * so key events and text can be inputted. */ -class KeyMapperImeMessengerImpl( +class ImeInputEventInjectorImpl( context: Context, private val keyEventRelayService: KeyEventRelayServiceWrapper, private val inputMethodAdapter: InputMethodAdapter, -) : KeyMapperImeMessenger { +) : ImeInputEventInjector { companion object { // DON'T CHANGE THESE!!! @@ -110,20 +111,36 @@ class KeyMapperImeMessengerImpl( when (model.inputType) { InputEventType.DOWN_UP -> { val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) - keyEventRelayService.sendKeyEvent(downKeyEvent, imePackageName) + keyEventRelayService.sendKeyEvent( + downKeyEvent, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent(upKeyEvent, imePackageName) + keyEventRelayService.sendKeyEvent( + upKeyEvent, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) } InputEventType.DOWN -> { val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) - keyEventRelayService.sendKeyEvent(downKeyEvent, imePackageName) + keyEventRelayService.sendKeyEvent( + downKeyEvent, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) } InputEventType.UP -> { val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent(upKeyEvent, imePackageName) + keyEventRelayService.sendKeyEvent( + upKeyEvent, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) } } } @@ -174,7 +191,11 @@ class KeyMapperImeMessengerImpl( // with the current key character map. if (events != null) { for (e in events) { - keyEventRelayService.sendKeyEvent(e, imePackageName) + keyEventRelayService.sendKeyEvent( + e, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) } return @@ -190,10 +211,14 @@ class KeyMapperImeMessengerImpl( 0, ) - keyEventRelayService.sendKeyEvent(event, imePackageName) + keyEventRelayService.sendKeyEvent( + event, + imePackageName, + KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + ) } } -interface KeyMapperImeMessenger : InputEventInjector { +interface ImeInputEventInjector : InputEventInjector { fun inputText(text: String) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt index 5ff369e6aa..3bf0d1f0d0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt @@ -32,6 +32,8 @@ class KeyMapperImeHelper(private val imeAdapter: InputMethodAdapter) { KEY_MAPPER_LEANBACK_IME_PACKAGE, KEY_MAPPER_HACKERS_KEYBOARD_PACKAGE, ) + + const val MIN_SUPPORTED_GUI_KEYBOARD_VERSION_CODE: Int = 20 } val isCompatibleImeEnabledFlow: Flow = @@ -46,15 +48,13 @@ class KeyMapperImeHelper(private val imeAdapter: InputMethodAdapter) { } } - suspend fun chooseCompatibleInputMethod(): Result = - getLastUsedCompatibleImeId().suspendThen { - imeAdapter.chooseImeWithoutUserInput(it) - } + suspend fun chooseCompatibleInputMethod(): Result = getLastUsedCompatibleImeId().suspendThen { + imeAdapter.chooseImeWithoutUserInput(it) + } - suspend fun chooseLastUsedIncompatibleInputMethod(): Result = - getLastUsedIncompatibleImeId().then { - imeAdapter.chooseImeWithoutUserInput(it) - } + suspend fun chooseLastUsedIncompatibleInputMethod(): Result = getLastUsedIncompatibleImeId().then { + imeAdapter.chooseImeWithoutUserInput(it) + } suspend fun toggleCompatibleInputMethod(): Result = if (isCompatibleImeChosen()) { chooseLastUsedIncompatibleInputMethod() @@ -62,8 +62,7 @@ class KeyMapperImeHelper(private val imeAdapter: InputMethodAdapter) { chooseCompatibleInputMethod() } - fun isCompatibleImeChosen(): Boolean = - imeAdapter.chosenIme.value?.packageName in KEY_MAPPER_IME_PACKAGE_LIST + fun isCompatibleImeChosen(): Boolean = imeAdapter.chosenIme.value?.packageName in KEY_MAPPER_IME_PACKAGE_LIST fun isCompatibleImeEnabled(): Boolean = imeAdapter.inputMethods .map { containsCompatibleIme(it) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index 5e62e0794c..2773a4eddf 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -6,9 +6,11 @@ import android.content.Intent import android.content.IntentFilter import android.inputmethodservice.InputMethodService import android.view.KeyEvent +import android.view.MotionEvent import androidx.core.content.ContextCompat import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback +import io.github.sds100.keymapper.api.KeyEventRelayService import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapperImpl /** @@ -80,18 +82,23 @@ class KeyMapperImeService : InputMethodService() { private val keyEventReceiverCallback: IKeyEventRelayServiceCallback = object : IKeyEventRelayServiceCallback.Stub() { - override fun onKeyEvent(event: KeyEvent?, sourcePackageName: String?): Boolean { + override fun onKeyEvent(event: KeyEvent?): Boolean { // Only accept key events from Key Mapper - if (sourcePackageName != Constants.PACKAGE_NAME) { - return false - } - return currentInputConnection?.sendKeyEvent(event) ?: false } + + override fun onMotionEvent(event: MotionEvent?): Boolean { + // Do nothing if the IME receives a motion event. + return false + } } private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl(this, keyEventReceiverCallback) + KeyEventRelayServiceWrapperImpl( + ctx = this, + id = KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, + callback = keyEventReceiverCallback, + ) } override fun onCreate() { @@ -113,11 +120,53 @@ class KeyMapperImeService : InputMethodService() { keyEventRelayServiceWrapper.onCreate() } - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = - keyEventRelayServiceWrapper.sendKeyEvent(event, Constants.PACKAGE_NAME) + override fun onGenericMotionEvent(event: MotionEvent?): Boolean { + event ?: return super.onGenericMotionEvent(null) + + val consume = keyEventRelayServiceWrapper.sendMotionEvent( + event = event, + targetPackageName = Constants.PACKAGE_NAME, + callbackId = KeyEventRelayService.CALLBACK_ID_ACCESSIBILITY_SERVICE, + ) + + return if (consume) { + true + } else { + super.onGenericMotionEvent(event) + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + event ?: return super.onKeyDown(keyCode, null) - override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean = - keyEventRelayServiceWrapper.sendKeyEvent(event, Constants.PACKAGE_NAME) + val consume = keyEventRelayServiceWrapper.sendKeyEvent( + event = event, + targetPackageName = Constants.PACKAGE_NAME, + callbackId = KeyEventRelayService.CALLBACK_ID_ACCESSIBILITY_SERVICE, + ) + + return if (consume) { + true + } else { + super.onKeyDown(keyCode, event) + } + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + event ?: return super.onKeyUp(keyCode, null) + + val consume = keyEventRelayServiceWrapper.sendKeyEvent( + event = event, + targetPackageName = Constants.PACKAGE_NAME, + callbackId = KeyEventRelayService.CALLBACK_ID_ACCESSIBILITY_SERVICE, + ) + + return if (consume) { + true + } else { + super.onKeyUp(keyCode, event) + } + } override fun onDestroy() { unregisterReceiver(broadcastReceiver) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt index b4e2b274db..aa24829d97 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt @@ -2,9 +2,9 @@ package io.github.sds100.keymapper.system.navigation import android.view.KeyEvent import androidx.core.view.accessibility.AccessibilityNodeInfoCompat -import io.github.sds100.keymapper.shizuku.InputEventInjector import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.system.accessibility.IAccessibilityService +import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter diff --git a/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index 386a8fd003..423a033661 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -96,7 +96,7 @@ class AndroidPermissionAdapter( * show it again. */ private val neverRequestDndPermission: StateFlow = - preferenceRepository.get(Keys.neverShowDndError) + preferenceRepository.get(Keys.neverShowDndAccessError) .map { it == true } .stateIn(coroutineScope, SharingStarted.Eagerly, false) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt index 96bed81db7..2756671e47 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt @@ -154,6 +154,7 @@ fun Error.getFullMessage(resourceProvider: ResourceProvider): String = when (thi } Error.PurchasingNotImplemented -> resourceProvider.getString(R.string.purchasing_error_not_implemented) + Error.DpadTriggerImeNotSelected -> resourceProvider.getString(R.string.trigger_error_dpad_ime_not_selected) } val Error.isFixable: Boolean diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt index 2914328dd9..8372d15652 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt @@ -30,6 +30,7 @@ import io.github.sds100.keymapper.mappings.fingerprintmaps.ListFingerprintMapsUs import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapViewModel import io.github.sds100.keymapper.mappings.keymaps.CreateKeyMapShortcutViewModel import io.github.sds100.keymapper.mappings.keymaps.ListKeyMapsUseCaseImpl +import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCaseImpl import io.github.sds100.keymapper.onboarding.AppIntroUseCaseImpl import io.github.sds100.keymapper.onboarding.AppIntroViewModel import io.github.sds100.keymapper.reportbug.ReportBugUseCaseImpl @@ -52,40 +53,35 @@ import io.github.sds100.keymapper.system.intents.ConfigIntentViewModel object Inject { - fun chooseActionViewModel(ctx: Context): ChooseActionViewModel.Factory = - ChooseActionViewModel.Factory( - UseCases.createAction(ctx), - ServiceLocator.resourceProvider(ctx), - UseCases.isActionSupported(ctx), - ) + fun chooseActionViewModel(ctx: Context): ChooseActionViewModel.Factory = ChooseActionViewModel.Factory( + UseCases.createAction(ctx), + ServiceLocator.resourceProvider(ctx), + UseCases.isActionSupported(ctx), + ) - fun chooseAppViewModel(context: Context): ChooseAppViewModel.Factory = - ChooseAppViewModel.Factory( - UseCases.displayPackages(context), - ) + fun chooseAppViewModel(context: Context): ChooseAppViewModel.Factory = ChooseAppViewModel.Factory( + UseCases.displayPackages(context), + ) - fun chooseActivityViewModel(context: Context): ChooseActivityViewModel.Factory = - ChooseActivityViewModel.Factory( - UseCases.displayPackages(context), - ) + fun chooseActivityViewModel(context: Context): ChooseActivityViewModel.Factory = ChooseActivityViewModel.Factory( + UseCases.displayPackages(context), + ) - fun chooseAppShortcutViewModel(context: Context): ChooseAppShortcutViewModel.Factory = - ChooseAppShortcutViewModel.Factory( - DisplayAppShortcutsUseCaseImpl( - ServiceLocator.appShortcutAdapter(context), - ), - ServiceLocator.resourceProvider(context), - ) + fun chooseAppShortcutViewModel(context: Context): ChooseAppShortcutViewModel.Factory = ChooseAppShortcutViewModel.Factory( + DisplayAppShortcutsUseCaseImpl( + ServiceLocator.appShortcutAdapter(context), + ), + ServiceLocator.resourceProvider(context), + ) - fun chooseConstraintListViewModel(ctx: Context): ChooseConstraintViewModel.Factory = - ChooseConstraintViewModel.Factory( - CreateConstraintUseCaseImpl( - ServiceLocator.networkAdapter(ctx), - ServiceLocator.inputMethodAdapter(ctx), - ServiceLocator.settingsRepository(ctx), - ), - ServiceLocator.resourceProvider(ctx), - ) + fun chooseConstraintListViewModel(ctx: Context): ChooseConstraintViewModel.Factory = ChooseConstraintViewModel.Factory( + CreateConstraintUseCaseImpl( + ServiceLocator.networkAdapter(ctx), + ServiceLocator.inputMethodAdapter(ctx), + ServiceLocator.settingsRepository(ctx), + ), + ServiceLocator.resourceProvider(ctx), + ) fun configKeyEventViewModel( context: Context, @@ -102,32 +98,27 @@ object Inject { fun chooseKeyCodeViewModel(): ChooseKeyCodeViewModel.Factory = ChooseKeyCodeViewModel.Factory() - fun configIntentViewModel(ctx: Context): ConfigIntentViewModel.Factory = - ConfigIntentViewModel.Factory(ServiceLocator.resourceProvider(ctx)) + fun configIntentViewModel(ctx: Context): ConfigIntentViewModel.Factory = ConfigIntentViewModel.Factory(ServiceLocator.resourceProvider(ctx)) - fun soundFileActionTypeViewModel(ctx: Context): ChooseSoundFileViewModel.Factory = - ChooseSoundFileViewModel.Factory( - ServiceLocator.resourceProvider(ctx), - ChooseSoundFileUseCaseImpl( - ServiceLocator.fileAdapter(ctx), - ServiceLocator.soundsManager(ctx), - ), - ) + fun soundFileActionTypeViewModel(ctx: Context): ChooseSoundFileViewModel.Factory = ChooseSoundFileViewModel.Factory( + ServiceLocator.resourceProvider(ctx), + ChooseSoundFileUseCaseImpl( + ServiceLocator.fileAdapter(ctx), + ServiceLocator.soundsManager(ctx), + ), + ) - fun tapCoordinateActionTypeViewModel(context: Context): PickDisplayCoordinateViewModel.Factory = - PickDisplayCoordinateViewModel.Factory( - ServiceLocator.resourceProvider(context), - ) + fun tapCoordinateActionTypeViewModel(context: Context): PickDisplayCoordinateViewModel.Factory = PickDisplayCoordinateViewModel.Factory( + ServiceLocator.resourceProvider(context), + ) - fun swipeCoordinateActionTypeViewModel(context: Context): SwipePickDisplayCoordinateViewModel.Factory = - SwipePickDisplayCoordinateViewModel.Factory( - ServiceLocator.resourceProvider(context), - ) + fun swipeCoordinateActionTypeViewModel(context: Context): SwipePickDisplayCoordinateViewModel.Factory = SwipePickDisplayCoordinateViewModel.Factory( + ServiceLocator.resourceProvider(context), + ) - fun pinchCoordinateActionTypeViewModel(context: Context): PinchPickDisplayCoordinateViewModel.Factory = - PinchPickDisplayCoordinateViewModel.Factory( - ServiceLocator.resourceProvider(context), - ) + fun pinchCoordinateActionTypeViewModel(context: Context): PinchPickDisplayCoordinateViewModel.Factory = PinchPickDisplayCoordinateViewModel.Factory( + ServiceLocator.resourceProvider(context), + ) fun configKeyMapViewModel( ctx: Context, @@ -141,6 +132,10 @@ object Inject { UseCases.createAction(ctx), ServiceLocator.resourceProvider(ctx), ServiceLocator.purchasingManager(ctx), + SetupGuiKeyboardUseCaseImpl( + ServiceLocator.inputMethodAdapter(ctx), + ServiceLocator.packageManagerAdapter(ctx), + ), ) fun configFingerprintMapViewModel( @@ -190,6 +185,10 @@ object Inject { UseCases.showImePicker(ctx), UseCases.onboarding(ctx), ServiceLocator.resourceProvider(ctx), + SetupGuiKeyboardUseCaseImpl( + ServiceLocator.inputMethodAdapter(ctx), + ServiceLocator.packageManagerAdapter(ctx), + ), ) fun settingsViewModel(context: Context): SettingsViewModel.Factory = SettingsViewModel.Factory( @@ -243,43 +242,41 @@ object Inject { fun accessibilityServiceController( service: MyAccessibilityService, keyEventRelayService: KeyEventRelayServiceWrapper, - ): AccessibilityServiceController = - AccessibilityServiceController( - coroutineScope = service.lifecycleScope, - accessibilityService = service, - inputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventsToService, - outputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventReceiver, - detectConstraintsUseCase = UseCases.detectConstraints(service), - performActionsUseCase = UseCases.performActions( - ctx = service, - service = service, - keyEventRelayService = keyEventRelayService, - ), - detectKeyMapsUseCase = UseCases.detectKeyMaps( - ctx = service, - service = service, - keyEventRelayService = keyEventRelayService, - ), - detectFingerprintMapsUseCase = UseCases.detectFingerprintMaps(service), - pauseMappingsUseCase = UseCases.pauseMappings(service), - devicesAdapter = ServiceLocator.devicesAdapter(service), - suAdapter = ServiceLocator.suAdapter(service), - rerouteKeyEventsUseCase = UseCases.rerouteKeyEvents( - ctx = service, - keyEventRelayService = keyEventRelayService, - ), - inputMethodAdapter = ServiceLocator.inputMethodAdapter(service), - settingsRepository = ServiceLocator.settingsRepository(service), - ) + ): AccessibilityServiceController = AccessibilityServiceController( + coroutineScope = service.lifecycleScope, + accessibilityService = service, + inputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventsToService, + outputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventReceiver, + detectConstraintsUseCase = UseCases.detectConstraints(service), + performActionsUseCase = UseCases.performActions( + ctx = service, + service = service, + keyEventRelayService = keyEventRelayService, + ), + detectKeyMapsUseCase = UseCases.detectKeyMaps( + ctx = service, + service = service, + keyEventRelayService = keyEventRelayService, + ), + detectFingerprintMapsUseCase = UseCases.detectFingerprintMaps(service), + pauseMappingsUseCase = UseCases.pauseMappings(service), + devicesAdapter = ServiceLocator.devicesAdapter(service), + suAdapter = ServiceLocator.suAdapter(service), + rerouteKeyEventsUseCase = UseCases.rerouteKeyEvents( + ctx = service, + keyEventRelayService = keyEventRelayService, + ), + inputMethodAdapter = ServiceLocator.inputMethodAdapter(service), + settingsRepository = ServiceLocator.settingsRepository(service), + ) - fun chooseBluetoothDeviceViewModel(ctx: Context): ChooseBluetoothDeviceViewModel.Factory = - ChooseBluetoothDeviceViewModel.Factory( - ChooseBluetoothDeviceUseCaseImpl( - ServiceLocator.devicesAdapter(ctx), - ServiceLocator.permissionAdapter(ctx), - ), - ServiceLocator.resourceProvider(ctx), - ) + fun chooseBluetoothDeviceViewModel(ctx: Context): ChooseBluetoothDeviceViewModel.Factory = ChooseBluetoothDeviceViewModel.Factory( + ChooseBluetoothDeviceUseCaseImpl( + ServiceLocator.devicesAdapter(ctx), + ServiceLocator.permissionAdapter(ctx), + ), + ServiceLocator.resourceProvider(ctx), + ) fun logViewModel(ctx: Context): LogViewModel.Factory = LogViewModel.Factory( DisplayLogUseCaseImpl( diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt index 222cfafa67..786a07fb8f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt @@ -136,6 +136,11 @@ sealed class Error : Result() { data object NetworkError : PurchasingError() data class Unexpected(val message: String) : PurchasingError() } + + /** + * DPAD triggers require a Key Mapper keyboard to be selected. + */ + data object DpadTriggerImeNotSelected : Error() } inline fun Result.onSuccess(f: (T) -> Unit): Result { diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt index 91f115270b..4db23d48bb 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.util import android.os.Parcelable import io.github.sds100.keymapper.actions.ActionData +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -20,6 +21,7 @@ sealed class ServiceEvent { data class RecordedTriggerKey( val keyCode: Int, val device: InputDeviceInfo?, + val detectionSource: KeyEventDetectionSource, ) : ServiceEvent(), Parcelable diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/ViewModelHelper.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/ViewModelHelper.kt index 6f1d750fc9..0a7fdb4ebe 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/ViewModelHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/ViewModelHelper.kt @@ -91,19 +91,11 @@ object ViewModelHelper { } } - suspend fun handleAccessibilityServiceStoppedSnackBar( + suspend fun handleAccessibilityServiceStoppedDialog( resourceProvider: ResourceProvider, popupViewModel: PopupViewModel, startService: () -> Boolean, - @StringRes message: Int, ) { - val snackBar = PopupUi.SnackBar( - message = resourceProvider.getString(message), - actionText = resourceProvider.getString(R.string.pos_turn_on), - ) - - popupViewModel.showPopup("snackbar_enable_service", snackBar) ?: return - val explanationResponse = showAccessibilityServiceExplanationDialog(resourceProvider, popupViewModel) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 9a01f8a351..a86dfc74ce 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -23,6 +23,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + <b>MUSÍTE</b> přečtěte si toto <b>vše</b> , jinak v budoucnu získáte <b>frustrovanou</b> </b>.!\n\nKlepnutím na \"částečně opravit\" <b>může</b> zabránit Androidu v zastavení aplikace v době, kdy je na pozadí.\n\n<b>Toto NEPOČÁTEK</b>. Skin OEM jako MIUI nebo Samsung Experience může mít další funkce zabíjení aplikací, takže je MUSÍ vypnout pro mapování klíčových klíčů a také sledovat online průvodce na dontkillmyapp. kde: Odeslat zpětnou vazbu Přečtěte si prosím návod jak nahlásit problémy na webových stránkách. - Zapněte službu usnadnění přístupu, abyste mohli zaznamenat spouštěč. Restartujte službu přístupnosti vypnutím vypněte a zapněte , abyste mohli zaznamenat spouštěč. - Zapněte službu usnadnění přístupu, abyste mohli vyzkoušet akci. Restartujte službu přístupnosti vypnutím vypněte a dne , abyste mohli vyzkoušet akci. Restartujte službu přístupnosti vypnutím a dne. Použití tohoto spouštěče může způsobit černou obrazovku, když odemknete zařízení po použití nastavení připnutí obrazovky v nastavení zařízení. To lze opravit restartem. To se neděje na všech zařízeních, takže beware a vypněte nastavení, pokud tak učiní! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 370ef81eb7..9185feba32 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -395,9 +395,7 @@ Debes leer esto completamente, de lo contrario te frustrarás en el futuro.\n\nPulsar \"arreglar parcialmente\" puede evitar que Android detenga la aplicación mientras está en segundo plano.\n\nEsto NO ES SUFICIENTE.El tema de tu OEM como MIUI o Samsung Experience puede tener otras funciones de eliminación de aplicaciones, por lo que DEBES desactivarlas también para Key Mapper siguiendo la guía online en dontkillmyapp.com. Enviar comentarios Por favor, lea la guía sobre cómo informar de los problemas en el sitio web. - Activa el servicio de accesibilidad para poder grabar un disparador. Reinicie el servicio de accesibilidad apagándolo y encendiéndolo para poder grabar el activador. - Active el servicio de accesibilidad para poder probar la acción. Reinicie el servicio de accesibilidad apagándolo y encendiéndolo para poder probar la acción. Reinicie el servicio de accesibilidad apagándolo y encendiéndolo. El uso de este activador puede causar una pantalla negra cuando se desbloquea el dispositivo después de usar la configuración de fijación de la pantalla en los ajustes del dispositivo. Esto se puede arreglar con un reinicio. ¡Esto no ocurre en todos los dispositivos, así que ten cuidado y desactiva el ajuste si lo hace! diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f41e9aa83d..ed59ea4ec8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -423,9 +423,7 @@ MUSISZ to wszystko przeczytać, w przeciwnym razie otrzymasz frustrację w przyszłości!\n\nKliknięcie \"Napraw częściowo\" może uniemożliwić Androidowi zatrzymanie aplikacji, gdy działa ona w tle.\n\nTo NIE WYSTARCZY. Skórka producenta, taka jak MIUI lub Samsung Experience, może mieć inne funkcje zatrzymywania aplikacji, więc MUSISZ je wyłączyć dla Key Mapper, postępując zgodnie z przewodnikiem online na dontkillmyapp.com. Prześlij opinię Prosimy o zapoznanie się z poradnikiem jak zgłaszać problemy, na stronie internetowej. - Włącz usługę ułatwień dostępu, aby wprowadzić wyzwalacz. Uruchom ponownie usługę ułatwień dostępu, wyłączając ją i włączając ponownie, zanim będzie można wprowadzić wyzwalacz. - Włącz usługę ułatwień dostępu, aby przetestować czynność. Uruchom ponownie usługę ułatwień dostępu, wyłączając ją i włączając ponownie, aby móc przetestować czynność. Uruchom ponownie usługę ułatwień dostępu, wyłączając ją i włączając ponownie. Użycie tego wyzwalacza może spowodować czarny ekran po odblokowaniu urządzenia po użyciu fukcji przypinania ekranu w ustawieniach urządzenia. Można to naprawić przez ponowne uruchomienie urządzenia. Nie dzieje się tak na wszystkich urządzeniach, więc uważaj i wyłącz to ustawienie, jeśli tak się stanie! diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c7dc6c31cd..488f1730da 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -418,9 +418,7 @@ Você DEVE ler tudo isso senão você ficará frustrado no futuro!\n\nTocar em \"corrigir parcialmente\" pode impedir que o Android pare o aplicativo enquanto ele está em segundo plano.\n\nISSO NÃO É SUFICIENTE. A interface do seu OEM, como MIUI ou Samsung Experience, pode ter outros recursos de encerramento de aplicativos, então você DEVE desativá-los para o Key Mapper também, seguindo o guia online em dontkillmyapp.com. Enviar feedback Leia o guia sobre como relatar problemas no site. - Ative o serviço de acessibilidade para que você possa gravar um gatilho. Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente para que você possa gravar um gatilho. - Ative o serviço de acessibilidade para que você possa testar a ação. Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente para que você possa testar a ação. Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente. Usar este gatilho pode causar uma tela preta quando você desbloqueia seu dispositivo após usar a configuração de fixação de tela nas configurações do seu dispositivo. Isso pode ser corrigido com uma reinicialização. Isso não acontece em todos os dispositivos, então fique atento e desative a configuração se isso ocorrer! diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4d55cb73e7..eafff10cb3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -396,9 +396,7 @@ Вы MUST прочитать это все иначе вы получите фрустрировано в будущем!\n\nВыполнение «исправления частично» майт помешать Android остановить приложение, пока оно находится в фоновом режиме.\n\nЭто НЕДОСТАТОЧНО. Скин вашего OEM-производителя, такой как MIUI или Samsung Experience, может иметь другие функции уничтожения приложений, поэтому вы ДОЛЖНЫ отключить их для Key Mapper, а также следуя онлайн-руководству на dontkillmyapp.com. Отправить отзыв Пожалуйста, ознакомьтесь с руководством о том, как сообщать о проблемах на веб-сайте. - Включите службу специальных возможностей, чтобы можно было записать триггер. Перезапустите службу специальных возможностей, включив ее off и on чтобы можно было записать триггер. - Включите службу специальных возможностей, чтобы можно было протестировать действие. Перезапустите службу специальных возможностей, включив ее выключенной и он чтобы можно было протестировать действие. Перезапустите службу спец. возможностей, выключив и включив её. Использование этого триггера может привести к черному экрану при разблокировке устройства после использования параметра закрепления экрана в настройках устройства. Это можно исправить с помощью перезагрузки. Это происходит не на всех устройствах, поэтому будьте осторожны и отключите настройку, если это так! diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 083daab72b..dc4eb004f9 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -370,9 +370,7 @@ MUSÍTE si prečítať totocelé, inak budete v budúcnosti frustrovaný! \n\nStlačte \"opraviť čiastočne\" môže zabrániť Androidu zastaviť aplikáciu, keď je na pozadí.\n\nToto NESTAČÍ. Váš OEM skin, ako napríklad MIUI alebo Samsung Experience, môže obsahovať iné funkcie na zabíjanie aplikácií, takže ich musíte vypnúť aj pre Key Mapper podľa online sprievodcu na dontkillmyapp.com. Odoslať spätnú väzbu Prečítajte si príručku o tom, ako nahlásiť problémy na webovú stránku. - Zapnite službu zjednodušenia ovládania, aby ste mohli nahrať spúšťač. Reštartujte službu zjednodušenia ovládania tak, že ju vypnete a zapnete, aby ste mohli nahrať spúšťač. - Zapnite službu zjednodušenia ovládania, aby ste mohli akciu otestovať. Reštartujte službu zjednodušenia ovládania tak, že ju vyp. a zap., aby ste mohli akciu otestovať. Reštartujte službu zjednodušenia ovládania tak, že ju vyp. a zap.. Použitie tohto spúšťača môže spôsobiť čiernu obrazovku pri odomknutí zariadenia po použití nastavenia pripovania obrazovky v nastaveniach zariadenia. To môže byť opravené reštartom. To sa nestane na všetkých zariadeniach, takže pozor a vypnite nastavenie, ak to robí! diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index e45a61696a..1df90c6695 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -402,9 +402,7 @@ Bạn PHẢI đọc tất cả này nếu không bạn sẽ thất vọng trong tương lai!\n\nNhấn vào \"sửa một phần\" có thể ngăn Android dừng ứng dụng khi ứng dụng đang chạy trong nền.\n\nĐiều này KHÔNG ĐỦ. Giao diện OEM của bạn như MIUI hoặc Samsung Experience có thể có các tính năng tiêu diệt ứng dụng khác, vì vậy bạn PHẢI tắt chúng cho Key Mapper bằng cách làm theo hướng dẫn trực tuyến tại Dontkillmyapp.com. Gửi phản hồi Vui lòng đọc hướng dẫn về cách báo cáo sự cố trên trang web. - Bật dịch vụ trợ năng để bạn có thể ghi lại trình kích hoạt. Khởi động lại dịch vụ trợ năng bằng cách tắtbật để bạn có thể ghi lại trình kích hoạt. - Bật dịch vụ trợ năng để bạn có thể kiểm tra hành động. Khởi động lại dịch vụ trợ năng bằng cách tắtbật để bạn có thể kiểm tra hành động. Khởi động lại dịch vụ trợ năng bằng cách tắtbật. Việc sử dụng trình kích hoạt này có thể gây ra màn hình đen khi bạn mở khóa thiết bị sau khi sử dụng cài đặt ghim màn hình trong cài đặt của thiết bị. Điều này có thể được khắc phục bằng cách khởi động lại. Điều này không xảy ra trên tất cả các thiết bị vì vậy hãy cẩn thận và tắt cài đặt nếu xảy ra! diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 25073c5956..779ff8a140 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -419,9 +419,7 @@ 必须阅读此全部,否则您将来会沮丧!\n\n点击“部分修复”可能 防止 Android 在后台停止应用程序。\n\n这还不够。 您的 OEM 厂商(例如 MIUI 或 Samsung Experience)可能具有其他应用程序终止功能,因此您必须按照 dontkillmyapp.com 上的在线指南为 Key Mapper 关闭它们。 发送反馈 请阅读有关如何在网站上报告问题的指南。 - 打开无障碍服务,以便您可以记录触发器。 通过offon重新启动无障碍服务,这样您就可以记录触发器。 - 打开无障碍服务,以便您可以测试操作。 通过打开关闭重新启动无障碍服务,以便您可以测试该操作。 通过打开关闭重新启动无障碍服务,以便您可以测试该操作。 使用设备设置中的屏幕固定设置后解锁设备时,使用此触发器可能会导致黑屏。 这可以通过重新启动来解决。 并非所有设备都会发生这种情况,因此请注意并关闭设置! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 814f42a8a2..335d690412 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Unleash your keys! No action chosen - Key Mapper requires the use of an accessibility service so that it can detect and change what your button presses do while you are outside of the app. Your key maps will only work once you have enabled the accessibility service. It must also be turned on to create a trigger. + Key Mapper requires the use of an accessibility service so that it can detect and change what your button presses do while you are outside of the app. Your key maps will only work once you have enabled the accessibility service. It must also be turned on to create a trigger and test actions. %d selected Key Mapper Basic Input Method Enable @@ -106,11 +106,13 @@ Key Mapper requires permission to modify the Do Not Disturb mode if you want the buttons to work as expected in Do Not Disturb mode! This trigger will not work as expected in Do Not Disturb mode! The option to trigger when the screen is off needs root permission to work! - The option to trigger when the screen is off will not work! + Screen off triggers will not work! This trigger won\'t work while ringing or in a phone call! Android doesn\'t let accessibility services detect volume button presses while your phone is ringing or it is in a phone call but it does let input method services detect them. Therefore, you must use one of the Key Mapper keyboards if you want this trigger to work. Too many fingers to perform gesture due to android limitations. Gesture duration is too high due to android limitations. + You must be using a Key Mapper keyboard for DPAD triggers to work! + DPAD triggers will not work! Your mappings will stop working randomly! Your mappings are paused! @@ -543,10 +545,8 @@ Send feedback Please read the guide on how to report issues on the website. - Turn on the accessibility service so that you can record a trigger. Restart the accessibility service by turning it off and on so that you can record a trigger. - Turn on the accessibility service so that you can test the action. Restart the accessibility service by turning it off and on so that you can test the action. Restart the accessibility service by turning it off and on. @@ -581,6 +581,14 @@ Grant Do Not Disturb access You will be taken to your device\'s settings page to manage which apps can modify the Do Not Disturb state. This is not present on some devices so tap don\'t show again if you do not see Key Mapper in the list. + Good to know! + If you see this symbol (⌨) next to a trigger key then you MUST use a Key Mapper keyboard for it to be detected. This is a restriction in Android and is only required for some buttons. + + Important! + You must update the Key Mapper GUI Keyboard so it is compatible with this version of Key Mapper. Some key maps may not work until you update! + Update now + Ignore + Yes Confirm Done @@ -777,6 +785,13 @@ Default mapping options Change the default options for your mappings. + Reset all settings + DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. + DANGER! + Are you sure you want to reset all settings in the app to the default? Your key maps will NOT be reset. The introduction screen and all warning pop ups will show again. + Yes, reset + + @@ -849,6 +864,7 @@ Do not remap Hold down until swiped again Allow other apps to trigger this key map + @@ -1417,4 +1433,25 @@ Please fill the following information so I can help you.\n\n1. Device model:\n2. Android version:\n3. Key maps (make a back up in the home screen menu):\n4. Screenshot of Key Mapper home screen:\n5. Describe the problem you are having: The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature. Download Play build + + + + Want to remap DPAD buttons? + You must set up the Key Mapper GUI Keyboard following the steps below. + 1. Install the keyboard app + Install + Installed + 2. Enable the keyboard + Enable + Enabled + 3. Use the keyboard + Change keyboard + Keyboard selected + Setup complete! Tap \'Done\' and your DPAD trigger should work. + + + Button not detected? + You can try using the Key Mapper GUI Keyboard app to record your trigger instead of the accessibility service. + Setup complete! Tap \'Done\' and try recording your trigger again. If it doesn’t work then Android will not let it be remapped 🫤. + diff --git a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt index cfd6c56ac3..f422764f08 100644 --- a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt @@ -10,10 +10,11 @@ import io.github.sds100.keymapper.mappings.keymaps.KeyMapAction import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode -import io.github.sds100.keymapper.system.keyevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.singleKeyTrigger @@ -50,104 +51,138 @@ class ConfigKeyMapUseCaseTest { ) } + @Test + fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + TriggerKeyDevice.Any, + KeyEventDetectionSource.INPUT_METHOD, + ) + + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + + val actionList = useCase.mapping.value.dataOrNull()!!.actionList + assertThat(actionList[0].holdDown, `is`(true)) + assertThat(actionList[0].repeat, `is`(false)) + } + /** * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. */ @Test - fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = - runTest(testDispatcher) { - useCase.mapping.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.setParallelTriggerMode() + fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) + useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) + useCase.setParallelTriggerMode() + + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(2)) + assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } @Test - fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = - runTest(testDispatcher) { - useCase.mapping.value = State.Data(KeyMap()) + fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.setParallelTriggerMode() - - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) + useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) + useCase.setParallelTriggerMode() + + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.keys, hasSize(2)) + assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) + assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } @Test - fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = - runTest(testDispatcher) { - useCase.mapping.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any) - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_UP, TriggerKeyDevice.Any) - useCase.setTriggerLongPress() + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Set click type to short press when adding assistant key to double press trigger key`() = - runTest(testDispatcher) { - useCase.mapping.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to double press trigger key`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any) - useCase.setTriggerDoublePress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.setTriggerDoublePress() + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Set click type to short press when adding assistant key to long press trigger key`() = - runTest(testDispatcher) { - useCase.mapping.value = State.Data(KeyMap()) + fun `Set click type to short press when adding assistant key to long press trigger key`() = runTest(testDispatcher) { + useCase.mapping.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any) - useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.setTriggerLongPress() + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } @Test - fun `Do not allow long press for parallel trigger with assistant key`() = - runTest(testDispatcher) { - val keyMap = KeyMap( - trigger = Trigger( - mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), - keys = listOf( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, - ), + fun `Do not allow long press for parallel trigger with assistant key`() = runTest(testDispatcher) { + val keyMap = KeyMap( + trigger = Trigger( + mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), + keys = listOf( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, ), ), - ) + ), + ) - useCase.mapping.value = State.Data(keyMap) - useCase.setTriggerLongPress() + useCase.mapping.value = State.Data(keyMap) + useCase.setTriggerLongPress() - val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } + val trigger = useCase.mapping.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } /** * Issue #753. If a modifier key is used as a trigger then it the @@ -155,131 +190,133 @@ class ConfigKeyMapUseCaseTest { * key can still be used normally. */ @Test - fun `when add modifier key trigger, enable do not remap option`() = - runTest(testDispatcher) { - val modifierKeys = setOf( - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_ALT_RIGHT, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_SYM, - KeyEvent.KEYCODE_NUM, - KeyEvent.KEYCODE_FUNCTION, - ) + fun `when add modifier key trigger, enable do not remap option`() = runTest(testDispatcher) { + val modifierKeys = setOf( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_SYM, + KeyEvent.KEYCODE_NUM, + KeyEvent.KEYCODE_FUNCTION, + ) - for (modifierKeyCode in modifierKeys) { - // GIVEN - useCase.mapping.value = State.Data(KeyMap()) + for (modifierKeyCode in modifierKeys) { + // GIVEN + useCase.mapping.value = State.Data(KeyMap()) - // WHEN - useCase.addKeyCodeTriggerKey(modifierKeyCode, TriggerKeyDevice.Internal) + // WHEN + useCase.addKeyCodeTriggerKey( + modifierKeyCode, + TriggerKeyDevice.Internal, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) - // THEN - val trigger = useCase.mapping.value.dataOrNull()!!.trigger + // THEN + val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.keys[0].consumeEvent, `is`(false)) - } + assertThat(trigger.keys[0].consumeEvent, `is`(false)) } + } /** * Issue #753. */ @Test - fun `when add non-modifier key trigger, do ont enable do not remap option`() = - runTest(testDispatcher) { - // GIVEN - useCase.mapping.value = State.Data(KeyMap()) - - // WHEN - useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_A, TriggerKeyDevice.Internal) + fun `when add non-modifier key trigger, do ont enable do not remap option`() = runTest(testDispatcher) { + // GIVEN + useCase.mapping.value = State.Data(KeyMap()) + + // WHEN + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_A, + TriggerKeyDevice.Internal, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) - // THEN - val trigger = useCase.mapping.value.dataOrNull()!!.trigger + // THEN + val trigger = useCase.mapping.value.dataOrNull()!!.trigger - assertThat(trigger.keys[0].consumeEvent, `is`(true)) - } + assertThat(trigger.keys[0].consumeEvent, `is`(true)) + } /** * Issue #852. Add a phone ringing constraint when you add an action * to answer a phone call. */ @Test - fun `when add answer phone call action, then add phone ringing constraint`() = - runTest(testDispatcher) { - // GIVEN - useCase.mapping.value = State.Data(KeyMap()) - val action = ActionData.AnswerCall + fun `when add answer phone call action, then add phone ringing constraint`() = runTest(testDispatcher) { + // GIVEN + useCase.mapping.value = State.Data(KeyMap()) + val action = ActionData.AnswerCall - // WHEN - useCase.addAction(action) + // WHEN + useCase.addAction(action) - // THEN - val keyMap = useCase.mapping.value.dataOrNull()!! - assertThat(keyMap.constraintState.constraints, contains(Constraint.PhoneRinging)) - } + // THEN + val keyMap = useCase.mapping.value.dataOrNull()!! + assertThat(keyMap.constraintState.constraints, contains(Constraint.PhoneRinging)) + } /** * Issue #852. Add a in phone call constraint when you add an action * to end a phone call. */ @Test - fun `when add end phone call action, then add in phone call constraint`() = - runTest(testDispatcher) { - // GIVEN - useCase.mapping.value = State.Data(KeyMap()) - val action = ActionData.EndCall + fun `when add end phone call action, then add in phone call constraint`() = runTest(testDispatcher) { + // GIVEN + useCase.mapping.value = State.Data(KeyMap()) + val action = ActionData.EndCall - // WHEN - useCase.addAction(action) + // WHEN + useCase.addAction(action) - // THEN - val keyMap = useCase.mapping.value.dataOrNull()!! - assertThat(keyMap.constraintState.constraints, contains(Constraint.InPhoneCall)) - } + // THEN + val keyMap = useCase.mapping.value.dataOrNull()!! + assertThat(keyMap.constraintState.constraints, contains(Constraint.InPhoneCall)) + } /** * issue #593 */ @Test - fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = - runTest(testDispatcher) { - // given - val action = KeyMapAction( - data = ActionData.TapScreen(100, 100, null), - holdDown = true, - ) + fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = runTest(testDispatcher) { + // given + val action = KeyMapAction( + data = ActionData.TapScreen(100, 100, null), + holdDown = true, + ) - val keyMap = KeyMap( - 0, - trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), - actionList = listOf(action), - ) + val keyMap = KeyMap( + 0, + trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), + actionList = listOf(action), + ) - // when - useCase.mapping.value = State.Data(keyMap) + // when + useCase.mapping.value = State.Data(keyMap) - // then - assertThat(useCase.mapping.value.dataOrNull()!!.actionList, `is`(listOf(action))) - } + // then + assertThat(useCase.mapping.value.dataOrNull()!!.actionList, `is`(listOf(action))) + } @Test - fun `add modifier key event action, enable hold down option and disable repeat option`() = - runTest(testDispatcher) { - KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> - useCase.mapping.value = State.Data(KeyMap()) - - useCase.addAction(ActionData.InputKeyEvent(keyCode)) - - useCase.mapping.value.dataOrNull()!!.actionList - .single() - .let { - assertThat(it.holdDown, `is`(true)) - assertThat(it.repeat, `is`(false)) - } - } + fun `add modifier key event action, enable hold down option and disable repeat option`() = runTest(testDispatcher) { + InputEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> + useCase.mapping.value = State.Data(KeyMap()) + + useCase.addAction(ActionData.InputKeyEvent(keyCode)) + + useCase.mapping.value.dataOrNull()!!.actionList + .single() + .let { + assertThat(it.holdDown, `is`(true)) + assertThat(it.repeat, `is`(false)) + } } + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt index 483d51af3c..b338b65d2c 100644 --- a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt @@ -4,8 +4,8 @@ import android.view.KeyEvent import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeMessenger import io.github.sds100.keymapper.system.popup.PopupMessageAdapter import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.InputEventType @@ -40,14 +40,14 @@ class PerformActionsUseCaseTest { private val testScope = TestScope(testDispatcher) private lateinit var useCase: PerformActionsUseCaseImpl - private lateinit var mockKeyMapperImeMessenger: KeyMapperImeMessenger + private lateinit var mockImeInputEventInjector: ImeInputEventInjector private lateinit var fakeDevicesAdapter: FakeDevicesAdapter private lateinit var mockAccessibilityService: IAccessibilityService private lateinit var mockToastAdapter: PopupMessageAdapter @Before fun init() { - mockKeyMapperImeMessenger = mock() + mockImeInputEventInjector = mock() fakeDevicesAdapter = FakeDevicesAdapter() mockAccessibilityService = mock() mockToastAdapter = mock() @@ -63,7 +63,7 @@ class PerformActionsUseCaseTest { shellAdapter = mock(), intentAdapter = mock(), getActionError = mock(), - keyMapperImeMessenger = mockKeyMapperImeMessenger, + imeInputEventInjector = mockImeInputEventInjector, packageManagerAdapter = mock(), appShortcutAdapter = mock(), popupMessageAdapter = mockToastAdapter, @@ -92,239 +92,233 @@ class PerformActionsUseCaseTest { * issue #771 */ @Test - fun `dont show accessibility service not found error for open menu action`() = - runTest(testDispatcher) { - // GIVEN - val action = ActionData.OpenMenu - - whenever( - mockAccessibilityService.performActionOnNode( - any(), - any(), - ), - ).doReturn(Error.FailedToFindAccessibilityNode) - - // WHEN - useCase.perform(action) - - // THEN - verify(mockToastAdapter, never()).showPopupMessage(anyOrNull()) - } + fun `dont show accessibility service not found error for open menu action`() = runTest(testDispatcher) { + // GIVEN + val action = ActionData.OpenMenu + + whenever( + mockAccessibilityService.performActionOnNode( + any(), + any(), + ), + ).doReturn(Error.FailedToFindAccessibilityNode) + + // WHEN + useCase.perform(action) + + // THEN + verify(mockToastAdapter, never()).showPopupMessage(anyOrNull()) + } /** * issue #772 */ @Test - fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = - runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) - - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) - - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeGamePad.id, - scanCode = 0, - repeat = 0, - ) + fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + ) - verify(mockKeyMapperImeMessenger, times(1)).inputKeyEvent(expectedInputKeyModel) - } + fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) + + // WHEN + useCase.perform(action) + + // THEN + val expectedInputKeyModel = InputKeyModel( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + inputType = InputEventType.DOWN_UP, + metaState = 0, + deviceId = fakeGamePad.id, + scanCode = 0, + repeat = 0, + ) + + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = - runTest(testDispatcher) { - // GIVEN - fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) - - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) - - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = 0, - scanCode = 0, - repeat = 0, - ) + fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = runTest(testDispatcher) { + // GIVEN + fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) - verify(mockKeyMapperImeMessenger, times(1)).inputKeyEvent(expectedInputKeyModel) - } + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) + + // WHEN + useCase.perform(action) + + // THEN + val expectedInputKeyModel = InputKeyModel( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + inputType = InputEventType.DOWN_UP, + metaState = 0, + deviceId = 0, + scanCode = 0, + repeat = 0, + ) + + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = - runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - ) - - val fakeKeyboard = InputDeviceInfo( + fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + ) + + val fakeKeyboard = InputDeviceInfo( + descriptor = "keyboard", + name = "Keyboard", + id = 2, + isExternal = true, + isGameController = false, + ) + + fakeDevicesAdapter.connectedInputDevices.value = + State.Data(listOf(fakeGamePad, fakeKeyboard)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = ActionData.InputKeyEvent.Device( descriptor = "keyboard", name = "Keyboard", - id = 2, - isExternal = true, - isGameController = false, - ) - - fakeDevicesAdapter.connectedInputDevices.value = - State.Data(listOf(fakeGamePad, fakeKeyboard)) - - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = ActionData.InputKeyEvent.Device( - descriptor = "keyboard", - name = "Keyboard", - ), - ) - - // WHEN - useCase.perform(action) + ), + ) - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeKeyboard.id, - scanCode = 0, - repeat = 0, - ) + // WHEN + useCase.perform(action) + + // THEN + val expectedInputKeyModel = InputKeyModel( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + inputType = InputEventType.DOWN_UP, + metaState = 0, + deviceId = fakeKeyboard.id, + scanCode = 0, + repeat = 0, + ) - verify(mockKeyMapperImeMessenger, times(1)).inputKeyEvent(expectedInputKeyModel) - } + verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) + } /** * issue #637 */ @Test - fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = - runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" + fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" + + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + useShell = false, + device = ActionData.InputKeyEvent.Device( + descriptor = descriptor, + name = "fake_name_2", + ), + ) - val action = ActionData.InputKeyEvent( - keyCode = 1, - metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device( + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( descriptor = descriptor, - name = "fake_name_2", + name = "fake_name_1", + id = 10, + isExternal = true, + isGameController = false, ), - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name_1", - id = 10, - isExternal = true, - isGameController = false, - ), - - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name_2", - id = 11, - isExternal = true, - isGameController = false, - ), - ), - ) - - // none of the devices support the key code - fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } - - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) - - // THEN - verify(mockKeyMapperImeMessenger, times(1)).inputKeyEvent( - InputKeyModel( - keyCode = 1, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = 11, - scanCode = 0, - repeat = 0, + + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name_2", + id = 11, + isExternal = true, + isGameController = false, ), - ) - } + ), + ) - @Test - fun `perform key event action with no device name, ensure action is still performed with correct device id`() = - runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" + // none of the devices support the key code + fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } + + // WHEN + useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) - val action = ActionData.InputKeyEvent( + // THEN + verify(mockImeInputEventInjector, times(1)).inputKeyEvent( + InputKeyModel( keyCode = 1, + inputType = InputEventType.DOWN_UP, metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name", - id = 10, - isExternal = true, - isGameController = false, - ), - ), - ) - - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) - - // THEN - verify(mockKeyMapperImeMessenger, times(1)).inputKeyEvent( - InputKeyModel( - keyCode = 1, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = 10, - scanCode = 0, - repeat = 0, + deviceId = 11, + scanCode = 0, + repeat = 0, + ), + ) + } + + @Test + fun `perform key event action with no device name, ensure action is still performed with correct device id`() = runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" + + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + useShell = false, + device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name", + id = 10, + isExternal = true, + isGameController = false, ), - ) - } + ), + ) + + // WHEN + useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) + + // THEN + verify(mockImeInputEventInjector, times(1)).inputKeyEvent( + InputKeyModel( + keyCode = 1, + inputType = InputEventType.DOWN_UP, + metaState = 0, + deviceId = 10, + scanCode = 0, + repeat = 0, + ), + ) + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FakePreferenceRepository.kt b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FakePreferenceRepository.kt index fce29c3763..8d75b55cd6 100644 --- a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FakePreferenceRepository.kt +++ b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FakePreferenceRepository.kt @@ -9,7 +9,8 @@ import kotlinx.coroutines.flow.map * Created by sds100 on 26/04/2021. */ class FakePreferenceRepository : PreferenceRepository { - private val preferences: MutableStateFlow, Any?>> = MutableStateFlow(emptyMap()) + private val preferences: MutableStateFlow, Any?>> = + MutableStateFlow(emptyMap()) @Suppress("UNCHECKED_CAST") override fun get(key: Preferences.Key): Flow { @@ -21,4 +22,8 @@ class FakePreferenceRepository : PreferenceRepository { this[key] = value } } + + override fun deleteAll() { + preferences.value = emptyMap() + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt index dc9e2bf70d..fbac8d08d0 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt @@ -3,12 +3,15 @@ package io.github.sds100.keymapper.mappings.keymaps import android.view.KeyEvent import io.github.sds100.keymapper.R import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerState import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordedKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.onboarding.FakeOnboardingUseCase +import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.ui.FakeResourceProvider import io.github.sds100.keymapper.util.ui.PopupUi import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -79,7 +82,11 @@ class ConfigTriggerViewModelTest { onBlocking { getTriggerErrors(any()) }.thenReturn(emptyList()) }, fakeResourceProvider, - purchasingManager = mock(), + purchasingManager = mock { + onBlocking { isPurchased(ProductId.ASSISTANT_TRIGGER) }.thenReturn(Success(false)) + onBlocking { getProductPrice(ProductId.ASSISTANT_TRIGGER) }.thenReturn(Success("")) + }, + mock(), ) } @@ -87,21 +94,21 @@ class ConfigTriggerViewModelTest { * issue #602 */ @Test - fun `when create back button trigger key then prompt the user to disable screen pinning`() = - runTest(testDispatcher) { - // GIVEN - fakeResourceProvider.stringResourceMap[R.string.dialog_message_screen_pinning_warning] = - "bla" + fun `when create back button trigger key then prompt the user to disable screen pinning`() = runTest(testDispatcher) { + // GIVEN + fakeResourceProvider.stringResourceMap[R.string.dialog_message_screen_pinning_warning] = + "bla" - // WHEN - onRecordKey.emit( - RecordedKey( - keyCode = KeyEvent.KEYCODE_BACK, - device = TriggerKeyDevice.Internal, - ), - ) + // WHEN + onRecordKey.emit( + RecordedKey( + keyCode = KeyEvent.KEYCODE_BACK, + device = TriggerKeyDevice.Internal, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ), + ) - // THEN - assertThat(viewModel.showPopup.first().ui, `is`(PopupUi.Ok("bla"))) - } + // THEN + assertThat(viewModel.showPopup.first().ui, `is`(PopupUi.Ok("bla"))) + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt new file mode 100644 index 0000000000..b323630108 --- /dev/null +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt @@ -0,0 +1,293 @@ +package io.github.sds100.keymapper.mappings.keymaps + +import android.view.KeyEvent +import io.github.sds100.keymapper.mappings.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import junitparams.JUnitParamsRunner +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.hasSize +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Created by sds100 on 15/05/2021. + */ + +@ExperimentalCoroutinesApi +@RunWith(JUnitParamsRunner::class) +class DpadMotionEventTrackerTest { + + companion object { + private val CONTROLLER_1_DEVICE = InputDeviceInfo( + id = 0, + descriptor = "controller_1", + name = "Controller 1", + isExternal = true, + isGameController = true, + ) + + private val CONTROLLER_2_DEVICE = InputDeviceInfo( + id = 1, + descriptor = "controller_2", + name = "Controller 2", + isExternal = true, + isGameController = true, + ) + } + + private lateinit var tracker: DpadMotionEventTracker + + @Before + fun init() { + tracker = DpadMotionEventTracker() + } + + @Test + fun `Detect multiple key events if two DPAD buttons changed in the same motion event`() { + var motionEvent = createMotionEvent(axisHatX = -1.0f) + tracker.convertMotionEvent(motionEvent) + + motionEvent = motionEvent.copy(axisHatY = -1.0f) + tracker.convertMotionEvent(motionEvent) + + motionEvent = motionEvent.copy(axisHatX = 0.0f, axisHatY = 0.0f) + val keyEvents = tracker.convertMotionEvent(motionEvent) + + assertThat(keyEvents, hasSize(2)) + assertThat( + keyEvents, + hasItem( + MyKeyEvent( + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.ACTION_UP, + metaState = 0, + scanCode = 0, + device = CONTROLLER_1_DEVICE, + repeatCount = 0, + ), + ), + ) + assertThat( + keyEvents, + hasItem( + MyKeyEvent( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.ACTION_UP, + metaState = 0, + scanCode = 0, + device = CONTROLLER_1_DEVICE, + repeatCount = 0, + ), + ), + ) + } + + @Test + fun `Consume DPAD key events when joystick motion events are received while multiple DPAD buttons are pressed`() { + var motionEvent = createMotionEvent(axisHatX = -1.0f) + tracker.convertMotionEvent(motionEvent) + + motionEvent = motionEvent.copy(axisHatY = -1.0f) + tracker.convertMotionEvent(motionEvent) + + var consume = + tracker.onKeyEvent(createDownKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(true)) + consume = + tracker.onKeyEvent(createDownKeyEvent(KeyEvent.KEYCODE_DPAD_UP, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(true)) + + motionEvent = motionEvent.copy(axisHatX = 0.0f) + tracker.convertMotionEvent(motionEvent) + motionEvent = motionEvent.copy(axisHatY = 0.0f) + tracker.convertMotionEvent(motionEvent) + + consume = + tracker.onKeyEvent(createUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(false)) + tracker.onKeyEvent(createUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(false)) + } + + @Test + fun `Consume DPAD key events when joystick motion events are received while processing DPAD motion event`() { + tracker.convertMotionEvent(createMotionEvent(axisHatX = -1.0f)) + var consume = + tracker.onKeyEvent(createDownKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(true)) + + tracker.convertMotionEvent(createMotionEvent(axisHatX = 0.0f)) + + consume = + tracker.onKeyEvent(createUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, CONTROLLER_1_DEVICE)) + assertThat(consume, `is`(false)) + } + + @Test + fun `Track DPAD left and right key events from two controllers`() { // Press DPAD left + tracker.convertMotionEvent( + createMotionEvent( + axisHatX = -1.0f, + device = CONTROLLER_1_DEVICE, + ), + ) + val keyEvent = tracker.convertMotionEvent( + createMotionEvent( + axisHatX = 1.0f, + device = CONTROLLER_2_DEVICE, + ), + ) + + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_RIGHT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + } + + @Test + fun `Track DPAD left key events from two controllers`() { // Press DPAD left + tracker.convertMotionEvent( + createMotionEvent( + axisHatX = -1.0f, + device = CONTROLLER_1_DEVICE, + ), + ) + val keyEvent = tracker.convertMotionEvent( + createMotionEvent( + axisHatX = -1.0f, + device = CONTROLLER_2_DEVICE, + ), + ) + + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_LEFT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + } + + @Test + fun `Interleave press and release of two dpad buttons`() { + var motionEvent = createMotionEvent(axisHatX = -1.0f) + // Press DPAD left + tracker.convertMotionEvent(motionEvent) + + // Press DPAD up + motionEvent = motionEvent.copy(axisHatY = -1.0f) + var keyEvent = tracker.convertMotionEvent(motionEvent) + + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_UP)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + + // Release DPAD left + motionEvent = motionEvent.copy(axisHatX = 0.0f) + keyEvent = tracker.convertMotionEvent(motionEvent) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_LEFT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + + // Release DPAD up + motionEvent = motionEvent.copy(axisHatY = 0.0f) + keyEvent = tracker.convertMotionEvent(motionEvent) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_UP)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + } + + @Test + fun `Track DPAD up key event while left is pressed down`() { + val motionEvent = createMotionEvent(axisHatX = -1.0f) + // Press DPAD left + tracker.convertMotionEvent(motionEvent) + + // Press DPAD up + var keyEvent = tracker.convertMotionEvent(motionEvent.copy(axisHatY = -1.0f)) + + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_UP)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + + // Release DPAD up + keyEvent = tracker.convertMotionEvent(motionEvent.copy(axisHatY = 0.0f)) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_UP)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + + // Release DPAD left + keyEvent = tracker.convertMotionEvent(motionEvent.copy(axisHatX = 0.0f)) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_LEFT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + } + + @Test + fun `Track DPAD right key event after left is pressed down and released`() { + // Press DPAD left + tracker.convertMotionEvent(createMotionEvent(axisHatX = -1.0f)) + // Release DPAD left + tracker.convertMotionEvent(createMotionEvent(axisHatX = 0.0f)) + + // Press DPAD right + var keyEvent = tracker.convertMotionEvent(createMotionEvent(axisHatX = 1.0f)) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_RIGHT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + + // Release DPAD right + keyEvent = tracker.convertMotionEvent(createMotionEvent(axisHatX = 0.0f)) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_RIGHT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + } + + @Test + fun `DPAD left key event is UP on release`() { + // Press DPAD left + tracker.convertMotionEvent(createMotionEvent(axisHatX = -1.0f)) + + // Release DPAD left + val keyEvent = tracker.convertMotionEvent(createMotionEvent(axisHatX = 0.0f)) + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_LEFT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_UP)) + } + + @Test + fun `DPAD left key event is DOWN when pressed down`() { + // Press DPAD left + val keyEvent = tracker.convertMotionEvent(createMotionEvent(axisHatX = -1.0f)) + + assertThat(keyEvent.first().keyCode, `is`(KeyEvent.KEYCODE_DPAD_LEFT)) + assertThat(keyEvent.first().action, `is`(KeyEvent.ACTION_DOWN)) + } + + private fun createMotionEvent( + axisHatX: Float = 0.0f, + axisHatY: Float = 0.0f, + device: InputDeviceInfo = CONTROLLER_1_DEVICE, + isDpad: Boolean = true, + ): MyMotionEvent { + return MyMotionEvent( + metaState = 0, + device = device, + axisHatX = axisHatX, + axisHatY = axisHatY, + isDpad = isDpad, + ) + } + + private fun createDownKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { + return MyKeyEvent( + keyCode = keyCode, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + scanCode = 0, + device = device, + repeatCount = 0, + ) + } + + private fun createUpKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { + return MyKeyEvent( + keyCode = keyCode, + action = KeyEvent.ACTION_UP, + metaState = 0, + scanCode = 0, + device = device, + repeatCount = 0, + ) + } +} diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt index ff930a4603..debed73113 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt @@ -13,12 +13,15 @@ import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.MyKeyEvent +import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.parallelTrigger @@ -74,7 +77,20 @@ class KeyMapControllerTest { private const val FAKE_HEADPHONE_DESCRIPTOR = "fake_headphone" private val FAKE_HEADPHONE_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( descriptor = FAKE_HEADPHONE_DESCRIPTOR, - name = "Fake HeadPhones", + name = "Fake Headphones", + ) + + private const val FAKE_CONTROLLER_DESCRIPTOR = "fake_controller" + private val FAKE_CONTROLLER_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + descriptor = FAKE_CONTROLLER_DESCRIPTOR, + name = "Fake Controller", + ) + private val FAKE_CONTROLLER_INPUT_DEVICE = InputDeviceInfo( + descriptor = FAKE_CONTROLLER_DESCRIPTOR, + name = "Fake Controller", + id = 0, + isExternal = true, + isGameController = true, ) private const val FAKE_PACKAGE_NAME = "test_package" @@ -165,296 +181,523 @@ class KeyMapControllerTest { ) } + @Test + fun `Hold down key event action while DPAD button is held down via motion events`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.SHORT_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + ), + ) + + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_Q), + holdDown = true, + ) + + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger, actionList = listOf(action)), + ) + + inOrder(performActionsUseCase) { + inputMotionEvent(axisHatX = -1.0f) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.DOWN) + + delay(1000) // Hold down the DPAD button for 1 second. + inputMotionEvent(axisHatX = 0.0f) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.UP) + } + } + + @Test + fun `Trigger short press key map from DPAD motion event while another DPAD button is held down`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.SHORT_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), + ) + + var motionEvent = createMotionEvent(axisHatY = 1.0f) + val consumeDown1 = controller.onMotionEvent(motionEvent) + assertThat(consumeDown1, `is`(false)) + + motionEvent = motionEvent.copy(axisHatX = -1.0f) + val consumeDown2 = controller.onMotionEvent(motionEvent) + assertThat(consumeDown2, `is`(true)) + + motionEvent = motionEvent.copy(axisHatX = 0.0f) + val consumeUp2 = controller.onMotionEvent(motionEvent) + assertThat(consumeUp2, `is`(true)) + + motionEvent = motionEvent.copy(axisHatY = 0.0f) + val consumeUp1 = controller.onMotionEvent(motionEvent) + assertThat(consumeUp1, `is`(false)) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Trigger short press key map from DPAD motion event while a volume button is held down`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.SHORT_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), + ) + + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN) + + val consumeDown = inputMotionEvent(axisHatX = -1.0f) + assertThat(consumeDown, `is`(true)) + + val consumeUp = inputMotionEvent(axisHatX = 0.0f) + + assertThat(consumeUp, `is`(true)) + + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Trigger long press key map from DPAD motion event`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.LONG_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), + ) + + val consumeDown = inputMotionEvent(axisHatX = -1.0f) + assertThat(consumeDown, `is`(true)) + + delay(LONG_PRESS_DELAY) + + val consumeUp = inputMotionEvent(axisHatX = 0.0f) + + assertThat(consumeUp, `is`(true)) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Trigger short press key map from DPAD motion event`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.SHORT_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), + ) + + val consumeDown = inputMotionEvent(axisHatX = -1.0f) + assertThat(consumeDown, `is`(true)) + + val consumeUp = inputMotionEvent(axisHatX = 0.0f) + + assertThat(consumeUp, `is`(true)) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + /** + * Issue #491. While a DPAD button is held down many key events are sent with increasing + * repeatCount values. + */ + @Test + fun `Trigger short press key map once when a key event is repeated`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.SHORT_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = trigger, + actionList = listOf(TEST_ACTION), + ), + ) + + val consumeFirstDown = + inputKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_DOWN, repeatCount = 0) + + assertThat(consumeFirstDown, `is`(true)) + + repeat(10) { count -> + val consumeRepeatedDown = + inputKeyEvent( + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.ACTION_DOWN, + repeatCount = count + 1, + ) + + assertThat(consumeRepeatedDown, `is`(true)) + + delay(50) + } + + val consumeUp = + inputKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_UP, repeatCount = 0) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + + assertThat(consumeUp, `is`(true)) + } + + /** + * Issue #491. While a DPAD button is held down many key events are sent with increasing + * repeatCount values. If a long press trigger is used then all these key events must + * be consumed. + */ + @Test + fun `Consume repeated DPAD key events for a long press trigger`() = runTest(testDispatcher) { + val longPressTrigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_DPAD_LEFT, + clickType = ClickType.LONG_PRESS, + detectionSource = KeyEventDetectionSource.INPUT_METHOD, + ), + ) + + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = longPressTrigger, + actionList = listOf(TEST_ACTION), + ), + ) + + repeat(20) { count -> + val consumeDown = + inputKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_DOWN, repeatCount = count) + + assertThat(consumeDown, `is`(true)) + + delay(50) + } + + val consumeUp = + inputKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_UP, repeatCount = 0) + + assertThat(consumeUp, `is`(true)) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + /** * #1271 but with long press trigger instead of double press. */ @Test - fun `Trigger short press key map if constraints allow it and a long press key map to the same button is not allowed`() = - runTest(testDispatcher) { - val shortPressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - ) - val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) + fun `Trigger short press key map if constraints allow it and a long press key map to the same button is not allowed`() = runTest(testDispatcher) { + val shortPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) - val longPressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) - val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) + val longPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) + val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) - keyMapListFlow.value = listOf( - KeyMap( - 0, - trigger = shortPressTrigger, - actionList = listOf(TEST_ACTION), - constraintState = shortPressConstraints, - ), - KeyMap( - 1, - trigger = longPressTrigger, - actionList = listOf(TEST_ACTION_2), - constraintState = doublePressConstraints, - ), - ) + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = shortPressTrigger, + actionList = listOf(TEST_ACTION), + constraintState = shortPressConstraints, + ), + KeyMap( + 1, + trigger = longPressTrigger, + actionList = listOf(TEST_ACTION_2), + constraintState = doublePressConstraints, + ), + ) - // Only the short press trigger is allowed. - mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } + // Only the short press trigger is allowed. + mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } - mockTriggerKeyInput(shortPressTrigger.keys.first()) + mockTriggerKeyInput(shortPressTrigger.keys.first()) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - } + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + } /** * #1271 */ @Test - fun `ignore double press key maps overlapping short press key maps if the constraints aren't satisfied`() = - runTest(testDispatcher) { - val shortPressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - ) - val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) + fun `ignore double press key maps overlapping short press key maps if the constraints aren't satisfied`() = runTest(testDispatcher) { + val shortPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) - val doublePressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), - ) - val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) + val doublePressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), + ) + val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) - keyMapListFlow.value = listOf( - KeyMap( - 0, - trigger = shortPressTrigger, - actionList = listOf(TEST_ACTION), - constraintState = shortPressConstraints, - ), - KeyMap( - 1, - trigger = doublePressTrigger, - actionList = listOf(TEST_ACTION_2), - constraintState = doublePressConstraints, - ), - ) + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = shortPressTrigger, + actionList = listOf(TEST_ACTION), + constraintState = shortPressConstraints, + ), + KeyMap( + 1, + trigger = doublePressTrigger, + actionList = listOf(TEST_ACTION_2), + constraintState = doublePressConstraints, + ), + ) - // Only the short press trigger is allowed. - mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } + // Only the short press trigger is allowed. + mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } - mockTriggerKeyInput(shortPressTrigger.keys.first()) + mockTriggerKeyInput(shortPressTrigger.keys.first()) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - } + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + } @Test - fun `Don't imitate button if 1 long press trigger is successful and another with a longer delay fails`() = - runTest(testDispatcher) { - // GIVEN - - val longerTrigger = - singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.LONG_PRESS, - ), - ) - .copy(longPressDelay = 900) - - val shorterTrigger = - singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.LONG_PRESS, - ), - ) - .copy(longPressDelay = 500) + fun `Don't imitate button if 1 long press trigger is successful and another with a longer delay fails`() = runTest(testDispatcher) { + // GIVEN - keyMapListFlow.value = listOf( - KeyMap(0, trigger = longerTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = shorterTrigger, actionList = listOf(TEST_ACTION_2)), + val longerTrigger = + singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.LONG_PRESS, + ), ) + .copy(longPressDelay = 900) - inOrder(performActionsUseCase, detectKeyMapsUseCase) { - // If only the shorter trigger is detected + val shorterTrigger = + singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.LONG_PRESS, + ), + ) + .copy(longPressDelay = 500) - mockTriggerKeyInput(shorterTrigger.keys[0], 600L) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = longerTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = shorterTrigger, actionList = listOf(TEST_ACTION_2)), + ) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) + inOrder(performActionsUseCase, detectKeyMapsUseCase) { + // If only the shorter trigger is detected + + mockTriggerKeyInput(shorterTrigger.keys[0], 600L) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) - // If both triggers are detected + // If both triggers are detected - mockTriggerKeyInput(shorterTrigger.keys[0], 1000L) + mockTriggerKeyInput(shorterTrigger.keys[0], 1000L) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) - // If no triggers are detected + // If no triggers are detected - mockTriggerKeyInput(shorterTrigger.keys[0], 100L) + mockTriggerKeyInput(shorterTrigger.keys[0], 100L) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) - } + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) } + } /** * #739 */ @Test - fun `Long press trigger shouldn't be triggered if the constraints are changed by the actions`() = - runTest(testDispatcher) { - // GIVEN - val actionData = ActionData.Flashlight.Toggle(CameraLens.BACK) - - val keyMap = KeyMap( - trigger = singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.LONG_PRESS, - ), - ), - actionList = listOf(KeyMapAction(data = actionData)), - constraintState = ConstraintState( - constraints = setOf(Constraint.FlashlightOn(CameraLens.BACK)), + fun `Long press trigger shouldn't be triggered if the constraints are changed by the actions`() = runTest(testDispatcher) { + // GIVEN + val actionData = ActionData.Flashlight.Toggle(CameraLens.BACK) + + val keyMap = KeyMap( + trigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.LONG_PRESS, ), - ) + ), + actionList = listOf(KeyMapAction(data = actionData)), + constraintState = ConstraintState( + constraints = setOf(Constraint.FlashlightOn(CameraLens.BACK)), + ), + ) - keyMapListFlow.value = listOf(keyMap) + keyMapListFlow.value = listOf(keyMap) - var isFlashlightEnabled = false + var isFlashlightEnabled = false - // WHEN THEN - whenever(detectConstraintsUseCase.getSnapshot()).then { - mock { - on { isSatisfied(any()) }.then { isFlashlightEnabled } - } + // WHEN THEN + whenever(detectConstraintsUseCase.getSnapshot()).then { + mock { + on { isSatisfied(any()) }.then { isFlashlightEnabled } } + } - whenever(performActionsUseCase.perform(any(), any(), any())).doAnswer { - isFlashlightEnabled = !isFlashlightEnabled - } + whenever(performActionsUseCase.perform(any(), any(), any())).doAnswer { + isFlashlightEnabled = !isFlashlightEnabled + } - inOrder(performActionsUseCase) { - // flashlight is initially disabled so don't trigger. - mockTriggerKeyInput(keyMap.trigger.keys[0]) - verify(performActionsUseCase, never()).perform(any(), any(), any()) + inOrder(performActionsUseCase) { + // flashlight is initially disabled so don't trigger. + mockTriggerKeyInput(keyMap.trigger.keys[0]) + verify(performActionsUseCase, never()).perform(any(), any(), any()) - isFlashlightEnabled = true - // trigger because flashlight is enabled. Triggering the action will disable the flashlight. - mockTriggerKeyInput(keyMap.trigger.keys[0]) - verify(performActionsUseCase, times(1)).perform(any(), any(), any()) + isFlashlightEnabled = true + // trigger because flashlight is enabled. Triggering the action will disable the flashlight. + mockTriggerKeyInput(keyMap.trigger.keys[0]) + verify(performActionsUseCase, times(1)).perform(any(), any(), any()) - // Don't trigger because the flashlight is now disabled - mockTriggerKeyInput(keyMap.trigger.keys[0]) - verify(performActionsUseCase, never()).perform(any(), any(), any()) - } + // Don't trigger because the flashlight is now disabled + mockTriggerKeyInput(keyMap.trigger.keys[0]) + verify(performActionsUseCase, never()).perform(any(), any(), any()) } + } /** * #693 */ @Test - fun `multiple key maps with the same long press trigger but different long press delays should all work`() = - runTest(testDispatcher) { - // GIVEN - val keyMap1 = KeyMap( - trigger = Trigger( - keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)), - longPressDelay = 500, - ), - actionList = listOf(TEST_ACTION), - ) + fun `multiple key maps with the same long press trigger but different long press delays should all work`() = runTest(testDispatcher) { + // GIVEN + val keyMap1 = KeyMap( + trigger = Trigger( + keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)), + longPressDelay = 500, + ), + actionList = listOf(TEST_ACTION), + ) - val keyMap2 = KeyMap( - trigger = Trigger( - keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)), - longPressDelay = 1000, - ), - actionList = listOf(TEST_ACTION_2), - ) + val keyMap2 = KeyMap( + trigger = Trigger( + keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)), + longPressDelay = 1000, + ), + actionList = listOf(TEST_ACTION_2), + ) - keyMapListFlow.value = listOf(keyMap1, keyMap2) + keyMapListFlow.value = listOf(keyMap1, keyMap2) - // WHEN - inOrder(performActionsUseCase) { - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), - `is`(true), - ) - delay(600) - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), - `is`(true), - ) - advanceUntilIdle() + // WHEN + inOrder(performActionsUseCase) { + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), + `is`(true), + ) + delay(600) + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), + `is`(true), + ) + advanceUntilIdle() - // THEN - verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) + // THEN + verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) - // WHEN - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), - `is`(true), - ) - delay(1100) - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), - `is`(true), - ) - advanceUntilIdle() + // WHEN + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), + `is`(true), + ) + delay(1100) + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), + `is`(true), + ) + advanceUntilIdle() - // THEN - verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) - verify(performActionsUseCase, times(1)).perform(keyMap2.actionList[0].data) - } + // THEN + verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) + verify(performActionsUseCase, times(1)).perform(keyMap2.actionList[0].data) } + } /** * #694 */ @Test - fun `don't consume down and up event if no valid actions to perform`() = - runTest(testDispatcher) { - // GIVEN - val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(2))) + fun `don't consume down and up event if no valid actions to perform`() = runTest(testDispatcher) { + // GIVEN + val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + val actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(2))) - keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = actionList)) + keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = actionList)) - // WHEN - whenever(performActionsUseCase.getError(actionList[0].data)).thenReturn(Error.NoCompatibleImeChosen) + // WHEN + whenever(performActionsUseCase.getError(actionList[0].data)).thenReturn(Error.NoCompatibleImeChosen) - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), - `is`(false), - ) - assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(false)) - advanceUntilIdle() + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), + `is`(false), + ) + assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(false)) + advanceUntilIdle() - // THEN - verify(performActionsUseCase, never()).perform(actionList[0].data) - } + // THEN + verify(performActionsUseCase, never()).perform(actionList[0].data) + } /** * #689 @@ -487,269 +730,261 @@ class KeyMapControllerTest { * #663 */ @Test - fun `action with repeat until limit reached shouldn't stop repeating when trigger is released`() = - runTest(testDispatcher) { - // GIVEN - val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - - val action = KeyMapAction( - data = ActionData.InputKeyEvent(1), - repeat = true, - repeatMode = RepeatMode.LIMIT_REACHED, - repeatLimit = 2, - ) + fun `action with repeat until limit reached shouldn't stop repeating when trigger is released`() = runTest(testDispatcher) { + // GIVEN + val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - keyMapListFlow.value = listOf( - KeyMap(trigger = trigger, actionList = listOf(action)), - ) + val action = KeyMapAction( + data = ActionData.InputKeyEvent(1), + repeat = true, + repeatMode = RepeatMode.LIMIT_REACHED, + repeatLimit = 2, + ) - // WHEN - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), - `is`(true), - ) - assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(true)) - advanceUntilIdle() + keyMapListFlow.value = listOf( + KeyMap(trigger = trigger, actionList = listOf(action)), + ) - // THEN - // 3 times because it performs once and then repeats twice - verify(performActionsUseCase, times(3)).perform(action.data) - } + // WHEN + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), + `is`(true), + ) + assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(true)) + advanceUntilIdle() + + // THEN + // 3 times because it performs once and then repeats twice + verify(performActionsUseCase, times(3)).perform(action.data) + } @Test - fun `key map with multiple actions and delay in between, perform all actions even when trigger is released`() = - runTest(testDispatcher) { - // GIVEN - val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - delayBeforeNextAction = 500, - ) + fun `key map with multiple actions and delay in between, perform all actions even when trigger is released`() = runTest(testDispatcher) { + // GIVEN + val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val action2 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - delayBeforeNextAction = 1000, - ) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + delayBeforeNextAction = 500, + ) - val action3 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 3), - ) + val action2 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + delayBeforeNextAction = 1000, + ) - val keyMaps = listOf( - KeyMap( - trigger = trigger, - actionList = listOf(action1, action2, action3), - ), - ) + val action3 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 3), + ) - keyMapListFlow.value = keyMaps + val keyMaps = listOf( + KeyMap( + trigger = trigger, + actionList = listOf(action1, action2, action3), + ), + ) - // WHEN + keyMapListFlow.value = keyMaps - // ensure consumed - mockTriggerKeyInput(trigger.keys[0]) + // WHEN - // THEN + // ensure consumed + mockTriggerKeyInput(trigger.keys[0]) - advanceUntilIdle() - verify(performActionsUseCase, times(1)).perform(action1.data) - verify(performActionsUseCase, times(1)).perform(action2.data) - verify(performActionsUseCase, times(1)).perform(action3.data) - } + // THEN + + advanceUntilIdle() + verify(performActionsUseCase, times(1)).perform(action1.data) + verify(performActionsUseCase, times(1)).perform(action2.data) + verify(performActionsUseCase, times(1)).perform(action3.data) + } @Test - fun `multiple key maps with same trigger, perform both key maps`() = - runTest(testDispatcher) { - // GIVEN - val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + fun `multiple key maps with same trigger, perform both key maps`() = runTest(testDispatcher) { + // GIVEN + val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val keyMaps = listOf( - KeyMap( - trigger = trigger, - actionList = listOf(TEST_ACTION), - ), - KeyMap( - trigger = trigger, - actionList = listOf(TEST_ACTION_2), - ), - ) + val keyMaps = listOf( + KeyMap( + trigger = trigger, + actionList = listOf(TEST_ACTION), + ), + KeyMap( + trigger = trigger, + actionList = listOf(TEST_ACTION_2), + ), + ) - keyMapListFlow.value = keyMaps + keyMapListFlow.value = keyMaps - // WHEN + // WHEN - // ensure consumed - assertThat( - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), - `is`(true), - ) - delay(50) - assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(true)) + // ensure consumed + assertThat( + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), + `is`(true), + ) + delay(50) + assertThat(inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP), `is`(true)) - // THEN + // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - } + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + } /** * issue #663 */ @Test - fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached and not when the trigger is released`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.LIMIT_REACHED, - repeatLimit = 10, - ) + fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached and not when the trigger is released`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.LIMIT_REACHED, + repeatLimit = 10, + ) - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), - actionList = listOf(action), - ) + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), + actionList = listOf(action), + ) - keyMapListFlow.value = listOf(keyMap) + keyMapListFlow.value = listOf(keyMap) - // WHEN - mockTriggerKeyInput(keyMap.trigger.keys[0]) - advanceUntilIdle() + // WHEN + mockTriggerKeyInput(keyMap.trigger.keys[0]) + advanceUntilIdle() - // THEN - verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) - } + // THEN + verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) + } /** * issue #663 */ @Test - fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, - repeatLimit = 10, - repeatRate = 100, - repeatDelay = 100, - ) - - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), - actionList = listOf(action), - ) + fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + repeatLimit = 10, + repeatRate = 100, + repeatDelay = 100, + ) - keyMapListFlow.value = listOf(keyMap) + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), + actionList = listOf(action), + ) - // WHEN - mockTriggerKeyInput(keyMap.trigger.keys[0]) - testScheduler.apply { - advanceTimeBy(200) - runCurrent() - } - mockTriggerKeyInput(keyMap.trigger.keys[0]) + keyMapListFlow.value = listOf(keyMap) - // THEN - verify(performActionsUseCase, times(4)).perform(action.data) + // WHEN + mockTriggerKeyInput(keyMap.trigger.keys[0]) + testScheduler.apply { + advanceTimeBy(200) + runCurrent() } + mockTriggerKeyInput(keyMap.trigger.keys[0]) + + // THEN + verify(performActionsUseCase, times(4)).perform(action.data) + } /** * issue #663 */ @Test - fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, - repeatLimit = 10, - ) - - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), - actionList = listOf(action), - ) + fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + repeatLimit = 10, + ) - keyMapListFlow.value = listOf(keyMap) + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), + actionList = listOf(action), + ) - // WHEN - mockTriggerKeyInput(keyMap.trigger.keys[0]) - testScheduler.apply { - advanceTimeBy(5000) - runCurrent() - } - mockTriggerKeyInput(keyMap.trigger.keys[0]) + keyMapListFlow.value = listOf(keyMap) - // THEN - // performed an extra 2 times each time the trigger is pressed. This is the expected behaviour even for the option to repeat until pressed again. - verify(performActionsUseCase, times(action.repeatLimit!! + 2)).perform(action.data) + // WHEN + mockTriggerKeyInput(keyMap.trigger.keys[0]) + testScheduler.apply { + advanceTimeBy(5000) + runCurrent() } + mockTriggerKeyInput(keyMap.trigger.keys[0]) + + // THEN + // performed an extra 2 times each time the trigger is pressed. This is the expected behaviour even for the option to repeat until pressed again. + verify(performActionsUseCase, times(action.repeatLimit!! + 2)).perform(action.data) + } /** * issue #663 */ @Test - fun `when triggering action that repeats until released with repeat limit, then stop repeating when the trigger has been released`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.TRIGGER_RELEASED, - repeatLimit = 10, - repeatRate = 100, - repeatDelay = 100, - ) + fun `when triggering action that repeats until released with repeat limit, then stop repeating when the trigger has been released`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.TRIGGER_RELEASED, + repeatLimit = 10, + repeatRate = 100, + repeatDelay = 100, + ) - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), - actionList = listOf(action), - ) + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), + actionList = listOf(action), + ) - keyMapListFlow.value = listOf(keyMap) + keyMapListFlow.value = listOf(keyMap) - // WHEN - mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 300) + // WHEN + mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 300) - // THEN - verify(performActionsUseCase, times(3)).perform(action.data) - } + // THEN + verify(performActionsUseCase, times(3)).perform(action.data) + } /** * issue #663 */ @Test - fun `when triggering action that repeats until released with repeat limit, then stop repeating when the limit has been reached and the action is still being held down`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.TRIGGER_RELEASED, - repeatLimit = 10, - ) + fun `when triggering action that repeats until released with repeat limit, then stop repeating when the limit has been reached and the action is still being held down`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.TRIGGER_RELEASED, + repeatLimit = 10, + ) - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), - actionList = listOf(action), - ) + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)), + actionList = listOf(action), + ) - keyMapListFlow.value = listOf(keyMap) + keyMapListFlow.value = listOf(keyMap) - // WHEN + // WHEN - mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 5000) + mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 5000) - // THEN + // THEN - verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) - } + verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) + } /** * issue #653 @@ -980,127 +1215,124 @@ class KeyMapControllerTest { * issue #664 */ @Test - fun `imitate button presses when a short press trigger with multiple keys fails`() = - runTest(testDispatcher) { - // GIVEN - val trigger = parallelTrigger( - triggerKey(keyCode = 1), - triggerKey(keyCode = 2), - ) + fun `imitate button presses when a short press trigger with multiple keys fails`() = runTest(testDispatcher) { + // GIVEN + val trigger = parallelTrigger( + triggerKey(keyCode = 1), + triggerKey(keyCode = 2), + ) - keyMapListFlow.value = listOf( - KeyMap( - trigger = trigger, - actionList = listOf(TEST_ACTION), - ), - ) + keyMapListFlow.value = listOf( + KeyMap( + trigger = trigger, + actionList = listOf(TEST_ACTION), + ), + ) - inOrder(detectKeyMapsUseCase, performActionsUseCase) { - // WHEN - inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN) - inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) - - // THEN - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress(keyCode = 1) - verifyNoMoreInteractions() - - // verify nothing happens and no key events are consumed when the 2nd key in the trigger is pressed - // WHEN - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(false)) - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(false)) - - // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) - verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) - - // verify the action is performed and no keys are imitated when triggering the key map - // WHEN - assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN), `is`(true)) - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(true)) - assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP), `is`(true)) - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(true)) - - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - - // change the order of the keys being released - // WHEN - assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN), `is`(true)) - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(true)) - assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(true)) - assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP), `is`(true)) - - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - } + inOrder(detectKeyMapsUseCase, performActionsUseCase) { + // WHEN + inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN) + inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) + + // THEN + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress(keyCode = 1) + verifyNoMoreInteractions() + + // verify nothing happens and no key events are consumed when the 2nd key in the trigger is pressed + // WHEN + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(false)) + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(false)) + + // THEN + verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) + verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) + + // verify the action is performed and no keys are imitated when triggering the key map + // WHEN + assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN), `is`(true)) + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(true)) + assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP), `is`(true)) + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(true)) + + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + + // change the order of the keys being released + // WHEN + assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN), `is`(true)) + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN), `is`(true)) + assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(true)) + assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP), `is`(true)) + + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } + } /** * issue #664 */ @Test - fun `don't imitate button press when a short press trigger is triggered`() = - runTest(testDispatcher) { - // GIVEN - val trigger = parallelTrigger( - triggerKey(keyCode = 1), - triggerKey(keyCode = 2), - ) + fun `don't imitate button press when a short press trigger is triggered`() = runTest(testDispatcher) { + // GIVEN + val trigger = parallelTrigger( + triggerKey(keyCode = 1), + triggerKey(keyCode = 2), + ) - keyMapListFlow.value = listOf( - KeyMap( - trigger = trigger, - actionList = listOf(TEST_ACTION), - ), - ) + keyMapListFlow.value = listOf( + KeyMap( + trigger = trigger, + actionList = listOf(TEST_ACTION), + ), + ) - // WHEN - inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN) - inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN) - inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) - inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP) + // WHEN + inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_DOWN) + inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_DOWN) + inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) + inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP) - // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) - } + // THEN + verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) + verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + } /** * issue #662 */ @Test - fun `don't repeat when trigger is released for an action that has these options when the trigger is held down`() = - runTest(testDispatcher) { - // GIVEN - val action = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - delayBeforeNextAction = 10, - repeatDelay = 10, - repeatRate = 190, - ) - - val keyMap = KeyMap( - trigger = singleKeyTrigger(triggerKey(keyCode = 2)), - actionList = listOf(action), - ) + fun `don't repeat when trigger is released for an action that has these options when the trigger is held down`() = runTest(testDispatcher) { + // GIVEN + val action = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + delayBeforeNextAction = 10, + repeatDelay = 10, + repeatRate = 190, + ) - keyMapListFlow.value = listOf(keyMap) - // WHEN + val keyMap = KeyMap( + trigger = singleKeyTrigger(triggerKey(keyCode = 2)), + actionList = listOf(action), + ) - mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1) + keyMapListFlow.value = listOf(keyMap) + // WHEN - // see if the action repeats - testScope.testScheduler.apply { - advanceTimeBy(500) - runCurrent() - } - controller.reset() + mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1) - // THEN - verify(performActionsUseCase, times(1)).perform(action.data) + // see if the action repeats + testScope.testScheduler.apply { + advanceTimeBy(500) + runCurrent() } + controller.reset() + + // THEN + verify(performActionsUseCase, times(1)).perform(action.data) + } /** * See #626 in issue tracker @@ -1114,43 +1346,42 @@ class KeyMapControllerTest { * On long press: don't trigger #1 and start repeating #2 */ @Test - fun `don't initialise repeating if repeat when trigger is released after failed long press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - ) + fun `don't initialise repeating if repeat when trigger is released after failed long press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + ) - val trigger2 = - parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) - val action2 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 3), - repeat = true, - ) + val trigger2 = + parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) + val action2 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 3), + repeat = true, + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + ) - performActionsUseCase.inOrder { - // when short press - mockParallelTrigger(trigger1) - delay(2000) // let it try to repeat + performActionsUseCase.inOrder { + // when short press + mockParallelTrigger(trigger1) + delay(2000) // let it try to repeat - // then - verify(performActionsUseCase, times(1)).perform(action1.data) - verifyNoMoreInteractions() + // then + verify(performActionsUseCase, times(1)).perform(action1.data) + verifyNoMoreInteractions() - // when long press - mockParallelTrigger(trigger2, delay = 2000) // let it repeat + // when long press + mockParallelTrigger(trigger2, delay = 2000) // let it repeat - // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) - } + // then + verify(performActionsUseCase, atLeast(2)).perform(action2.data) } + } /** * See #626 in issue tracker @@ -1164,40 +1395,39 @@ class KeyMapControllerTest { * On double press: don't trigger #1 and trigger #2 */ @Test - fun `don't initialise repeating if repeat when trigger is released after failed failed double press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - ) + fun `don't initialise repeating if repeat when trigger is released after failed failed double press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + ) - val trigger2 = - sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) - val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3)) + val trigger2 = + sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) + val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + ) - performActionsUseCase.inOrder { - // when short press - mockParallelTrigger(trigger1) - delay(2000) // let it repeat + performActionsUseCase.inOrder { + // when short press + mockParallelTrigger(trigger1) + delay(2000) // let it repeat - // then - verify(performActionsUseCase, times(1)).perform(action1.data) - verifyNoMoreInteractions() + // then + verify(performActionsUseCase, times(1)).perform(action1.data) + verifyNoMoreInteractions() - // when double press - mockTriggerKeyInput(trigger2.keys[0]) + // when double press + mockTriggerKeyInput(trigger2.keys[0]) - // then - verify(performActionsUseCase, times(1)).perform(action2.data) - } + // then + verify(performActionsUseCase, times(1)).perform(action2.data) } + } /** * See #626 in issue tracker @@ -1213,54 +1443,53 @@ class KeyMapControllerTest { * On double press: don't trigger #1, don't trigger #2, trigger #3 */ @Test - fun `don't initialise repeating if repeat when trigger is released after failed double press and failed long press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - ) + fun `don't initialise repeating if repeat when trigger is released after failed double press and failed long press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + ) - val trigger2 = - parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) - val action2 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 3), - repeat = true, - ) + val trigger2 = + parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) + val action2 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 3), + repeat = true, + ) - val trigger3 = - sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) - val action3 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 4)) + val trigger3 = + sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) + val action3 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - KeyMap(2, trigger = trigger3, actionList = listOf(action3)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + KeyMap(2, trigger = trigger3, actionList = listOf(action3)), + ) - performActionsUseCase.inOrder { - // when short press - mockParallelTrigger(trigger1) - advanceUntilIdle() + performActionsUseCase.inOrder { + // when short press + mockParallelTrigger(trigger1) + advanceUntilIdle() - // then - verify(performActionsUseCase, times(1)).perform(action1.data) - verifyNoMoreInteractions() + // then + verify(performActionsUseCase, times(1)).perform(action1.data) + verifyNoMoreInteractions() - // when long press - mockParallelTrigger(trigger2, delay = 2000) // let it repeat + // when long press + mockParallelTrigger(trigger2, delay = 2000) // let it repeat - // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + // then + verify(performActionsUseCase, atLeast(2)).perform(action2.data) - // when double press - mockTriggerKeyInput(trigger3.keys[0]) + // when double press + mockTriggerKeyInput(trigger3.keys[0]) - // then - verify(performActionsUseCase, times(1)).perform(action3.data) - } + // then + verify(performActionsUseCase, times(1)).perform(action3.data) } + } /** * See #626 in issue tracker @@ -1274,43 +1503,42 @@ class KeyMapControllerTest { * On long press: don't trigger #1 and start repeating #2 */ @Test - fun `initialise repeating if repeat until pressed again on failed long press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, - ) + fun `initialise repeating if repeat until pressed again on failed long press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + ) - val trigger2 = - parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) - val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3), repeat = true) + val trigger2 = + parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) + val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3), repeat = true) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + ) - performActionsUseCase.inOrder { - // when short press - mockParallelTrigger(trigger1) - delay(2000) // let it repeat + performActionsUseCase.inOrder { + // when short press + mockParallelTrigger(trigger1) + delay(2000) // let it repeat - // then - mockParallelTrigger(trigger1) // press the key again to stop it repeating + // then + mockParallelTrigger(trigger1) // press the key again to stop it repeating - verify(performActionsUseCase, atLeast(2)).perform(action1.data) - verifyNoMoreInteractions() + verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verifyNoMoreInteractions() - // when long press - mockParallelTrigger(trigger2, delay = 2000) // let it repeat + // when long press + mockParallelTrigger(trigger2, delay = 2000) // let it repeat - // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) - } + // then + verify(performActionsUseCase, atLeast(2)).perform(action2.data) } + } /** * See #626 in issue tracker @@ -1324,46 +1552,45 @@ class KeyMapControllerTest { * On double press: don't trigger #1 and trigger #2 */ @Test - fun `initialise repeating if repeat until pressed again on failed double press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, - ) + fun `initialise repeating if repeat until pressed again on failed double press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + ) - val trigger2 = - sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) - val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3)) + val trigger2 = + sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) + val action2 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + ) - performActionsUseCase.inOrder { - // when short press - mockParallelTrigger(trigger1) - delay(2000) // let it repeat + performActionsUseCase.inOrder { + // when short press + mockParallelTrigger(trigger1) + delay(2000) // let it repeat - // then + // then - mockParallelTrigger(trigger1) // press the key again to stop it repeating - advanceUntilIdle() + mockParallelTrigger(trigger1) // press the key again to stop it repeating + advanceUntilIdle() - verify(performActionsUseCase, atLeast(2)).perform(action1.data) - verifyNoMoreInteractions() + verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verifyNoMoreInteractions() - // when double press - mockTriggerKeyInput(trigger2.keys[0]) - advanceUntilIdle() + // when double press + mockTriggerKeyInput(trigger2.keys[0]) + advanceUntilIdle() - // then - verify(performActionsUseCase, times(1)).perform(action2.data) - } + // then + verify(performActionsUseCase, times(1)).perform(action2.data) } + } /** * See #626 in issue tracker @@ -1379,87 +1606,85 @@ class KeyMapControllerTest { * On double press: don't trigger #1, don't trigger #2, trigger #3 */ @Test - fun `initialise repeating if repeat until pressed again on failed double press and failed long press`() = - runTest(testDispatcher) { - // given - val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) - val action1 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 2), - repeat = true, - repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, - ) + fun `initialise repeating if repeat until pressed again on failed double press and failed long press`() = runTest(testDispatcher) { + // given + val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) + val action1 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 2), + repeat = true, + repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN, + ) - val trigger2 = - parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) - val action2 = KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 3), - repeat = true, - ) + val trigger2 = + parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) + val action2 = KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 3), + repeat = true, + ) - val trigger3 = - sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) - val action3 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 4)) + val trigger3 = + sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) + val action3 = KeyMapAction(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = trigger1, actionList = listOf(action1)), - KeyMap(1, trigger = trigger2, actionList = listOf(action2)), - KeyMap(2, trigger = trigger3, actionList = listOf(action3)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = trigger1, actionList = listOf(action1)), + KeyMap(1, trigger = trigger2, actionList = listOf(action2)), + KeyMap(2, trigger = trigger3, actionList = listOf(action3)), + ) - // when short press - mockParallelTrigger(trigger1) + // when short press + mockParallelTrigger(trigger1) - delay(2000) // let it repeat + delay(2000) // let it repeat - performActionsUseCase.inOrder { - // then - mockParallelTrigger(trigger1) // press the key again to stop it repeating - advanceUntilIdle() + performActionsUseCase.inOrder { + // then + mockParallelTrigger(trigger1) // press the key again to stop it repeating + advanceUntilIdle() - verify(performActionsUseCase, atLeast(2)).perform(action1.data) - verifyNoMoreInteractions() + verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verifyNoMoreInteractions() - // when long press - mockParallelTrigger(trigger2, delay = 2000) // let it repeat + // when long press + mockParallelTrigger(trigger2, delay = 2000) // let it repeat - // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + // then + verify(performActionsUseCase, atLeast(2)).perform(action2.data) - delay(1000) // have a delay after a long press of the key is released so a double press isn't detected + delay(1000) // have a delay after a long press of the key is released so a double press isn't detected - // when double press - mockTriggerKeyInput(trigger3.keys[0]) + // when double press + mockTriggerKeyInput(trigger3.keys[0]) - // then - verify(performActionsUseCase, times(1)).perform(action3.data) - verifyNoMoreInteractions() - } + // then + verify(performActionsUseCase, times(1)).perform(action3.data) + verifyNoMoreInteractions() } + } /** * this helped fix issue #608 */ @Test - fun `short press key and double press same key sequence trigger, double press key, don't perform action`() = - runTest(testDispatcher) { - val trigger = sequenceTrigger( - triggerKey(KeyEvent.KEYCODE_A), - triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS), - ) + fun `short press key and double press same key sequence trigger, double press key, don't perform action`() = runTest(testDispatcher) { + val trigger = sequenceTrigger( + triggerKey(KeyEvent.KEYCODE_A), + triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS), + ) - keyMapListFlow.value = listOf( - KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), - ) + keyMapListFlow.value = listOf( + KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), + ) - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS)) + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS)) - verify(performActionsUseCase, never()).perform(any(), any(), any()) + verify(performActionsUseCase, never()).perform(any(), any(), any()) - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A)) - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS)) + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A)) + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - } + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } /** * issue #563 @@ -1625,381 +1850,371 @@ class KeyMapControllerTest { } @Test - fun `parallel trigger with 2 keys and the 2nd key is another trigger, press 2 key trigger, only the action for 2 key trigger should be performed `() = - runTest(testDispatcher) { - // GIVEN - val twoKeyTrigger = parallelTrigger( - triggerKey(KeyEvent.KEYCODE_SHIFT_LEFT), - triggerKey(KeyEvent.KEYCODE_A), - ) + fun `parallel trigger with 2 keys and the 2nd key is another trigger, press 2 key trigger, only the action for 2 key trigger should be performed `() = runTest(testDispatcher) { + // GIVEN + val twoKeyTrigger = parallelTrigger( + triggerKey(KeyEvent.KEYCODE_SHIFT_LEFT), + triggerKey(KeyEvent.KEYCODE_A), + ) - val oneKeyTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_A), - ) + val oneKeyTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_A), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = oneKeyTrigger, actionList = listOf(TEST_ACTION_2)), - KeyMap(1, trigger = twoKeyTrigger, actionList = listOf(TEST_ACTION)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = oneKeyTrigger, actionList = listOf(TEST_ACTION_2)), + KeyMap(1, trigger = twoKeyTrigger, actionList = listOf(TEST_ACTION)), + ) - inOrder(performActionsUseCase) { - // test 1. test triggering 2 key trigger - // WHEN - inputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_DOWN) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN) + inOrder(performActionsUseCase) { + // test 1. test triggering 2 key trigger + // WHEN + inputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_DOWN) + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN) - inputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_UP) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP) - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + inputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_UP) + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP) + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - // test 2. test triggering 1 key trigger - // WHEN - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN) + // test 2. test triggering 1 key trigger + // WHEN + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_DOWN) - inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP) - advanceUntilIdle() + inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP) + advanceUntilIdle() - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - } + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) } + } @Test - fun `trigger for a specific device and trigger for any device, input trigger from a different device, only detect trigger for any device`() = - runTest(testDispatcher) { - // GIVEN - val triggerKeyboard = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE), - ) + fun `trigger for a specific device and trigger for any device, input trigger from a different device, only detect trigger for any device`() = runTest(testDispatcher) { + // GIVEN + val triggerKeyboard = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE), + ) - val triggerAnyDevice = singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_A, - device = TriggerKeyDevice.Any, - ), - ) + val triggerAnyDevice = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_A, + device = TriggerKeyDevice.Any, + ), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = triggerKeyboard, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = triggerAnyDevice, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = triggerKeyboard, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = triggerAnyDevice, actionList = listOf(TEST_ACTION_2)), + ) - // WHEN - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE)) + // WHEN + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE)) - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - } + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + } @Test - fun `trigger for a specific device, input trigger from a different device, do not detect trigger`() = - runTest(testDispatcher) { - // GIVEN - val triggerHeadphone = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE), - ) + fun `trigger for a specific device, input trigger from a different device, do not detect trigger`() = runTest(testDispatcher) { + // GIVEN + val triggerHeadphone = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = triggerHeadphone, actionList = listOf(TEST_ACTION)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = triggerHeadphone, actionList = listOf(TEST_ACTION)), + ) - // WHEN - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE)) + // WHEN + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE)) - // THEN - verify(performActionsUseCase, never()).perform(any(), any(), any()) - } + // THEN + verify(performActionsUseCase, never()).perform(any(), any(), any()) + } @Test - fun `long press trigger and action with Hold Down until pressed again flag, input valid long press, hold down until long pressed again`() = - runTest(testDispatcher) { - // GIVEN - val trigger = - singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.LONG_PRESS)) - - val action = KeyMapAction( - data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_B), - holdDown = true, - stopHoldDownWhenTriggerPressedAgain = true, - ) + fun `long press trigger and action with Hold Down until pressed again flag, input valid long press, hold down until long pressed again`() = runTest(testDispatcher) { + // GIVEN + val trigger = + singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.LONG_PRESS)) - val keymap = KeyMap( - 0, - trigger = trigger, - actionList = listOf(action), - ) + val action = KeyMapAction( + data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_B), + holdDown = true, + stopHoldDownWhenTriggerPressedAgain = true, + ) - keyMapListFlow.value = listOf(keymap) + val keymap = KeyMap( + 0, + trigger = trigger, + actionList = listOf(action), + ) - // WHEN - mockTriggerKeyInput(trigger.keys[0]) + keyMapListFlow.value = listOf(keymap) - // THEN - verify(performActionsUseCase, times(1)).perform( - action.data, - InputEventType.DOWN, - ) + // WHEN + mockTriggerKeyInput(trigger.keys[0]) - // WHEN - mockTriggerKeyInput(trigger.keys[0]) + // THEN + verify(performActionsUseCase, times(1)).perform( + action.data, + InputEventType.DOWN, + ) - verify(performActionsUseCase, times(1)).perform( - action.data, - InputEventType.UP, - ) - } + // WHEN + mockTriggerKeyInput(trigger.keys[0]) + + verify(performActionsUseCase, times(1)).perform( + action.data, + InputEventType.UP, + ) + } /** * #478 */ @Test - fun `trigger with modifier key and modifier keycode action, don't include metastate from the trigger modifier key when an unmapped modifier key is pressed`() = - runTest(testDispatcher) { - val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT)) + fun `trigger with modifier key and modifier keycode action, don't include metastate from the trigger modifier key when an unmapped modifier key is pressed`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT)) - keyMapListFlow.value = listOf( - KeyMap( - 0, - trigger = trigger, - actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ALT_LEFT))), - ), - ) + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = trigger, + actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ALT_LEFT))), + ), + ) - // imitate how modifier keys are sent on Android by also changing the metastate of the keyevent + // imitate how modifier keys are sent on Android by also changing the metastate of the keyevent - inputKeyEvent( - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.ACTION_DOWN, - metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON, - ) - inputKeyEvent( - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.ACTION_DOWN, - metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, - ) - inputKeyEvent( - KeyEvent.KEYCODE_C, - KeyEvent.ACTION_DOWN, - metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, - ) + inputKeyEvent( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.ACTION_DOWN, + metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON, + ) + inputKeyEvent( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.ACTION_DOWN, + metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, + ) + inputKeyEvent( + KeyEvent.KEYCODE_C, + KeyEvent.ACTION_DOWN, + metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, + ) - inputKeyEvent( - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.ACTION_UP, - metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, - ) - inputKeyEvent( - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.ACTION_UP, - metaState = KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, - ) + inputKeyEvent( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.ACTION_UP, + metaState = KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, + ) + inputKeyEvent( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.ACTION_UP, + metaState = KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, + ) - inputKeyEvent(KeyEvent.KEYCODE_C, KeyEvent.ACTION_UP) + inputKeyEvent(KeyEvent.KEYCODE_C, KeyEvent.ACTION_UP) - inOrder(detectKeyMapsUseCase) { - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( - any(), - metaState = eq(KeyEvent.META_ALT_LEFT_ON + KeyEvent.META_ALT_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON), - any(), - any(), - any(), - ) + inOrder(detectKeyMapsUseCase) { + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + any(), + metaState = eq(KeyEvent.META_ALT_LEFT_ON + KeyEvent.META_ALT_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON), + any(), + any(), + any(), + ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( - any(), - metaState = eq(0), - any(), - any(), - any(), - ) - } + verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + any(), + metaState = eq(0), + any(), + any(), + any(), + ) } + } @Test - fun `2x key sequence trigger and 3x key sequence trigger with the last 2 keys being the same, trigger 3x key trigger, ignore the first 2x key trigger`() = - runTest(testDispatcher) { - val firstTrigger = sequenceTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, - ), - triggerKey(KeyEvent.KEYCODE_VOLUME_UP), - ) + fun `2x key sequence trigger and 3x key sequence trigger with the last 2 keys being the same, trigger 3x key trigger, ignore the first 2x key trigger`() = runTest(testDispatcher) { + val firstTrigger = sequenceTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + device = TriggerKeyDevice.Any, + ), + triggerKey(KeyEvent.KEYCODE_VOLUME_UP), + ) - val secondTrigger = sequenceTrigger( - triggerKey(KeyEvent.KEYCODE_HOME), - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, - ), - triggerKey(KeyEvent.KEYCODE_VOLUME_UP), - ) + val secondTrigger = sequenceTrigger( + triggerKey(KeyEvent.KEYCODE_HOME), + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + device = TriggerKeyDevice.Any, + ), + triggerKey(KeyEvent.KEYCODE_VOLUME_UP), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = firstTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = secondTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = firstTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = secondTrigger, actionList = listOf(TEST_ACTION_2)), + ) - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_HOME)) - mockTriggerKeyInput( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, - ), - ) - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_UP)) + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_HOME)) + mockTriggerKeyInput( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + device = TriggerKeyDevice.Any, + ), + ) + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_UP)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - } + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + } @Test - fun `2x key long press parallel trigger with HOME or RECENTS keycode, trigger successfully, don't do normal action`() = - runTest(testDispatcher) { + fun `2x key long press parallel trigger with HOME or RECENTS keycode, trigger successfully, don't do normal action`() = runTest(testDispatcher) { /* HOME */ - val homeTrigger = parallelTrigger( - triggerKey(KeyEvent.KEYCODE_HOME, clickType = ClickType.LONG_PRESS), - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) + val homeTrigger = parallelTrigger( + triggerKey(KeyEvent.KEYCODE_HOME, clickType = ClickType.LONG_PRESS), + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = homeTrigger, actionList = listOf(TEST_ACTION)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = homeTrigger, actionList = listOf(TEST_ACTION)), + ) - val consumedHomeDown = inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + val consumedHomeDown = inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, null) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) - advanceUntilIdle() + advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) - assertThat(consumedHomeDown, `is`(true)) + assertThat(consumedHomeDown, `is`(true)) /* RECENTS */ - val recentsTrigger = parallelTrigger( - triggerKey(KeyEvent.KEYCODE_APP_SWITCH, clickType = ClickType.LONG_PRESS), - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) + val recentsTrigger = parallelTrigger( + triggerKey(KeyEvent.KEYCODE_APP_SWITCH, clickType = ClickType.LONG_PRESS), + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = recentsTrigger, actionList = listOf(TEST_ACTION)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = recentsTrigger, actionList = listOf(TEST_ACTION)), + ) - val consumedRecentsDown = - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + val consumedRecentsDown = + inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, null) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) - advanceUntilIdle() + advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) - assertThat(consumedRecentsDown, `is`(true)) - } + assertThat(consumedRecentsDown, `is`(true)) + } @Test - fun shortPressTriggerDoublePressTrigger_holdDown_onlyDetectDoublePressTrigger() = - runTest(testDispatcher) { - // given - val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val doublePressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), - ) + fun shortPressTriggerDoublePressTrigger_holdDown_onlyDetectDoublePressTrigger() = runTest(testDispatcher) { + // given + val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + val doublePressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), + ) - // when - mockTriggerKeyInput( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.DOUBLE_PRESS, - ), - ) + // when + mockTriggerKeyInput( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.DOUBLE_PRESS, + ), + ) - // then - // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + // then + // the first action performed shouldn't be the short press action + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) /* rerun the test to see if the short press trigger action is performed correctly. */ - // when - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - advanceUntilIdle() + // when + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + advanceUntilIdle() - // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - } + // then + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } @Test - fun shortPressTriggerLongPressTrigger_holdDown_onlyDetectLongPressTrigger() = - runTest(testDispatcher) { - // GIVEN - val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val longPressTrigger = singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.LONG_PRESS, - ), - ) + fun shortPressTriggerLongPressTrigger_holdDown_onlyDetectLongPressTrigger() = runTest(testDispatcher) { + // GIVEN + val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + val longPressTrigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.LONG_PRESS, + ), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), + ) - mockTriggerKeyInput( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) - advanceUntilIdle() + mockTriggerKeyInput( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) + advanceUntilIdle() - // THEN - // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + // THEN + // the first action performed shouldn't be the short press action + verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - // WHEN - // rerun the test to see if the short press trigger action is performed correctly. - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + // WHEN + // rerun the test to see if the short press trigger action is performed correctly. + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - // THEN - // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - } + // THEN + // the first action performed shouldn't be the short press action + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } @Test @Parameters(method = "params_repeatAction") - fun parallelTrigger_holdDown_repeatAction10Times(description: String, trigger: Trigger) = - runTest(testDispatcher) { - // given - val action = KeyMapAction( - data = ActionData.Volume.Up(showVolumeUi = false), - repeat = true, - ) - - keyMapListFlow.value = listOf(KeyMap(0, trigger = trigger, actionList = listOf(action))) + fun parallelTrigger_holdDown_repeatAction10Times(description: String, trigger: Trigger) = runTest(testDispatcher) { + // given + val action = KeyMapAction( + data = ActionData.Volume.Up(showVolumeUi = false), + repeat = true, + ) - when (trigger.mode) { - is TriggerMode.Parallel -> mockParallelTrigger(trigger, delay = 2000L) - TriggerMode.Undefined -> mockTriggerKeyInput(trigger.keys[0], delay = 2000L) - TriggerMode.Sequence -> {} - } + keyMapListFlow.value = listOf(KeyMap(0, trigger = trigger, actionList = listOf(action))) - verify(performActionsUseCase, atLeast(10)).perform(action.data) + when (trigger.mode) { + is TriggerMode.Parallel -> mockParallelTrigger(trigger, delay = 2000L) + TriggerMode.Undefined -> mockTriggerKeyInput(trigger.keys[0], delay = 2000L) + TriggerMode.Sequence -> {} } + verify(performActionsUseCase, atLeast(10)).perform(action.data) + } + fun params_repeatAction() = listOf( arrayOf( "long press multiple keys", @@ -2034,32 +2249,31 @@ class KeyMapControllerTest { fun dualParallelTrigger_input2ndKey_doNotConsumeUp( description: String, trigger: Trigger, - ) = - runTest(testDispatcher) { - // given - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) - - // when - (trigger.keys[1] as KeyCodeTriggerKey).let { - inputKeyEvent( - it.keyCode, - KeyEvent.ACTION_DOWN, - triggerKeyDeviceToInputDevice(it.device), - ) - } + ) = runTest(testDispatcher) { + // given + keyMapListFlow.value = + listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) - (trigger.keys[1] as KeyCodeTriggerKey).let { - val consumed = inputKeyEvent( - it.keyCode, - KeyEvent.ACTION_UP, - triggerKeyDeviceToInputDevice(it.device), - ) + // when + (trigger.keys[1] as KeyCodeTriggerKey).let { + inputKeyEvent( + it.keyCode, + KeyEvent.ACTION_DOWN, + triggerKeyDeviceToInputDevice(it.device), + ) + } - // then - assertThat(consumed, `is`(false)) - } + (trigger.keys[1] as KeyCodeTriggerKey).let { + val consumed = inputKeyEvent( + it.keyCode, + KeyEvent.ACTION_UP, + triggerKeyDeviceToInputDevice(it.device), + ) + + // then + assertThat(consumed, `is`(false)) } + } fun params_dualParallelTrigger_input2ndKey_doNotConsumeUp() = listOf( arrayOf( @@ -2160,126 +2374,122 @@ class KeyMapControllerTest { } @Test - fun keymappedToLongPressAndDoublePress_invalidLongPress_imitateOnce() = - runTest(testDispatcher) { - // given - val longPressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) + fun keymappedToLongPressAndDoublePress_invalidLongPress_imitateOnce() = runTest(testDispatcher) { + // given + val longPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) - val doublePressTrigger = singleKeyTrigger( - triggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - clickType = ClickType.DOUBLE_PRESS, - ), - ) + val doublePressTrigger = singleKeyTrigger( + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + clickType = ClickType.DOUBLE_PRESS, + ), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = longPressTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = longPressTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), + ) - // when - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - advanceUntilIdle() + // when + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + advanceUntilIdle() - // then - verify( - detectKeyMapsUseCase, - times(1), - ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - } + // then + verify( + detectKeyMapsUseCase, + times(1), + ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + } @Test - fun keymappedToSingleShortPressAndLongPress_validShortPress_onlyPerformActiondoNotImitateKey() = - runTest(testDispatcher) { - // given - val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + fun keymappedToSingleShortPressAndLongPress_validShortPress_onlyPerformActiondoNotImitateKey() = runTest(testDispatcher) { + // given + val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val longPressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), - ) + val longPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), + ) - // when - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + // when + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) - } + // then + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) + } @Test - fun keymappedToShortPressAndDoublePress_validShortPress_onlyPerformActionDoNotImitateKey() = - runTest(testDispatcher) { - // given - val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - - val doublePressTrigger = singleKeyTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), - ) + fun keymappedToShortPressAndDoublePress_validShortPress_onlyPerformActionDoNotImitateKey() = runTest(testDispatcher) { + // given + val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), - ) + val doublePressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), + ) - // when - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - advanceUntilIdle() + keyMapListFlow.value = listOf( + KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), + ) - // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + // when + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + advanceUntilIdle() - // wait for the double press to try and imitate the key. - verify(detectKeyMapsUseCase, never()).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) - } + // then + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + + // wait for the double press to try and imitate the key. + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) + } @Test - fun singleKeyTriggerAndShortPressParallelTriggerWithSameInitialKey_validSingleKeyTriggerInput_onlyPerformActiondoNotImitateKey() = - runTest(testDispatcher) { - // given - val singleKeyTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val parallelTrigger = parallelTrigger( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - triggerKey(KeyEvent.KEYCODE_VOLUME_UP), - ) + fun singleKeyTriggerAndShortPressParallelTriggerWithSameInitialKey_validSingleKeyTriggerInput_onlyPerformActiondoNotImitateKey() = runTest(testDispatcher) { + // given + val singleKeyTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + val parallelTrigger = parallelTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + triggerKey(KeyEvent.KEYCODE_VOLUME_UP), + ) - keyMapListFlow.value = listOf( - KeyMap(0, trigger = singleKeyTrigger, actionList = listOf(TEST_ACTION)), - KeyMap(1, trigger = parallelTrigger, actionList = listOf(TEST_ACTION_2)), - ) + keyMapListFlow.value = listOf( + KeyMap(0, trigger = singleKeyTrigger, actionList = listOf(TEST_ACTION)), + KeyMap(1, trigger = parallelTrigger, actionList = listOf(TEST_ACTION_2)), + ) - // when - mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) + // when + mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - // then - verify(detectKeyMapsUseCase, never()).imitateButtonPress( - any(), - any(), - any(), - any(), - any(), - ) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - } + // then + verify(detectKeyMapsUseCase, never()).imitateButtonPress( + any(), + any(), + any(), + any(), + any(), + ) + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } @Test fun longPressSequenceTrigger_invalidLongPress_keyImitated() = runTest(testDispatcher) { @@ -2305,28 +2515,27 @@ class KeyMapControllerTest { @Test @Parameters(method = "params_multipleActionsPerformed") - fun validInput_multipleActionsPerformed(description: String, trigger: Trigger) = - runTest(testDispatcher) { - val actionList = listOf(TEST_ACTION, TEST_ACTION_2) - // GIVEN - keyMapListFlow.value = listOf( - KeyMap(trigger = trigger, actionList = actionList), - ) + fun validInput_multipleActionsPerformed(description: String, trigger: Trigger) = runTest(testDispatcher) { + val actionList = listOf(TEST_ACTION, TEST_ACTION_2) + // GIVEN + keyMapListFlow.value = listOf( + KeyMap(trigger = trigger, actionList = actionList), + ) - // WHEN - if (trigger.mode is TriggerMode.Parallel) { - mockParallelTrigger(trigger) - } else { - trigger.keys.forEach { - mockTriggerKeyInput(it) - } + // WHEN + if (trigger.mode is TriggerMode.Parallel) { + mockParallelTrigger(trigger) + } else { + trigger.keys.forEach { + mockTriggerKeyInput(it) } + } - // THEN - actionList.forEach { action -> - verify(performActionsUseCase, times(1)).perform(action.data) - } + // THEN + actionList.forEach { action -> + verify(performActionsUseCase, times(1)).perform(action.data) } + } fun params_multipleActionsPerformed() = listOf( arrayOf( @@ -2359,82 +2568,79 @@ class KeyMapControllerTest { @Test @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") - fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) = - runTest(testDispatcher) { - // GIVEN - keyMapListFlow.value = listOf(keyMap) + fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { + // GIVEN + keyMapListFlow.value = listOf(keyMap) - // WHEN - var consumedCount = 0 - - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { - val consumed = - inputKeyEvent( - 999, - KeyEvent.ACTION_DOWN, - triggerKeyDeviceToInputDevice(key.device), - ) - - if (consumed) { - consumedCount++ - } - } + // WHEN + var consumedCount = 0 - // THEN - assertThat(consumedCount, `is`(0)) + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + val consumed = + inputKeyEvent( + 999, + KeyEvent.ACTION_DOWN, + triggerKeyDeviceToInputDevice(key.device), + ) + + if (consumed) { + consumedCount++ + } } + // THEN + assertThat(consumedCount, `is`(0)) + } + @Test @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") - fun validInput_downConsumed(description: String, keyMap: KeyMap) = - runTest(testDispatcher) { - // GIVEN - keyMapListFlow.value = listOf(keyMap) - - var consumedCount = 0 - - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { - val consumed = - inputKeyEvent( - key.keyCode, - KeyEvent.ACTION_DOWN, - triggerKeyDeviceToInputDevice(key.device), - ) - - if (consumed) { - consumedCount++ - } - } + fun validInput_downConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { + // GIVEN + keyMapListFlow.value = listOf(keyMap) + + var consumedCount = 0 + + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + val consumed = + inputKeyEvent( + key.keyCode, + KeyEvent.ACTION_DOWN, + triggerKeyDeviceToInputDevice(key.device), + ) - assertThat(consumedCount, `is`(keyMap.trigger.keys.size)) + if (consumed) { + consumedCount++ + } } + assertThat(consumedCount, `is`(keyMap.trigger.keys.size)) + } + @Test @Parameters(method = "params_allTriggerKeyCombinationsdoNotConsume") @TestCaseName("{0}") - fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) = - runTest(testDispatcher) { - keyMapListFlow.value = listOf(keyMap) - - var consumedCount = 0 - - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { - val consumed = - inputKeyEvent( - key.keyCode, - KeyEvent.ACTION_DOWN, - triggerKeyDeviceToInputDevice(key.device), - ) - - if (consumed) { - consumedCount++ - } - } + fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) = runTest(testDispatcher) { + keyMapListFlow.value = listOf(keyMap) + + var consumedCount = 0 + + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + val consumed = + inputKeyEvent( + key.keyCode, + KeyEvent.ACTION_DOWN, + triggerKeyDeviceToInputDevice(key.device), + ) - assertThat(consumedCount, `is`(0)) + if (consumed) { + consumedCount++ + } } + assertThat(consumedCount, `is`(0)) + } + fun params_allTriggerKeyCombinationsdoNotConsume(): List> { val triggerAndDescriptions = listOf( "undefined single short-press this-device, do not consume" to singleKeyTrigger( @@ -3191,28 +3397,27 @@ class KeyMapControllerTest { @Test @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") - fun validInput_actionPerformed(description: String, keyMap: KeyMap) = - runTest(testDispatcher) { - // GIVEN - keyMapListFlow.value = listOf(keyMap) - - if (keyMap.trigger.mode is TriggerMode.Parallel) { - // WHEN - mockParallelTrigger(keyMap.trigger) - advanceUntilIdle() - } else { - // WHEN - keyMap.trigger.keys.forEach { - mockTriggerKeyInput(it) - } - - advanceUntilIdle() + fun validInput_actionPerformed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { + // GIVEN + keyMapListFlow.value = listOf(keyMap) + + if (keyMap.trigger.mode is TriggerMode.Parallel) { + // WHEN + mockParallelTrigger(keyMap.trigger) + advanceUntilIdle() + } else { + // WHEN + keyMap.trigger.keys.forEach { + mockTriggerKeyInput(it) } - // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + advanceUntilIdle() } + // THEN + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) { if (key !is KeyCodeTriggerKey) { return @@ -3249,18 +3454,51 @@ class KeyMapControllerTest { } } + private fun createMotionEvent( + axisHatX: Float = 0.0f, + axisHatY: Float = 0.0f, + device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, + isDpad: Boolean = true, + ): MyMotionEvent { + return MyMotionEvent( + metaState = 0, + device = device, + axisHatX = axisHatX, + axisHatY = axisHatY, + isDpad = isDpad, + ) + } + + private fun inputMotionEvent( + axisHatX: Float = 0.0f, + axisHatY: Float = 0.0f, + device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, + ): Boolean = controller.onMotionEvent( + MyMotionEvent( + metaState = 0, + device = device, + axisHatX = axisHatX, + axisHatY = axisHatY, + isDpad = true, + ), + ) + private fun inputKeyEvent( keyCode: Int, action: Int, device: InputDeviceInfo? = null, metaState: Int? = null, scanCode: Int = 0, + repeatCount: Int = 0, ): Boolean = controller.onKeyEvent( - keyCode = keyCode, - action = action, - metaState = metaState ?: 0, - scanCode = scanCode, - device = device, + MyKeyEvent( + keyCode = keyCode, + action = action, + metaState = metaState ?: 0, + scanCode = scanCode, + device = device, + repeatCount = repeatCount, + ), ) private suspend fun mockParallelTrigger( diff --git a/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt b/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt index 7316e60e69..9b03fa9065 100644 --- a/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt +++ b/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt @@ -47,4 +47,8 @@ class FakeOnboardingUseCase : OnboardingUseCase { override val promptForShizukuPermission: Flow = MutableStateFlow(false) override val showShizukuAppIntroSlide: Boolean = false + + override val showNoKeysDetectedBottomSheet: Flow = MutableStateFlow(false) + + override fun neverShowNoKeysRecordedBottomSheet() {} } diff --git a/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt index fdfaf78f6e..d212af3db8 100644 --- a/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt +++ b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.util import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice @@ -31,9 +32,11 @@ fun triggerKey( device: TriggerKeyDevice = TriggerKeyDevice.Internal, clickType: ClickType = ClickType.SHORT_PRESS, consume: Boolean = true, + detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ): KeyCodeTriggerKey = KeyCodeTriggerKey( keyCode = keyCode, device = device, clickType = clickType, consumeEvent = consume, + detectionSource = detectionSource, ) diff --git a/app/version.properties b/app/version.properties index 26400a9d7c..9e2838787d 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=2.7.3 +VERSION_NAME=2.8.0 VERSION_CODE=71 VERSION_NUM=0 \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fee1e1fb8e..0f1a86780d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -30,7 +30,7 @@ lane :prod do # Don't create changelog for f-droid because not committing it # File.write("metadata/android/en-US/changelogs/" + version_code + ".txt", whats_new) - gradle(task: "testDebugUnitTest") + gradle(task: "testProDebugUnitTest") github_token = prompt( text: "Github token: ", diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index ccbc3363eb..a0e90d0677 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,10 +1,11 @@ -New trigger! 🎉 Remap your Assistant & Bixby buttons! +New trigger! 🎉 Remap your Assistant, Bixby and DPAD buttons! What can be remapped? * Volume buttons. * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button. * Bluetooth/wired keyboards. + * DPAD buttons on game controllers. * Fingerprint gestures on supported devices. * Buttons on other connected devices should also work. @@ -15,7 +16,7 @@ You can combine multiple keys from a specific device or any device to form a "tr What can’t be remapped? * Mouse buttons - * Dpad, thumb sticks or triggers on game controllers + * Joysticks or triggers on game controllers Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.