From e8bfe0c4e4d0796bc8e80d92ad53038b0bde4841 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:14:43 -0400 Subject: [PATCH] fix: hold Home as Steam on Retroid Nova pads (#1674) * fix: hold Home as Steam on Retroid Nova pads * fix(input): track Home Guide apart from controller Guide * fix(input): keep held controls on Home cancel and resume on Home --- .../com/droiddeck/launcher/SessionActivity.kt | 8 ++- .../launcher/input/HomeButtonService.kt | 2 +- .../launcher/input/HomeGuideInput.kt | 7 +- .../droiddeck/launcher/input/PadBridge.java | 33 ++++++++-- .../launcher/input/HomeGuideInputTest.kt | 8 +++ .../launcher/input/PadBridgeQamTest.kt | 65 +++++++++++++++++++ 6 files changed, 111 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 6ee14aacf..49216ee5e 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -68,6 +68,7 @@ import com.droiddeck.launcher.gpu.ScreenEffects import com.droiddeck.launcher.gpu.Lossless import com.droiddeck.launcher.gpu.TurnipDriver import com.droiddeck.launcher.input.EvdevKeys +import com.droiddeck.launcher.input.HomeGuideInput import com.droiddeck.launcher.input.InputSpace import com.droiddeck.launcher.input.KeyboardHost import com.droiddeck.launcher.input.ControllerPrefs @@ -1330,7 +1331,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (incoming?.action != SessionService.ACTION_HOME_GUIDE) return incoming.action = null setIntent(incoming) - if (SessionState.running && SessionState.mode == SessionService.MODE_STEAM) sendSteamGuide() + if (SessionState.suspended) SessionService.resume(this) + else if (SessionState.running && SessionState.mode == SessionService.MODE_STEAM) sendSteamGuide() } @@ -2276,7 +2278,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return true } val resumeKey = event.keyCode == KeyEvent.KEYCODE_BUTTON_A || event.keyCode == KeyEvent.KEYCODE_BUTTON_START || - event.keyCode == KeyEvent.KEYCODE_BUTTON_MODE || event.keyCode == KeyEvent.KEYCODE_HOME + event.keyCode == KeyEvent.KEYCODE_BUTTON_MODE || HomeGuideInput.isHome(event.keyCode) val resumeKeyId = event.deviceId to event.keyCode if (fromController && resumeKey && (SessionState.suspended || resumeKeyId in resumeKeysDown)) { if (event.action == KeyEvent.ACTION_DOWN) { @@ -3290,7 +3292,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { enabled = { resumed && window.decorView.hasWindowFocus() && !ended && !drawerOpen && !pcKeyboardOpen && !achievementsOpen && !gestureSettingsOpen && !pipUi && SessionState.running && !SessionState.suspended && - SessionState.mode == SessionService.MODE_STEAM && padBridge?.canHoldGuide() == true + SessionState.mode == SessionService.MODE_STEAM && padBridge != null }, setPressed = { padBridge?.setHomeGuidePressed(it) }, onCancel = { padBridge?.cancelHomeGuide() }, diff --git a/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt b/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt index 874d8fe58..b85fecb12 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt @@ -38,7 +38,7 @@ object HomeButtonShortcuts { fun onKeyEvent(context: Context, event: KeyEvent): Boolean { // AYN maps its system-assistant key (gpio-keys KEY_F24) to HOME too; only the pad's Home is Guide. - if (event.keyCode != KeyEvent.KEYCODE_HOME || !PadBridge.isControllerKeyEvent(event)) return false + if (!HomeGuideInput.isHome(event.keyCode) || !PadBridge.isControllerKeyEvent(event)) return false return input.onKeyEvent(event, enabled() && HomeApp.isDefault(context)) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt b/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt index dfb7dbb0a..785f7c1a1 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt @@ -7,7 +7,7 @@ class HomeGuideInput(private val setPressed: (Boolean) -> Unit, private val onCa private val cancelled = mutableSetOf() fun onKeyEvent(event: KeyEvent, enabled: Boolean): Boolean { - if (event.keyCode != KeyEvent.KEYCODE_HOME) return false + if (!isHome(event.keyCode)) return false if (!enabled) cancel() val device = event.deviceId return when (event.action) { @@ -39,4 +39,9 @@ class HomeGuideInput(private val setPressed: (Boolean) -> Unit, private val onCa cancelled.remove(device) if (presses.remove(device) && presses.isEmpty()) { onCancel(); setPressed(false) } } + + companion object { + // The Retroid Nova pad falls back to Generic.kl, which maps its Home scan code to MOVE_HOME. + fun isHome(keyCode: Int) = keyCode == KeyEvent.KEYCODE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_HOME + } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java index 28ee6403f..7dd234daf 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java +++ b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java @@ -53,6 +53,7 @@ public final class PadBridge { private final FakeInputWriter[] playerWriters = new FakeInputWriter[SLOTS]; private final PadState[] playerStates = new PadState[SLOTS]; private final PadState effectiveState = new PadState(); + private final PadState homeGuideReport = new PadState(); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private boolean open; private volatile boolean selectSteam; @@ -63,6 +64,7 @@ public final class PadBridge { private boolean systemQamPressed; private boolean backQamPressed; private boolean homeGuidePressed; + private int homeGuideSlot = -1; private final PadState lastBackQamReport = new PadState(); private final PadState lastWrittenReport = new PadState(); private boolean backQamAxesBlocked; @@ -162,6 +164,7 @@ public final class PadBridge { public synchronized void stop() { homeGuidePressed = false; + homeGuideSlot = -1; clearBackQamWrites(); releaseBackPaddles(); activeControllerId = NO_CONTROLLER; @@ -229,6 +232,7 @@ public final class PadBridge { case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_HOME: + case KeyEvent.KEYCODE_MOVE_HOME: if (!isFromController(device)) return false; return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC @@ -329,7 +333,8 @@ public final class PadBridge { case KeyEvent.KEYCODE_BUTTON_THUMBL: pad.press(8, pressed); break; case KeyEvent.KEYCODE_BUTTON_THUMBR: pad.press(9, pressed); break; case KeyEvent.KEYCODE_BUTTON_MODE: - case KeyEvent.KEYCODE_HOME: pad.press(PadState.GUIDE, pressed); break; + case KeyEvent.KEYCODE_HOME: + case KeyEvent.KEYCODE_MOVE_HOME: pad.press(PadState.GUIDE, pressed); break; case KeyEvent.KEYCODE_BUTTON_Z: if (isPrimary(event)) DeckControls.INSTANCE.setGrip(isNovaController(event.getDevice()) ? DeckControls.R4 : DeckControls.L4, pressed); break; @@ -485,6 +490,7 @@ public final class PadBridge { public synchronized void releaseAll() { homeGuidePressed = false; + homeGuideSlot = -1; clearBackQamWrites(); routedTouch.clear(); backQamPressed = false; @@ -512,18 +518,24 @@ public final class PadBridge { return builtinPad && nativeQam(); } - public boolean canHoldGuide() { - return builtinPad; - } - public synchronized void setHomeGuidePressed(boolean pressed) { - if (pressed && !canHoldGuide()) return; + int slot = pressed ? firstPlayerSlot() : homeGuideSlot; + if (slot > 0) { + homeGuideSlot = pressed ? slot : -1; + publish(slot); + return; + } if (homeGuidePressed == pressed) return; homeGuidePressed = pressed; updateHeldModifier(PadState.GUIDE, pressed); } public synchronized void cancelHomeGuide() { + int slot = homeGuideSlot; + if (slot > 0) { + homeGuideSlot = -1; + publish(slot); + } if (!homeGuidePressed) return; clearBackQamWrites(); homeGuidePressed = false; @@ -784,6 +796,7 @@ public final class PadBridge { partners[slot] = NO_CONTROLLER; clearJoyCons(slot); slotControllers.set(slot, NO_CONTROLLER); + if (slot == homeGuideSlot) homeGuideSlot = -1; if (slot == firstPlayerSlot()) releaseBackPaddles(); if (slot == 0) { state.clear(); @@ -840,7 +853,13 @@ public final class PadBridge { } playerWriters[slot] = player; } - player.writePad(playerStates[slot]); + PadState report = playerStates[slot]; + if (slot == homeGuideSlot) { + homeGuideReport.copyFrom(report); + homeGuideReport.press(PadState.GUIDE, true); + report = homeGuideReport; + } + player.writePad(report); } private void publish() { diff --git a/app/src/test/java/com/droiddeck/launcher/input/HomeGuideInputTest.kt b/app/src/test/java/com/droiddeck/launcher/input/HomeGuideInputTest.kt index ccef31215..3d2a2863e 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/HomeGuideInputTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/HomeGuideInputTest.kt @@ -27,6 +27,14 @@ class HomeGuideInputTest { assertEquals(listOf(true, false), states) } + @Test fun genericLayoutMoveHomeHoldsGuideLikeHome() { + val down = KeyEvent(10, 110, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MOVE_HOME, 0, 0, 10, 102) + val up = KeyEvent(10, 200, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MOVE_HOME, 0, 0, 10, 102) + assertTrue(input.onKeyEvent(down, true)) + assertTrue(input.onKeyEvent(up, true)) + assertEquals(listOf(true, false), states) + } + @Test fun anUnownedHomePressKeepsAndroidBehaviorEvenIfSessionBecomesEligibleBeforeRelease() { assertFalse(input.onKeyEvent(home(KeyEvent.ACTION_DOWN), false)) assertFalse(input.onKeyEvent(home(KeyEvent.ACTION_UP), true)) diff --git a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeQamTest.kt b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeQamTest.kt index 9ac6ecfcc..47bf0719c 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeQamTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeQamTest.kt @@ -343,6 +343,71 @@ class PadBridgeQamTest { assertFalse(pad().isDown(PadState.X)) } + @Test fun homeGuideHoldsOnThePrimaryPlayerWithoutABuiltinPad() { + PadBridge.setFirstSlot(1) + try { + bridge.setBuiltinPad(false) + bridge.setHomeGuidePressed(true) + assertTrue(requireNotNull(JoyConPadTest.RecordingWriter.pads[1]).isDown(PadState.GUIDE)) + bridge.setHomeGuidePressed(false) + assertFalse(requireNotNull(JoyConPadTest.RecordingWriter.pads[1]).isDown(PadState.GUIDE)) + } finally { + PadBridge.setFirstSlot(0) + } + } + + @Test fun homeGuideAndControllerGuideReleaseIndependentlyOnThePrimaryPlayer() { + PadBridge.setFirstSlot(1) + try { + bridge.setBuiltinPad(false) + val device = controller() + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_MODE)) + bridge.setHomeGuidePressed(true) + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.ACTION_UP)) + assertTrue(requireNotNull(JoyConPadTest.RecordingWriter.pads[1]).isDown(PadState.GUIDE)) + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_MODE)) + bridge.setHomeGuidePressed(false) + assertTrue(requireNotNull(JoyConPadTest.RecordingWriter.pads[1]).isDown(PadState.GUIDE)) + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.ACTION_UP)) + assertFalse(requireNotNull(JoyConPadTest.RecordingWriter.pads[1]).isDown(PadState.GUIDE)) + } finally { + PadBridge.setFirstSlot(0) + } + } + + @Test fun removingThePrimaryPlayerForgetsItsHomeGuide() { + PadBridge.setFirstSlot(1) + try { + bridge.setBuiltinPad(false) + val device = controller() + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_A, KeyEvent.ACTION_UP)) + bridge.setHomeGuidePressed(true) + bridge.onDeviceRemoved(device.id) + JoyConPadTest.RecordingWriter.pads.remove(1) + bridge.cancelHomeGuide() + bridge.setHomeGuidePressed(false) + assertNull(JoyConPadTest.RecordingWriter.pads[1]) + } finally { + PadBridge.setFirstSlot(0) + } + } + + @Test fun cancellingHomeGuideOnAPlayerSlotKeepsItsHeldControls() { + PadBridge.setFirstSlot(1) + try { + bridge.setBuiltinPad(false) + val device = controller() + bridge.onKeyEvent(key(device, KeyEvent.KEYCODE_BUTTON_A)) + bridge.setHomeGuidePressed(true) + bridge.cancelHomeGuide() + val pad = requireNotNull(JoyConPadTest.RecordingWriter.pads[1]) + assertFalse(pad.isDown(PadState.GUIDE)) + assertTrue(pad.isDown(PadState.A)) + } finally { + PadBridge.setFirstSlot(0) + } + } + @Test fun cancellingHomeDiscardsQueuedPartnersAndReleasesGuideImmediately() { bridge.setHomeGuidePressed(true) bridge.applyExternal({ it.press(PadState.LB, true); it.leftY = -1f }, true)