feat(android): native display resolution + Settings screen

The connect mode was hardcoded to 720p60 — violating the "native client resolution, no
scaling" invariant. Derive the device's real display mode (landscape, long edge = width) and
add a Settings screen to tune the stream, mirroring the Linux/Apple clients.

- crates/punktfunk-android: nativeConnect gains bitrateKbps + compositorPref + gamepadPref
  (CompositorPref/GamepadPref wire bytes via from_u8); these were hardcoded Auto/Auto/0.
- app/Settings.kt: Settings (width/height/hz/bitrate/compositor/gamepad; 0 = native/auto) +
  a SharedPreferences store + nativeDisplayMode (Display.mode, landscape-swapped) +
  effectiveMode + the UI option tables.
- app/SettingsScreen.kt: dropdowns for resolution / refresh / bitrate / compositor / controller.
- MainActivity: App owns the settings + a Settings screen; ConnectScreen resolves the effective
  mode (Native = the display), shows it on the Connect button, and threads the prefs through
  nativeConnect.

Mic + codec selection deferred (mic uplink isn't wired yet; the decoder is HEVC-only).

Verified live (emulator pf_phone -> home-worker-2): default -> host mode=2400x1080@60 (the
emulator's native display, was 720p); Settings 1920x1080 + 20 Mbps + DualSense -> host
mode=1920x1080, requested_kbps=20000, gamepad=dualsense (host created a UHID DualSense).
Settings persist across screens; pinned reconnect stays silent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:13:55 +02:00
parent cd395f0c22
commit 655a3c7478
5 changed files with 290 additions and 16 deletions
@@ -27,7 +27,10 @@ object NativeBridge {
/**
* Connect, presenting [certPem]/[keyPem] (both empty = anonymous) and pinning [pinHex] (empty =
* trust-on-first-use — read [nativeHostFingerprint] after; else 64-hex host SHA-256, mismatch →
* `0`). Returns an opaque session handle, or `0` on failure. Pair with exactly one [nativeClose].
* `0`). [width]/[height]/[refreshHz] are the requested virtual-output mode (the host streams at
* exactly this); [bitrateKbps] 0 = host default; [compositorPref]/[gamepadPref] are the
* `CompositorPref`/`GamepadPref` wire bytes (0 = Auto). Returns an opaque session handle, or `0`
* on failure. Pair with exactly one [nativeClose].
*/
external fun nativeConnect(
host: String,
@@ -38,6 +41,9 @@ object NativeBridge {
certPem: String,
keyPem: String,
pinHex: String,
bitrateKbps: Int,
compositorPref: Int,
gamepadPref: Int,
): Long
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */