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
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-27 10:14:43 -04:00
1 parent b5cc6dd185
commit e8bfe0c4e4
6 files changed
+111 -12

No files matched your search

@@ -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() },
@@ -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))
}
@@ -7,7 +7,7 @@ class HomeGuideInput(private val setPressed: (Boolean) -> Unit, private val onCa
private val cancelled = mutableSetOf<Int>()
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
}
}
@@ -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() {
@@ -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))
@@ -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)