//! 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" ); } }