feat(bridge,android): BridgeEvent::PermissionState + JNI publish hook + c++_shared link
Per SDD-106 §5 add BridgeEvent::PermissionState{permission, state}
with the PermissionStateKind enum (Granted, Denied, PermanentlyDenied,
Unknown). The Kotlin side publishes mid-session permission changes
through a new JNI entry point Java_app_chanora_chanora_1flutter
_MainActivity_publishPermissionState routed by the new
permission_jni.rs module; the Rust audio engine subscribes and
authoritatively clamps the transmit gate (see SDD-106 §6).
Adds crates/chanora_bridge/build.rs to emit
cargo:rustc-link-lib=dylib=c++_shared on Android so libchanora_bridge
.so carries DT_NEEDED libc++_shared.so; this is required by Android
API 24+ per-library linker namespaces to resolve __cxa_pure_virtual
and friends at System.loadLibrary time.
Includes the FRB-regenerated Dart counterparts so each commit is
independently buildable.
Trace: SDD-105, SDD-106 §5, SDD-118 item 6 (extended).
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
//! Android JNI entry point for the platform permission requester.
|
||||
//!
|
||||
//! Trace: SDD-106 §5/§6, SRS-209.
|
||||
//!
|
||||
//! `AndroidPermissionRequester` (Kotlin) resolves the runtime
|
||||
//! `RECORD_AUDIO` permission state and forwards every transition to
|
||||
//! Dart over the existing MethodChannel. Per SDD-106 §5/§6 the
|
||||
//! authoritative consumer of that state is the Rust audio engine's
|
||||
//! `TransmitModeSelector`, not the Dart UI — Dart-only delivery is
|
||||
//! insufficient because the UI may not have re-rendered by the time
|
||||
//! the audio thread next reads the gate.
|
||||
//!
|
||||
//! This module exposes a JNI function that the Kotlin
|
||||
//! `MainActivity.stateChangeListener` calls in addition to the
|
||||
//! MethodChannel. The function:
|
||||
//!
|
||||
//! 1. Decodes the `permission` and `state` strings out of the JVM.
|
||||
//! 2. Maps the `state` string to [`crate::api::PermissionStateKind`]
|
||||
//! (unrecognised values map to `Unknown` — fail-safe per SRS-209).
|
||||
//! 3. Calls [`crate::api::publish_permission_state`] which:
|
||||
//! a. clamps the audio engine's transmit gate when the
|
||||
//! permission identifier is `RECORD_AUDIO` (SDD-106 §6); and
|
||||
//! b. broadcasts a `BridgeEvent::PermissionState` so any Dart
|
||||
//! subscriber observes the same authoritative state.
|
||||
//!
|
||||
//! ## Panic-safety
|
||||
//!
|
||||
//! Every call site is wrapped in [`std::panic::catch_unwind`]. A
|
||||
//! Rust panic must never unwind into the JVM (UB). Panics are
|
||||
//! logged via the `log` facade and otherwise swallowed; the JNI
|
||||
//! function returns `void`.
|
||||
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use jni::objects::{JClass, JString};
|
||||
use jni::JNIEnv;
|
||||
use log::{error, warn};
|
||||
|
||||
use crate::api::{publish_permission_state, PermissionStateKind};
|
||||
|
||||
/// JNI entry point called from
|
||||
/// `app.chanora.chanora_flutter.MainActivity.publishPermissionState`.
|
||||
///
|
||||
/// Symbol mangling note (mirrors SDD-105's `initChanoraContext`):
|
||||
/// the literal `_` inside the package segment `chanora_flutter` is
|
||||
/// escaped as `_1` in the JNI symbol — this is JNI's package-name
|
||||
/// encoding for `_`.
|
||||
///
|
||||
/// Trace: SDD-106 §5.
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_app_chanora_chanora_1flutter_MainActivity_publishPermissionState<
|
||||
'local,
|
||||
>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_class: JClass<'local>,
|
||||
permission: JString<'local>,
|
||||
state: JString<'local>,
|
||||
) {
|
||||
// Wrap the entire body in catch_unwind: a panic across the JNI
|
||||
// boundary is undefined behaviour. We log and swallow on panic.
|
||||
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let permission_str: String = match env.get_string(&permission) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
warn!("publishPermissionState: invalid permission string: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let state_str: String = match env.get_string(&state) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
warn!("publishPermissionState: invalid state string: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let kind = PermissionStateKind::from_kotlin_str(&state_str);
|
||||
// SDD-106 §5/§6: hands off to the bridge's shared publisher
|
||||
// which performs the audio-engine clamp and event fan-out.
|
||||
publish_permission_state(permission_str, kind);
|
||||
}));
|
||||
if let Err(_panic) = result {
|
||||
// Do NOT propagate the panic payload across the FFI
|
||||
// boundary. `_panic` may carry a non-UnwindSafe payload.
|
||||
error!(
|
||||
target: "chanora_bridge",
|
||||
"publishPermissionState: panic caught at JNI boundary; swallowed"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user