Skip to content

Commit

Permalink
Merge pull request #1398 from keymapperorg/feature/491-detect-dpad-ke…
Browse files Browse the repository at this point in the history
…y-events

#491 detect dpad key events
  • Loading branch information
sds100 authored Jan 19, 2025
2 parents dd0be28 + 51090da commit 8251040
Show file tree
Hide file tree
Showing 100 changed files with 4,857 additions and 2,931 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -109,6 +111,7 @@ class KeyMapController(
val parallelTriggerActionPerformers =
mutableMapOf<Int, ParallelTriggerActionPerformer>()
val parallelTriggerModifierKeyIndices = mutableListOf<Pair<Int, Int>>()
val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf<KeyCodeTriggerKey>()

// Only process key maps that can be triggered
val validKeyMaps = value.filter { keyMap ->
Expand All @@ -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
) {
Expand Down Expand Up @@ -378,6 +385,8 @@ class KeyMapController(
this.parallelTriggerActionPerformers = parallelTriggerActionPerformers
this.sequenceTriggerActionPerformers = sequenceTriggerActionPerformers

this.triggerKeysThatSendRepeatedKeyEvents = triggerKeysThatSendRepeatedKeyEvents

reset()
}

Expand Down Expand Up @@ -505,6 +514,18 @@ class KeyMapController(
*/
private val parallelTriggerLongPressJobs: SparseArrayCompat<Job> = SparseArrayCompat<Job>()

/**
* 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<KeyCodeTriggerKey> = emptySet()

private var parallelTriggerActionPerformers: Map<Int, ParallelTriggerActionPerformer> =
emptyMap()
private var sequenceTriggerActionPerformers: Map<Int, SequenceTriggerActionPerformer> =
Expand Down Expand Up @@ -548,6 +569,8 @@ class KeyMapController(
PreferenceDefaults.FORCE_VIBRATE,
)

private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker()

init {
coroutineScope.launch {
useCase.allKeyMapList.collectLatest { keyMapList ->
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -790,7 +859,7 @@ class KeyMapController(

if (isModifierKey(actionKeyCode)) {
val actionMetaState =
KeyEventUtils.modifierKeycodeToMetaState(actionKeyCode)
InputEventUtils.modifierKeycodeToMetaState(actionKeyCode)
metaStateFromActions =
metaStateFromActions.withFlag(actionMetaState)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1430,8 +1499,7 @@ class KeyMapController(
return detectedTriggerIndexes.isNotEmpty()
}

private fun encodeActionList(actions: List<KeyMapAction>): IntArray =
actions.map { getActionKey(it) }.toIntArray()
private fun encodeActionList(actions: List<KeyMapAction>): IntArray = actions.map { getActionKey(it) }.toIntArray()

/**
* @return the key for the action in [actionMap]. Returns -1 if the [action] can't be found.
Expand Down Expand Up @@ -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<KeyMapAction>) {
var key = 0
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ class ConfigTriggerViewModel(
createKeyMapShortcut: CreateKeyMapShortcutUseCase,
displayKeyMap: DisplayKeyMapUseCase,
resourceProvider: ResourceProvider,
private val purchasingManager: PurchasingManager,
purchasingManager: PurchasingManager,
setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase,
) : BaseConfigTriggerViewModel(
coroutineScope,
onboarding,
config,
recordTrigger,
createKeyMapShortcut,
displayKeyMap,
setupGuiKeyboardUseCase,
resourceProvider,
)
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 3 additions & 1 deletion app/src/main/assets/whats-new.txt
Original file line number Diff line number Diff line change
@@ -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.
New trigger! 🎉 You can now remap the DPAD buttons on your game controller!

Joysticks and triggers are coming soon... 😉
18 changes: 18 additions & 0 deletions app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Loading

0 comments on commit 8251040

Please sign in to comment.