use std::sync::{Mutex, MutexGuard, OnceLock};
use jni::errors::LogErrorAndDefault;
use jni::objects::{JClass, JObject, JValue};
use jni::refs::Global;
use jni::sys::{jint, jlong};
use jni::{Env, EnvUnowned, jni_sig, jni_str};
use super::{AlertOutcome, PresentError, Sender, wire};
pub(crate) const PRESENTER_CLASS_BINARY: &str = "dev.frust.nativewidgets.FrustNativePresenter";
static PRESENTER_CLASS: OnceLock<Global<JClass<'static>>> = OnceLock::new();
pub(crate) fn with_presenter<T>(
f: impl FnOnce(&mut Env<'_>, &Global<JClass<'static>>) -> Result<T, PresentError>,
) -> Result<T, PresentError> {
let attached = frust_plugin::android::with_jni_env(|env, context| {
let class = presenter_class(env, context)?;
f(env, class)
});
match attached {
Ok(inner) => inner,
Err(frust_plugin::PlatformHandleError::NotInitialized) => Err(PresentError::Platform(
"the Android platform handles are not installed (a scaffold predating \
nativeInitPlatform)"
.to_string(),
)),
Err(other) => Err(PresentError::Platform(format!(
"android presenter: platform handle error: {other}"
))),
}
}
fn presenter_class(
env: &mut Env<'_>,
context: &JObject,
) -> Result<&'static Global<JClass<'static>>, PresentError> {
if let Some(class) = PRESENTER_CLASS.get() {
return Ok(class);
}
let class = load_presenter_class(env, context)?;
Ok(PRESENTER_CLASS.get_or_init(|| class))
}
fn load_presenter_class(
env: &mut Env<'_>,
context: &JObject,
) -> Result<Global<JClass<'static>>, PresentError> {
let loader = jni_call(env, "Context.getClassLoader", |env| {
env.call_method(
context,
jni_str!("getClassLoader"),
jni_sig!("()Ljava/lang/ClassLoader;"),
&[],
)?
.l()
})?;
let class = jni_call(
env,
"ClassLoader.loadClass(FrustNativePresenter) — is the frust-native-widgets Gradle \
module linked?",
|env| {
let name = env.new_string(PRESENTER_CLASS_BINARY)?;
let class = env
.call_method(
&loader,
jni_str!("loadClass"),
jni_sig!("(Ljava/lang/String;)Ljava/lang/Class;"),
&[JValue::Object(&name)],
)?
.l()?;
env.cast_local::<JClass>(class)
},
)?;
jni_call(env, "NewGlobalRef(FrustNativePresenter)", |env| {
env.new_global_ref(class)
})
}
pub(crate) fn has_resumed_activity(
env: &mut Env<'_>,
class: &Global<JClass<'static>>,
) -> Result<bool, PresentError> {
jni_call(env, "FrustNativePresenter.hasResumedActivity", |env| {
env.call_static_method(class, jni_str!("hasResumedActivity"), jni_sig!("()Z"), &[])?
.z()
})
}
fn jni_call<'local, T>(
env: &mut Env<'local>,
op: &str,
f: impl FnOnce(&mut Env<'local>) -> Result<T, jni::errors::Error>,
) -> Result<T, PresentError> {
crate::android::run_jni(env, op, f).map_err(|e| PresentError::Platform(e.to_string()))
}
pub(crate) type Resolve = Box<dyn FnOnce(i32, i32) + Send>;
struct LiveEntry {
generation: u64,
resolve: Resolve,
}
static LIVE: Mutex<Option<LiveEntry>> = Mutex::new(None);
fn lock_live() -> MutexGuard<'static, Option<LiveEntry>> {
LIVE.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn install_live(generation: u64, resolve: Resolve) -> Option<Resolve> {
lock_live()
.replace(LiveEntry {
generation,
resolve,
})
.map(|displaced| displaced.resolve)
}
pub(crate) fn take_live(generation: u64) -> Option<Resolve> {
let mut live = lock_live();
match live.as_ref() {
Some(entry) if entry.generation == generation => live.take().map(|entry| entry.resolve),
_ => None,
}
}
pub(crate) fn alert_resolver(tx: Sender<AlertOutcome>, action_ids: Vec<String>) -> Resolve {
Box::new(move |code, action_index| {
tx.send(wire::alert_outcome(code, action_index, &action_ids));
})
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_frust_nativewidgets_FrustNativePresenter_nativeOnOutcome<'local>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
generation: jlong,
code: jint,
action_index: jint,
) {
env.with_env(|_env| {
match take_live(generation as u64) {
Some(resolve) => resolve(code, action_index),
None => log::debug!(
"frust-native-widgets: presenter outcome {code} for generation {generation} \
has no live presentation — dropped"
),
}
Ok::<(), jni::errors::Error>(())
})
.resolve::<LogErrorAndDefault>();
}