Files
punktfunk/clients/android
enricobuehler b0df291ffe
apple / swift (push) Successful in 55s
ci / rust (push) Failing after 1m11s
ci / web (push) Successful in 28s
android / android (push) Failing after 1m55s
ci / docs-site (push) Successful in 33s
ci / bench (push) Successful in 1m45s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m43s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m9s
docker / deploy-docs (push) Successful in 18s
feat(android): pairing/identity — persistent identity, TOFU pinning, SPAKE2 PIN ceremony
M4 Android stage 1 (trust). The client now presents a persistent self-signed identity on
every connect, pins host certs trust-on-first-use, and runs the SPAKE2 PIN pairing
ceremony — parity with the Apple/Linux clients. The Rust connector already exposed this;
this wires it through the JNI + a Keystore-backed Kotlin store + the connect UI.

- crates/punktfunk-android: nativeGenerateIdentity (mint), nativeConnect gains
  certPem/keyPem/pinHex (identity + TOFU/pinned), nativeHostFingerprint, nativePair
  (SPAKE2). hex32/parse_hex32 helpers.
- kit/security: IdentityStore (AndroidKeyStore AES-256-GCM-wrapped PEM blob; StrongBox
  with TEE fallback; four-state load so a decrypt failure never shadow-mints), PinStore
  (host-id -> fp-hex in SharedPreferences). obtainIdentity mints once on genuine first run.
- app: ConnectScreen loads/mints the identity, looks up the stored pin, and gates connect
  on a trust decision — TOFU prompt (first connect), fingerprint-changed warning, PIN dialog.
- AndroidManifest: allowBackup=false (Keystore keys don't restore; a restored device
  re-mints rather than carrying a dead blob).

Verified live (emulator -> home-worker-2, synthetic m3-host):
- identity: host logs the presented client fingerprint; stable across an app restart.
- TOFU: first-connect prompt -> Trust -> pins the observed host fp -> pinned reconnect
  skips the prompt.
- SPAKE2: PIN ceremony -> "pairing complete — client trusted" -> auto-connect under
  --require-pairing; wrong PIN / host down -> "Pairing failed".

Known follow-up: trust is keyed by mDNS instance id for discovered hosts but by
"host:port" for manually-typed ones, so pairing via one path isn't recognized by the other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:29:04 +02: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 (crates/punktfunk-androidlibpunktfunk_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

crates/punktfunk-android/          Rust cdylib (workspace member)
  src/lib.rs                       JNI_OnLoad + abiVersion/coreVersion (native-link proof)
  src/session.rs                   session handle lifecycle (connect/close); plane pumps = TODO

clients/android/                   Gradle project (this dir)
  settings.gradle.kts · build.gradle.kts · gradle.properties · gradlew
  app/                             :app — Compose application (MainActivity)
  kit/                             :kit — Android library: NativeBridge + the cargo-ndk build
    build.gradle.kts               cargoNdk{Debug,Release} → src/main/jniLibs/<abi>/*.so

Prerequisites (already set up on the dev Mac)

  • Android SDK + NDK r28 LTS (28.2.13676358), platforms;android-37.0, build-tools;37.0.0
  • JDK 21 for Gradle/AGP (the machine default JDK 25 is too new for AGP 9.2)
  • 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 (the machine default is JDK 25, so point Gradle at JDK 21):

export JAVA_HOME="$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home"
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/. The scaffold screen calls NativeBridge.abiVersion() across JNI — a live ABI version proves the whole native stack is wired.

Status

  • Scaffold (done): Gradle modules, cargo-ndk wiring, JNI native-link proof, phone+TV-installable manifest. crates/punktfunk-core rcgen switched to the ring backend so the client .so is aws-lc-free.
  • Next (M4 Android stage 1): video decode (AMediaCodec async → SurfaceView), audio (Opus + Oboe + jitter ring), input capture → send_input, pairing/identity (Keystore-wrapped), mDNS discovery, the phone/TV Compose UI. The Rust-side homes are stubbed in crates/punktfunk-android/src/session.rs with port pointers to crates/punktfunk-client-linux.