feat(android): connected-controllers debug view (Settings → Host)

The client end of the "host doesn't see my gamepad" triage chain: a
new ControllersScreen lists every InputDevice Android classifies as a
gamepad/joystick (name, VID:PID, source classes, the punktfunk pad
type it resolves to, rumble test) plus an "Other input devices"
section — a pad behind a BT→USB adapter (the Pico 2W tester case)
often enumerates with the adapter's identity or not as a gamepad at
all, and this makes that visible on the device instead of over a bug
report. A live input test (button chips + axis bars + raw last-keycode
line) consumes pad events via new MainActivity probe hooks ahead of
the focus-nav remap; hold B 1.2s to exit since the pad can't reach the
toggle while captured. Gamepad grows pads()/isPad() (firstPad
generalized).

Kotlin compiles green (kit + app); on-device validation pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:33:04 +00:00
parent 1a483aae06
commit 7ced80c4e3
4 changed files with 428 additions and 21 deletions
@@ -98,20 +98,20 @@ object Gamepad {
}
}
/** First connected gamepad/joystick [InputDevice], or null when none is attached. */
fun firstPad(): InputDevice? {
for (id in InputDevice.getDeviceIds()) {
val d = InputDevice.getDevice(id) ?: continue
val s = d.sources
if (s and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
s and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
) {
return d
}
}
return null
/** True when [dev]'s source classes include gamepad or joystick. */
fun isPad(dev: InputDevice?): Boolean {
val s = dev?.sources ?: return false
return s and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
s and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
}
/** All connected gamepad/joystick [InputDevice]s, in system enumeration order. */
fun pads(): List<InputDevice> =
InputDevice.getDeviceIds().toList().mapNotNull { InputDevice.getDevice(it) }.filter { isPad(it) }
/** First connected gamepad/joystick [InputDevice], or null when none is attached. */
fun firstPad(): InputDevice? = pads().firstOrNull()
/**
* The [GamepadPref] wire byte to send for the user's [setting] (the persisted gamepad index). A
* non-Auto setting is passed through unchanged; "Automatic" ([PREF_AUTO]) resolves to a concrete