diff --git a/Cargo.lock b/Cargo.lock index 9eb78b4..7a51ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -92,7 +98,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" dependencies = [ - "android_log-sys", + "android_log-sys 0.3.2", "env_filter", "log", ] @@ -328,10 +334,14 @@ dependencies = [ "chanora_core", "chanora_protocol", "flutter_rust_bridge", + "jni 0.21.1", + "log", + "ndk-context", "serde", "thiserror 2.0.18", "tokio", "tracing", + "tracing-android", "tracing-subscriber", ] @@ -3121,6 +3131,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12612be8f868a09c0ceae7113ff26afe79d81a24473a393cb9120ece162e86c0" +dependencies = [ + "android_log-sys 0.2.0", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" diff --git a/apps/chanora_flutter/android/app/src/main/AndroidManifest.xml b/apps/chanora_flutter/android/app/src/main/AndroidManifest.xml index 767982d..ca7dcb9 100644 --- a/apps/chanora_flutter/android/app/src/main/AndroidManifest.xml +++ b/apps/chanora_flutter/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,15 @@ + + + + + jint { + // Safety: Android guarantees `vm` is a valid JavaVM* for the + // lifetime of the library. + unsafe { + JAVA_VM = vm; + } + JNI_VERSION_1_6 +} + +/// Called by `MainActivity.onCreate` with the application Context. +/// Initialises the `ndk_context` so cpal's Oboe backend can locate +/// the Android audio services. +/// +/// Symbol mangling note: Kotlin / JNI mangles the underscore in +/// `chanora_flutter` to `chanora_1flutter` because the literal `_` +/// in a JNI symbol means package separator. +#[no_mangle] +pub extern "system" fn Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, + context: JObject<'local>, +) { + INIT_ONCE.call_once(|| { + let global = match env.new_global_ref(&context) { + Ok(g) => g, + Err(e) => { + error!("initChanoraContext: new_global_ref failed: {e}"); + return; + } + }; + let raw_ctx = global.as_obj().as_raw() as *mut std::ffi::c_void; + // Leak the global ref so it survives for the process lifetime + // (ndk_context borrows the pointer). + std::mem::forget(global); + unsafe { + ndk_context::initialize_android_context(JAVA_VM, raw_ctx); + } + info!( + "initChanoraContext: ndk_context initialised (vm={:p}, ctx={:p})", + unsafe { JAVA_VM }, + raw_ctx + ); + }); +} diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index aef4f7b..8f01997 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -45,13 +45,33 @@ fn session() -> &'static chanora_core::ChanoraSession { #[frb(init)] pub fn bridge_init() { flutter_rust_bridge::setup_default_user_utils(); - let _ = tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .with_target(true) - .try_init(); + + // On Android, also fan tracing output out to logcat so a user can + // see protocol/audio diagnostics via `adb logcat -s chanora`. + #[cfg(target_os = "android")] + { + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + let android_layer = tracing_android::layer("chanora").ok(); + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + let _ = tracing_subscriber::registry() + .with(filter) + .with(android_layer) + .try_init(); + } + + #[cfg(not(target_os = "android"))] + { + let _ = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .with_target(true) + .try_init(); + } + info!(target: "chanora_bridge", "bridge initialised"); } diff --git a/crates/chanora_bridge/src/lib.rs b/crates/chanora_bridge/src/lib.rs index cb217d3..bd61757 100644 --- a/crates/chanora_bridge/src/lib.rs +++ b/crates/chanora_bridge/src/lib.rs @@ -34,6 +34,9 @@ pub mod api; mod frb_generated; +#[cfg(target_os = "android")] +mod android_init; + use thiserror::Error; /// Errors raised at the bridge boundary. Production code must keep diff --git a/run-chanora.sh b/run-chanora.sh new file mode 100755 index 0000000..49e211d --- /dev/null +++ b/run-chanora.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Launch the Chanora v0.2.0-beta.1 desktop build. +# +# The Flutter app expects libchanora_bridge.so on the dynamic linker +# search path. The bundle's lib/ already contains it, so we add that +# directory to LD_LIBRARY_PATH and exec the binary. + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +BUNDLE="$HERE/apps/chanora_flutter/build/linux/x64/debug/bundle" + +if [[ ! -x "$BUNDLE/chanora_flutter" ]]; then + echo "error: Flutter bundle not built. Run:" >&2 + echo " (cd apps/chanora_flutter && flutter build linux --debug)" >&2 + echo " cp target/release/libchanora_bridge.so $BUNDLE/lib/" >&2 + exit 1 +fi +if [[ ! -e "$BUNDLE/lib/libchanora_bridge.so" ]]; then + echo "error: libchanora_bridge.so missing from $BUNDLE/lib/." >&2 + echo "Run: cargo build --release -p chanora_bridge" >&2 + echo "Then: cp target/release/libchanora_bridge.so $BUNDLE/lib/" >&2 + exit 1 +fi + +export LD_LIBRARY_PATH="$BUNDLE/lib:${LD_LIBRARY_PATH:-}" + +exec "$BUNDLE/chanora_flutter" "$@"