48933dc405
Four Android gamepad fixes bringing the client to parity with SDL/Apple: G4 — HAT batched history. Android batches joystick ACTION_MOVEs, so a rapid d-pad tap (press+release within one batch) lived only in the event's historical samples; onMotion read just the final getAxisValue and missed it. Feed every historical HAT sample through the transition logic (new `applyHat`) before the current one. Sticks/triggers stay latest-wins. G9 — floor the rumble one-shot duration. A v2 lease can carry ttl_ms==0 with a nonzero amplitude (past the (0,0) stop guard); createOneShot throws on a non-positive duration, and on the VibratorManager path the effect is built outside the vibrate() runCatching, so the throw would kill the whole rumble poll thread. `durationMs.coerceAtLeast(1)`. G18 — evict feedback binds on disconnect. Rumble/light bindings were cached by device id and freed only at session stop, so a controller unplugged mid-session leaked its open LightsSession. Add GamepadFeedback.onDeviceRemoved(deviceId) (closes the session, cancels rumble), invoked from GamepadRouter's slot-close via a new onSlotClosed callback wired in StreamScreen. The bind maps are now guarded by a lock (the poll threads write them; eviction runs on the main thread). G24 — held exit chord + releases. The emergency-exit chord (Select+Start+ L1+R1) quit the stream the instant it completed — an accidental brush killed the session, and the four held buttons were never released host-side. Now completing the chord ARMS a 1.5 s hold timer (matching DISCONNECT_HOLD on SDL/Apple); onExitChord fires only if still held at expiry, after releasing the held buttons + zeroing the axes on the triggering pad(s). onButton no longer returns the exit bool (async now); MainActivity + StreamScreen updated. G25 (Android half): no change — Android's stick/trigger `.toInt()` already truncates, the chosen cross-client convention. G23 (rich-input plane) stays deferred to its own doc. Verified on this Mac: :kit + :app compileDebugKotlin clean; kit lint unchanged at its pre-existing baseline. On-glass on a real phone + pad still owed (per the Android-regressions-only-show-on-hardware history): watch batched d-pad taps, the 1.5 s exit hold, and a mid-session unplug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>