feat(android): scaffold the native Android client (Rust-heavy JNI bridge)
apple / swift (push) Successful in 52s
ci / docs-site (push) Successful in 27s
android / android (push) Successful in 4m52s
ci / web (push) Successful in 26s
ci / bench (push) Successful in 1m33s
ci / rust (push) Successful in 6m56s
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 5s
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
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m54s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m29s
deb / build-publish (push) Successful in 6m46s
docker / deploy-docs (push) Successful in 22s
apple / swift (push) Successful in 52s
ci / docs-site (push) Successful in 27s
android / android (push) Successful in 4m52s
ci / web (push) Successful in 26s
ci / bench (push) Successful in 1m33s
ci / rust (push) Successful in 6m56s
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 5s
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
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m54s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m29s
deb / build-publish (push) Successful in 6m46s
docker / deploy-docs (push) Successful in 22s
Rust-heavy client model (like punktfunk-client-linux): a new cdylib crate crates/punktfunk-android links punktfunk-core and exposes the JNI seam; Kotlin (clients/android) owns only the Android-framework surface. Kotlin can't import the C header the way Swift can, so the bridge is written in Rust to reuse the Linux client's orchestration rather than re-port it. - crates/punktfunk-android: JNI bridge — abiVersion/coreVersion native-link proof + session connect/close handle; plane pumps stubbed for M4 stage 1. - clients/android: Gradle project — :app (Compose) + :kit (Android library with a cargo-ndk Exec task -> jniLibs). AGP 9.2 / Gradle 9.4.1 / Kotlin 2.3.21 / Compose BOM 2026.05.01 / compileSdk 37 / targetSdk 36 / minSdk 31, shipping arm64-v8a + x86_64. Phone + TV (leanback) installable. README rewritten. - .gitea/workflows/android.yml: CI mirroring apple.yml on a Linux runner. - punktfunk-core: switch rcgen to the ring backend so the whole quic tree is aws-lc-free (smaller client .so, cmake-free cross-compile; a win for all targets). Validated on this box: :app:assembleDebug -> APK with both ABIs; emulator first-light renders the bridge linked (core ABI v2) with logcat confirmation; clippy -D warnings + cargo fmt clean; core tests green on the ring backend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
//! Session handle lifecycle over JNI.
|
||||
//!
|
||||
//! A connected [`NativeClient`] is boxed and handed to Kotlin as an opaque `jlong`; [`nativeClose`]
|
||||
//! drops it, and the connector's `Drop` tears down the worker thread + QUIC connection (RAII). The
|
||||
//! client is `Sync`, so the Kotlin side is free to pull each plane from its own thread later.
|
||||
//!
|
||||
//! TODO(M4 Android stage 1): build out the plane pumps + IO on top of this handle. Port the
|
||||
//! orchestration from `crates/punktfunk-client-linux`:
|
||||
//!
|
||||
//! - video: `next_frame` → AnnexB access unit → `AMediaCodec` (NDK, async) → `SurfaceView`
|
||||
//! - audio: `next_audio` → Opus decode → jitter ring → Oboe (port `client-linux/src/audio.rs`)
|
||||
//! - input: Kotlin capture → `send_input` / `send_rich_input` (VK keymap from `keymap.rs`)
|
||||
//! - rumble/HID feedback: `next_rumble` / `next_hidout` → VibratorManager / LightsManager
|
||||
//! - trust: `generate_identity` + `pair` + pin (Keystore-wrapped), then pass `pin`/`identity` here
|
||||
//!
|
||||
//! The signatures below are deliberately minimal (TOFU, anonymous) so the scaffold can already
|
||||
//! stand up a session against a host that does not require pairing.
|
||||
|
||||
use jni::objects::{JObject, JString};
|
||||
use jni::sys::{jint, jlong};
|
||||
use jni::JNIEnv;
|
||||
use punktfunk_core::client::NativeClient;
|
||||
use punktfunk_core::config::{CompositorPref, GamepadPref, Mode};
|
||||
use std::time::Duration;
|
||||
|
||||
/// `NativeBridge.nativeConnect(host, port, width, height, refreshHz): Long`.
|
||||
///
|
||||
/// Trust-on-first-use (no pin) and anonymous (no client identity) — enough to bring up a stream
|
||||
/// against a host that does not require pairing. Returns an opaque session handle, or `0` on
|
||||
/// failure (the cause is logged to logcat).
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_this: JObject<'local>,
|
||||
host: JString<'local>,
|
||||
port: jint,
|
||||
width: jint,
|
||||
height: jint,
|
||||
refresh_hz: jint,
|
||||
) -> jlong {
|
||||
let host: String = match env.get_string(&host) {
|
||||
Ok(s) => s.into(),
|
||||
Err(_) => return 0,
|
||||
};
|
||||
let mode = Mode {
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
refresh_hz: refresh_hz as u32,
|
||||
};
|
||||
match NativeClient::connect(
|
||||
&host,
|
||||
port as u16,
|
||||
mode,
|
||||
CompositorPref::Auto,
|
||||
GamepadPref::Auto,
|
||||
0, // bitrate_kbps: let the host choose its default
|
||||
None, // launch: default app
|
||||
None, // pin: trust on first use
|
||||
None, // identity: anonymous (TODO: Keystore-backed identity + pairing)
|
||||
Duration::from_secs(10),
|
||||
) {
|
||||
Ok(client) => Box::into_raw(Box::new(client)) as jlong,
|
||||
Err(e) => {
|
||||
log::error!("nativeConnect to {host}:{port} failed: {e}");
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `NativeBridge.nativeClose(handle)` — drop the boxed [`NativeClient`] (RAII shutdown of the
|
||||
/// worker thread + QUIC connection). No-op on a `0` handle.
|
||||
///
|
||||
/// # Safety contract
|
||||
/// `handle` must be either `0` or a value previously returned by [`Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect`]
|
||||
/// and not already closed. Kotlin owns this invariant (one `nativeClose` per non-zero `nativeConnect`).
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClose(
|
||||
_env: JNIEnv,
|
||||
_this: JObject,
|
||||
handle: jlong,
|
||||
) {
|
||||
if handle != 0 {
|
||||
// SAFETY: per the contract above, `handle` is a live `Box<NativeClient>` pointer.
|
||||
unsafe { drop(Box::from_raw(handle as *mut NativeClient)) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user