use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::Stream;
use jni::objects::{JClass, JClassLoader, JObject, JString};
use jni::refs::{Global, LoaderContext};
use jni::sys::{jlong, jobject};
use jni::{jni_sig, jni_str, native_method, Env, EnvUnowned, JavaVM, NativeMethod, Outcome};
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::Value;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, error, trace, warn};
use crate::error::Error;
#[cfg(feature = "embedded-dex")]
const CLASSES_DEX: &[u8] = include_bytes!("classes.dex");
const BRIDGE_CLASS: &str = "com.plugin.blec.Bridge";
const IPC_TIMEOUT_MARGIN: Duration = Duration::from_secs(5);
struct Bridge {
vm: JavaVM,
class: Global<JClass<'static>>,
pending: Mutex<HashMap<u64, oneshot::Sender<Result<Value, String>>>>,
channels: Mutex<HashMap<u64, mpsc::UnboundedSender<Value>>>,
next_id: AtomicU64,
}
jni::bind_java_type! {
BridgeClass => "com.plugin.blec.Bridge",
methods {
static fn invoke(id: jlong, cmd: JString, args: JString) -> void,
},
hooks = {
load_class = |env, _, _| {
let bridge = bridge().map_err(|_| jni::errors::Error::ClassNotFound {
name: "com.plugin.blec.Bridge".to_string(),
})?;
env.new_local_ref(&bridge.class)
},
},
}
static BRIDGE: OnceCell<Bridge> = OnceCell::new();
fn bridge() -> Result<&'static Bridge, Error> {
BRIDGE
.get()
.ok_or_else(|| Error::Android("android bridge not initialized".to_string()))
}
pub fn init() -> Result<(), Error> {
if BRIDGE.get().is_some() {
return Ok(());
}
let ctx = std::panic::catch_unwind(ndk_context::android_context).map_err(|_| {
Error::Android(
"no android context available: the host did not initialize ndk-context. \
Call blec::android::init_with with a JNIEnv and a Context before blec::init"
.to_string(),
)
})?;
if ctx.vm().is_null() || ctx.context().is_null() {
return Err(Error::Android(
"no android context available: ndk-context holds null pointers".to_string(),
));
}
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) };
let raw_context: jobject = ctx.context().cast();
let class = vm.attach_current_thread(|env| -> Result<Global<JClass<'static>>, Error> {
let context = unsafe { JObject::from_raw(env, raw_context) };
load_bridge(env, &context)
})?;
store_bridge(vm, class);
Ok(())
}
pub unsafe fn init_with(env: *mut jni::sys::JNIEnv, context: jobject) -> Result<(), Error> {
if BRIDGE.get().is_some() {
return Ok(());
}
let mut unowned = unsafe { EnvUnowned::from_raw(env) };
let outcome = unowned
.with_env(|env| -> Result<(JavaVM, Global<JClass<'static>>), Error> {
let vm = env.get_java_vm()?;
let context = unsafe { JObject::from_raw(env, context) };
let class = load_bridge(env, &context)?;
Ok((vm, class))
})
.into_outcome();
let (vm, class) = match outcome {
Outcome::Ok(ok) => ok,
Outcome::Err(e) => return Err(e),
Outcome::Panic(_) => return Err(Error::Android("init_with panicked".to_string())),
};
store_bridge(vm, class);
Ok(())
}
fn load_bridge(env: &mut Env<'_>, context: &JObject<'_>) -> Result<Global<JClass<'static>>, Error> {
let class = load_bridge_class(env, context)?;
unsafe {
env.register_native_methods(
&class,
&[NATIVE_RESOLVE, NATIVE_REJECT, NATIVE_CHANNEL_SEND],
)?;
}
env.call_static_method(
&class,
jni_str!("init"),
jni_sig!((ctx: android.content.Context) -> void),
&[context.into()],
)?;
Ok(env.new_global_ref(&class)?)
}
fn load_bridge_class<'local>(
env: &mut Env<'local>,
context: &JObject<'_>,
) -> Result<JClass<'local>, Error> {
let app_loader = app_class_loader(env, context)?;
match LoaderContext::Loader(&app_loader).load_class(
env,
jni_str!("com.plugin.blec.Bridge"),
true,
) {
Ok(class) => Ok(class),
Err(jni::errors::Error::NoClassDefFound { .. }) => load_embedded_bridge_class(env),
Err(e) => Err(Error::Android(format!(
"could not load {BRIDGE_CLASS}: {e}"
))),
}
}
fn app_class_loader<'local>(
env: &mut Env<'local>,
context: &JObject<'_>,
) -> Result<JClassLoader<'local>, Error> {
let loader = env
.call_method(
context,
jni_str!("getClassLoader"),
jni_sig!(() -> java.lang.ClassLoader),
&[],
)?
.l()?;
Ok(env.cast_local::<JClassLoader>(loader)?)
}
#[cfg(feature = "embedded-dex")]
fn load_embedded_bridge_class<'local>(env: &mut Env<'local>) -> Result<JClass<'local>, Error> {
tracing::info!("{BRIDGE_CLASS} is not in the app, loading the embedded dex instead");
let loader = load_dex(env)?;
LoaderContext::Loader(&loader)
.load_class(env, jni_str!("com.plugin.blec.Bridge"), true)
.map_err(|e| Error::Android(format!("{BRIDGE_CLASS} missing from the embedded dex: {e}")))
}
#[cfg(not(feature = "embedded-dex"))]
fn load_embedded_bridge_class<'local>(_env: &mut Env<'local>) -> Result<JClass<'local>, Error> {
Err(Error::Android(format!(
"{BRIDGE_CLASS} is not in the app: its android module was not built into it. \
Use tauri-plugin-blec or dioxus-blec, add crates/blec/android/lib to the app's gradle \
build, or enable the `embedded-dex` feature of blec"
)))
}
fn store_bridge(vm: JavaVM, class: Global<JClass<'static>>) {
let _ = BRIDGE.set(Bridge {
vm,
class,
pending: Mutex::new(HashMap::new()),
channels: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
});
debug!("android bridge initialized");
}
#[cfg(feature = "embedded-dex")]
fn load_dex<'local>(env: &mut Env<'local>) -> Result<JClassLoader<'local>, Error> {
let parent = boot_class_loader(env)?;
let buffer =
unsafe { env.new_direct_byte_buffer(CLASSES_DEX.as_ptr().cast_mut(), CLASSES_DEX.len()) }?;
let loader = env
.new_object(
jni_str!("dalvik/system/InMemoryDexClassLoader"),
jni_sig!((buf: java.nio.ByteBuffer, parent: java.lang.ClassLoader) -> void),
&[(&buffer).into(), (&parent).into()],
)
.map_err(|e| {
Error::Android(format!(
"could not load the embedded dex: {e}. Loading code at runtime may be blocked \
for this app (for example by the GrapheneOS 'dynamic code loading' toggle)"
))
})?;
Ok(env.cast_local::<JClassLoader>(loader)?)
}
#[cfg(feature = "embedded-dex")]
fn boot_class_loader<'local>(env: &mut Env<'local>) -> Result<JClassLoader<'local>, Error> {
let object = env.find_class(jni_str!("java/lang/Object"))?;
let mut loader = env
.call_method(
&object,
jni_str!("getClassLoader"),
jni_sig!(() -> java.lang.ClassLoader),
&[],
)?
.l()?;
if loader.is_null() {
warn!("Object.class.getClassLoader() is null, falling back to the system class loader");
loader = env
.call_static_method(
jni_str!("java/lang/ClassLoader"),
jni_str!("getSystemClassLoader"),
jni_sig!(() -> java.lang.ClassLoader),
&[],
)?
.l()?;
}
Ok(env.cast_local::<JClassLoader>(loader)?)
}
pub unsafe fn set_activity(env: *mut jni::sys::JNIEnv, activity: jobject) -> Result<(), Error> {
let bridge = bridge()?;
let mut unowned = unsafe { EnvUnowned::from_raw(env) };
let outcome = unowned
.with_env(|env| -> Result<(), Error> {
let activity = unsafe { JObject::from_raw(env, activity) };
env.call_static_method(
&bridge.class,
jni_str!("setActivity"),
jni_sig!((activity: android.app.Activity) -> void),
&[(&activity).into()],
)?;
Ok(())
})
.into_outcome();
match outcome {
Outcome::Ok(()) => Ok(()),
Outcome::Err(e) => Err(e),
Outcome::Panic(_) => Err(Error::Android("set_activity panicked".to_string())),
}
}
pub(crate) async fn call<P: Serialize, R: DeserializeOwned>(
cmd: &'static str,
params: P,
timeout: Duration,
) -> Result<R, Error> {
let bridge = bridge()?;
let args = serde_json::to_string(¶ms)
.map_err(|e| Error::Android(format!("could not serialize arguments of {cmd}: {e}")))?;
let id = bridge.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
bridge
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, tx);
trace!("invoke {cmd}({args}) as {id}");
if let Err(e) = invoke(bridge, id, cmd, &args) {
bridge
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
return Err(e);
}
let result = match tokio::time::timeout(timeout + IPC_TIMEOUT_MARGIN, rx).await {
Ok(Ok(result)) => result,
Ok(Err(_)) => Err("the android side dropped the call".to_string()),
Err(_) => {
bridge
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
return Err(Error::Timeout(cmd.to_string()));
}
};
let value = result.map_err(|e| Error::Android(format!("{cmd} failed: {e}")))?;
serde_json::from_value(value)
.map_err(|e| Error::Android(format!("could not deserialize the answer of {cmd}: {e}")))
}
fn invoke(bridge: &Bridge, id: u64, cmd: &str, args: &str) -> Result<(), Error> {
bridge.vm.attach_current_thread(|env| {
#[allow(clippy::cast_possible_wrap)]
let id = id as jlong;
let cmd = JString::from_str(env, cmd)?;
let args = JString::from_str(env, args)?;
BridgeClass::invoke(env, id, &cmd, &args)?;
Ok(())
})
}
pub(crate) struct ChannelReceiver {
id: u64,
rx: mpsc::UnboundedReceiver<Value>,
}
impl Drop for ChannelReceiver {
fn drop(&mut self) {
if let Some(bridge) = BRIDGE.get() {
bridge
.channels
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&self.id);
}
}
}
impl Stream for ChannelReceiver {
type Item = Value;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Value>> {
self.rx.poll_recv(cx)
}
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(transparent)]
pub(crate) struct Channel(u64);
pub(crate) fn channel() -> Result<(Channel, ChannelReceiver), Error> {
let bridge = bridge()?;
let id = bridge.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = mpsc::unbounded_channel();
bridge
.channels
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, tx);
Ok((Channel(id), ChannelReceiver { id, rx }))
}
const NATIVE_RESOLVE: NativeMethod = native_method! {
static fn native_resolve(id: jlong, json: java.lang.String) -> void,
};
const NATIVE_REJECT: NativeMethod = native_method! {
static fn native_reject(id: jlong, message: java.lang.String) -> void,
};
const NATIVE_CHANNEL_SEND: NativeMethod = native_method! {
static fn native_channel_send(id: jlong, json: java.lang.String) -> void,
};
fn native_resolve(
env: &mut Env<'_>,
_class: JClass<'_>,
id: jlong,
json: JString<'_>,
) -> Result<(), jni::errors::Error> {
let json = json.try_to_string(env)?;
complete(id, parse(&json).map_err(|e| e.to_string()));
Ok(())
}
fn native_reject(
env: &mut Env<'_>,
_class: JClass<'_>,
id: jlong,
message: JString<'_>,
) -> Result<(), jni::errors::Error> {
let message = message.try_to_string(env)?;
complete(id, Err(message));
Ok(())
}
fn native_channel_send(
env: &mut Env<'_>,
_class: JClass<'_>,
id: jlong,
json: JString<'_>,
) -> Result<(), jni::errors::Error> {
let json = json.try_to_string(env)?;
let Some(bridge) = BRIDGE.get() else {
return Ok(());
};
let value = match parse(&json) {
Ok(value) => value,
Err(e) => {
error!("android sent a malformed channel payload: {e}");
return Ok(());
}
};
#[allow(clippy::cast_sign_loss)]
let id = id as u64;
let channels = bridge
.channels
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match channels.get(&id) {
Some(tx) => drop(tx.send(value)),
None => trace!("android sent to the closed channel {id}"),
}
Ok(())
}
fn complete(id: jlong, result: Result<Value, String>) {
let Some(bridge) = BRIDGE.get() else {
return;
};
#[allow(clippy::cast_sign_loss)]
let id = id as u64;
let pending = bridge
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
match pending {
Some(tx) => drop(tx.send(result)),
None => warn!("android answered the unknown call {id}"),
}
}
fn parse(json: &str) -> Result<Value, serde_json::Error> {
if json.is_empty() {
return Ok(Value::Null);
}
serde_json::from_str(json)
}