Files
punktfunk/clients/android
enricobuehler 3e6c9f6060 feat(gamepad): add virtual Xbox One/Series + DualShock 4 pad types
Extends virtual-controller support beyond Xbox 360 + DualSense. Goal: a
physical Xbox One or PS4 pad on the client gets a near-native matching virtual
pad on the host, auto-resolved from the controller type.

Protocol/core:
- GamepadPref gains XboxOne (wire 3) + DualShock4 (wire 4); to_u8/from_u8/
  from_name/as_str + C ABI PUNKTFUNK_GAMEPAD_XBOXONE/_DUALSHOCK4 constants
  (compile-time guard ties them to the enum). Single-byte wire form is
  unchanged, so it's forward-compatible (older peers degrade to Auto).

Host (Linux):
- New UHID DualShock 4 backend (inject/dualshock4.rs) bound by hid-playstation:
  lightbar, touchpad, motion, rumble — DualSense minus adaptive triggers /
  player LEDs / mute. Reuses the DualSense pure state + button mapping; only the
  report byte layout, the real-DS4 HID descriptor, the GET_REPORT handshake
  (0x12 MAC mandatory; 0x02 calibration; 0xa3 firmware) and the touchpad
  resolution (1920x942) differ. Touchpad/motion ride the existing 0xCC plane,
  lightbar the 0xCD Led plane (deduped); rumble the universal 0xCA plane.
- Xbox One/Series is the uinput Xbox-360 backend parameterized with the One S
  USB identity (045e:02ea) for matching glyphs — XInput-identical otherwise.
- PadBackend dispatch + resolver handle both; off Linux the UHID pads and
  One/Series fold into Xbox 360. Windows-host DS4 (ViGEm) deferred.

Clients (auto-resolve physical pad -> virtual type, plus manual settings):
- Linux/Windows (SDL3): SDL_GAMEPAD_TYPE_PS4 -> DualShock 4, _XBOXONE ->
  Xbox One; PadInfo carries the resolved pref; DS4 touchpad/motion capture +
  lightbar already type-agnostic. Linux settings combo + label updated.
- Apple (GameController): GCDualShockGamepad/GCXboxGamepad detection, DS4
  touchpad capture, settings picker entries.
- Android (Kotlin): InputDevice VID/PID auto-detect (matching the other
  clients) + settings entries.
- probe: --gamepad help/aliases.

Also hardens the Android JNI boundary: wrap the teardown + poll-thread shims in
catch_unwind so a panic degrades to a logged no-op instead of aborting the app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:34:44 +00:00
..

punktfunk Android client

Native Android client for punktfunk/1, targeting phone + TV (Compose, D-pad + touch).

Architecture — Rust-heavy (like the Linux client, not thin-native like Apple)

Kotlin cannot import the cbindgen C header the way Swift can, so a native bridge is unavoidable. We write it in Rust and link punktfunk-core directly — so the Android client reuses the Linux client's orchestration (audio jitter ring, VK keymap inverse, latency/skew math, capture state machine, trust logic) instead of re-porting it into Kotlin.

Side Owns
Rust (clients/android/nativelibpunktfunk_android.so) the JNI seam, NativeClient (QUIC control + UDP data plane), AnnexB→AMediaCodec decode, Opus+Oboe audio, VK keymap, latency math, trust/pairing
Kotlin (clients/android) Compose UI (host grid / settings / stream), SurfaceView lifecycle, input capture, NsdManager discovery, Keystore identity, permissions

The single seam is io.unom.punktfunk.kit.NativeBridgeJava_io_unom_punktfunk_kit_NativeBridge_*.

Layout

clients/android/native/          Rust cdylib (workspace member) — links punktfunk-core directly
  src/lib.rs                       JNI seam (connect/pair, input, plane getters, abi/core version)
  src/session.rs                   session lifecycle + plane pumps
  src/decode.rs                    AnnexB → AMediaCodec HEVC hardware decode → SurfaceView (incl. HDR10)
  src/audio.rs · src/mic.rs        Opus + Oboe playback / mic uplink (jitter ring)
  src/feedback.rs                  rumble + HID output (lightbar / adaptive triggers)
  src/stats.rs                     live video stats

clients/android/                   Gradle project (this dir)
  settings.gradle.kts · build.gradle.kts · gradle.properties · gradlew
  app/                             :app — Compose UI: Connect / Settings / Stream screens (phone + TV)
  kit/                             :kit — NativeBridge · discovery (NsdManager) · Gamepad · Keymap ·
                                         security (Keystore identity + known-host store) · cargo-ndk build

Prerequisites

  • Android SDK + NDK r30 (30.0.14904198), platforms;android-37.0, build-tools;37.0.0, cmake;3.22.1 (sdkmanager "cmake;3.22.1" — the cmake crate builds libopus with it)
  • JDK 21 for Gradle/AGP (AGP 9.2 runs on JDK 1721, not a newer default JDK like 25)
  • Rust + rustup target add aarch64-linux-android x86_64-linux-android + cargo install cargo-ndk

Toolchain pinned: AGP 9.2.0 · Gradle 9.4.1 · Kotlin 2.3.21 · Compose BOM 2026.05.01 · compileSdk 37 · targetSdk 36 · minSdk 31 · ABIs arm64-v8a + x86_64.

Build & run

Android Studio: open clients/android — it uses its bundled JBR 21 automatically. The cargoNdk* task builds the .so as part of the normal build.

CLI (point Gradle at a JDK 21 if your machine default is newer, e.g. JDK 25):

# Adoptium/Temurin 21 (installed by the Android Studio setup, or `brew install temurin@21`):
export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
cd clients/android
./gradlew :app:assembleDebug      # cargo-ndk cross-compiles libpunktfunk_android.so first
./gradlew :app:installDebug       # onto a running emulator/device

# Emulators (created during env setup):  emulator -avd pf_phone   |   emulator -avd pf_tv

The debug APK lands in app/build/outputs/apk/debug/. Launch it, pick a host from the list, pair, and stream.

Status

A working native client (phone + Android TV), at parity with the Linux and Apple apps for the core streaming experience:

  • VideoAMediaCodec hardware HEVC decode → SurfaceView, including HDR10 (Main10 / BT.2020 PQ), with low-latency decode tuning and a live stats HUD.
  • Audio — Opus + Oboe playback with a jitter ring, plus mic uplink to the host.
  • Input — game controllers (buttons + axes) with rumble and HID feedback; D-pad / game-controller focus navigation for the couch (TV + phone).
  • Discovery & trustNsdManager mDNS host list, SPAKE2 PIN pairing and TOFU, with a Keystore-wrapped client identity and a known-host store.
  • UI — Compose host list / settings / stream screens, Material You theming.
  • Shipping — built for arm64-v8a + x86_64; published to Google Play (Internal Testing).

crates/punktfunk-core uses the ring rcgen backend so the client .so is aws-lc-free.