Files
punktfunk/crates/punktfunk-android/src/lib.rs
T
enricobuehler ee9f636819 feat(android): mic uplink + connect-screen redesign
Microphone uplink (client → host's virtual mic, 0xCB) and a cleaner connect screen.

Mic (Rust-heavy, mirrors the audio playback path in reverse):
- crates/punktfunk-android/src/mic.rs: AAudio LowLatency **input** → realtime callback hands
  captured f32 to a channel → a worker thread Opus-encodes 20 ms stereo frames (48 kHz, VOIP,
  64 kbps) and calls NativeClient::send_mic. MicCapture owns the stream + encode thread (RAII stop).
- session.rs: SessionHandle gains a `mic` slot; nativeStartMic/nativeStopMic JNI (mirror of audio);
  stopped in Drop. NativeBridge: the two externs.
- Settings: a `micEnabled` flag + a Microphone toggle in SettingsScreen that requests RECORD_AUDIO
  (denied → stays off). StreamScreen starts the mic only if enabled AND the permission is held.

Connect-screen redesign:
- One scrollable Column (was a fixed centered layout that could clip with the new tab bar);
  host rows render via forEach (no nested LazyColumn). Colored section labels ("Saved hosts",
  "Discovered on the network", "Connect manually"), full-width host cards / fields / Connect button,
  a header + subtitle, and a muted footer.

Verified live (emulator pf_phone -> home-worker-2): toggling mic requests RECORD_AUDIO; with it
granted, a session sends mic frames (client "mic: sent=250 … peak=0.439" — real audio) and the host
logs "client datagram stream ended … mic=276". Redesigned screen confirmed via screenshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:05:25 +02:00

75 lines
3.0 KiB
Rust

//! punktfunk Android client — the JNI bridge ("nativecore") over `punktfunk-core`.
//!
//! Architecture: the **Rust-heavy** client model (like `punktfunk-client-linux`, *not* the
//! thin-native-over-C-ABI Apple model). This `cdylib` links `punktfunk-core` directly and drives
//! the whole `punktfunk/1` protocol through [`punktfunk_core::client::NativeClient`]; Kotlin owns
//! only the Android-framework surface (Compose UI, `SurfaceView` lifecycle, input capture,
//! `NsdManager` discovery, Keystore). The JNI seam below is the one place the two languages meet.
//!
//! Why Rust-heavy: Kotlin cannot `import` the cbindgen C header the way Swift can, so a native
//! bridge is unavoidable. Writing it in Rust lets the Android client reuse the Linux client's
//! orchestration verbatim — audio jitter ring, the VK keymap inverse, latency/skew math, the
//! input capture state machine, trust/pairing logic — instead of re-porting it into Kotlin.
//!
//! JNI symbols map to `io.unom.punktfunk.kit.NativeBridge` in the `:kit` Gradle module
//! (`clients/android`). The current surface is the scaffold's native-link proof
//! (`abiVersion`/`coreVersion`) plus the session handle lifecycle in [`session`]; the per-plane
//! pumps (video → AMediaCodec, audio → Oboe), input, audio, pairing and mode renegotiation are
//! the next milestone (see the TODOs in [`session`]).
use jni::objects::JObject;
use jni::sys::jint;
use jni::JNIEnv;
#[cfg(target_os = "android")]
mod audio;
#[cfg(target_os = "android")]
mod decode;
mod feedback;
#[cfg(target_os = "android")]
mod mic;
mod session;
/// Initialize `android_logger` once when the JVM loads the library. Logs land in logcat under the
/// `punktfunk` tag. Android-only — there is no JVM (and no logcat) on the host build.
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn JNI_OnLoad(
_vm: *mut jni::sys::JavaVM,
_reserved: *mut std::ffi::c_void,
) -> jint {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("punktfunk"),
);
log::info!(
"punktfunk_android loaded (core ABI v{})",
punktfunk_core::ABI_VERSION
);
jni::sys::JNI_VERSION_1_6
}
/// `NativeBridge.abiVersion(): Int` — the core's C-ABI version. A non-error return is the
/// scaffold's proof that `System.loadLibrary` found the `.so`, the JNI symbol resolved, and the
/// linked `punktfunk-core` is the one we expect.
#[no_mangle]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_abiVersion(
_env: JNIEnv,
_this: JObject,
) -> jint {
punktfunk_core::ABI_VERSION as jint
}
/// `NativeBridge.coreVersion(): String` — the crate version, proving JNI string marshaling works.
#[no_mangle]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_coreVersion<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
) -> jni::sys::jstring {
match env.new_string(env!("CARGO_PKG_VERSION")) {
Ok(s) => s.into_raw(),
Err(_) => JObject::null().into_raw(),
}
}