#![allow(dead_code)]
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, MutexGuard, OnceLock};
use jni::objects::{JClass, JObject, JValue};
use jni::refs::Global;
use jni::signature::MethodSignature;
use jni::strings::JNIStr;
use jni::{Env, jni_sig, jni_str};
use crate::NativeWidgetError;
use crate::controls::date_picker::CivilDate;
use crate::registry::SlotId;
pub(crate) const LISTENER_CLASS: &str = "dev.frust.nativewidgets.FrustNativeListener";
static CLASS_LOADER: OnceLock<Global<JObject<'static>>> = OnceLock::new();
static CLASSES: LazyLock<Mutex<HashMap<&'static str, Global<JClass<'static>>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) struct NativeCtx<'local, 'env> {
env: &'env mut Env<'local>,
activity: Option<&'env JObject<'local>>,
context: Option<&'env JObject<'local>>,
}
impl<'local, 'env> NativeCtx<'local, 'env> {
pub(crate) fn for_create(
env: &'env mut Env<'local>,
activity: &'env JObject<'local>,
context: &'env JObject<'local>,
) -> Self {
Self {
env,
activity: Some(activity),
context: Some(context),
}
}
pub(crate) fn detached(env: &'env mut Env<'local>) -> Self {
Self {
env,
activity: None,
context: None,
}
}
pub(crate) fn env(&mut self) -> &mut Env<'local> {
self.env
}
pub(crate) fn context(&self) -> Result<&'env JObject<'local>, NativeWidgetError> {
self.context.ok_or_else(|| {
NativeWidgetError::Platform(
"no Android Context on this call path (only createView carries one)".into(),
)
})
}
pub(crate) fn activity(&self) -> Result<&'env JObject<'local>, NativeWidgetError> {
self.activity.ok_or_else(|| {
NativeWidgetError::Platform(
"no Android Activity on this call path (only createView carries one)".into(),
)
})
}
pub(crate) fn run_jni<T>(
&mut self,
op: &str,
f: impl FnOnce(&mut Env<'local>) -> Result<T, jni::errors::Error>,
) -> Result<T, NativeWidgetError> {
run_jni(self.env, op, f)
}
pub(crate) fn class(
&mut self,
binary_name: &'static str,
) -> Result<JClass<'local>, NativeWidgetError> {
{
let cached = lock(&CLASSES);
if let Some(global) = cached.get(binary_name) {
return run_jni(self.env, "NewLocalRef(class)", |env| {
env.new_local_ref(&**global)
});
}
}
let loader = self.class_loader()?;
let class = run_jni(
self.env,
&format!("ClassLoader.loadClass(\"{binary_name}\")"),
|env| {
let name = env.new_string(binary_name)?;
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)
},
)?;
let global = run_jni(self.env, "NewGlobalRef(class)", |env| {
env.new_global_ref(&class)
})?;
lock(&CLASSES).entry(binary_name).or_insert(global);
Ok(class)
}
pub(crate) fn new_view(
&mut self,
binary_name: &'static str,
) -> Result<JObject<'local>, NativeWidgetError> {
let class = self.class(binary_name)?;
let context = self.context()?;
run_jni(self.env, &format!("new {binary_name}(Context)"), |env| {
env.new_object(
&class,
jni_sig!("(Landroid/content/Context;)V"),
&[JValue::Object(context)],
)
})
}
pub(crate) fn retain(
&mut self,
object: &JObject<'_>,
) -> Result<Global<JObject<'static>>, NativeWidgetError> {
run_jni(self.env, "NewGlobalRef", |env| env.new_global_ref(object))
}
pub(crate) fn call_void<'sig, 'sig_args>(
&mut self,
object: &JObject<'_>,
name: &JNIStr,
signature: impl AsRef<MethodSignature<'sig, 'sig_args>>,
args: &[JValue<'_>],
) -> Result<(), NativeWidgetError> {
run_jni(self.env, &format!("{name}"), |env| {
env.call_method(object, name, signature, args)?.v()
})
}
pub(crate) fn add_child(
&mut self,
parent: &JObject<'_>,
child: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
parent,
jni_str!("addView"),
jni_sig!("(Landroid/view/View;)V"),
&[JValue::Object(child)],
)
}
pub(crate) fn set_enabled(
&mut self,
view: &JObject<'_>,
enabled: bool,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("setEnabled"),
jni_sig!("(Z)V"),
&[JValue::Bool(enabled)],
)
}
pub(crate) fn set_content_description(
&mut self,
view: &JObject<'_>,
label: &str,
) -> Result<(), NativeWidgetError> {
let text = run_jni(self.env, "NewStringUTF(contentDescription)", |env| {
env.new_string(label)
})?;
self.call_void(
view,
jni_str!("setContentDescription"),
jni_sig!("(Ljava/lang/CharSequence;)V"),
&[JValue::Object(&text)],
)
}
pub(crate) fn new_listener(
&mut self,
slot_id: SlotId,
) -> Result<JObject<'local>, NativeWidgetError> {
let class = self.class(LISTENER_CLASS)?;
run_jni(self.env, "new FrustNativeListener(long)", |env| {
env.new_object(
&class,
jni_sig!("(J)V"),
&[JValue::Long(slot_id_to_jlong(slot_id))],
)
})
}
pub(crate) fn set_on_click_listener(
&mut self,
view: &JObject<'_>,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("setOnClickListener"),
jni_sig!("(Landroid/view/View$OnClickListener;)V"),
&[JValue::Object(listener)],
)
}
pub(crate) fn set_on_checked_change_listener(
&mut self,
view: &JObject<'_>,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("setOnCheckedChangeListener"),
jni_sig!("(Landroid/widget/CompoundButton$OnCheckedChangeListener;)V"),
&[JValue::Object(listener)],
)
}
pub(crate) fn set_on_seek_bar_change_listener(
&mut self,
view: &JObject<'_>,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("setOnSeekBarChangeListener"),
jni_sig!("(Landroid/widget/SeekBar$OnSeekBarChangeListener;)V"),
&[JValue::Object(listener)],
)
}
pub(crate) fn init_date_picker(
&mut self,
view: &JObject<'_>,
date: CivilDate,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("init"),
jni_sig!("(IIILandroid/widget/DatePicker$OnDateChangedListener;)V"),
&[
JValue::Int(date.year),
JValue::Int(i32::from(date.month) - 1),
JValue::Int(i32::from(date.day)),
JValue::Object(listener),
],
)
}
pub(crate) fn set_on_date_changed_listener(
&mut self,
view: &JObject<'_>,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(
view,
jni_str!("setOnDateChangedListener"),
jni_sig!("(Landroid/widget/DatePicker$OnDateChangedListener;)V"),
&[JValue::Object(listener)],
)
}
pub(crate) fn attach_listener(
&mut self,
view: &JObject<'_>,
slot_id: SlotId,
click: bool,
checked_change: bool,
seek_bar_change: bool,
) -> Result<(Global<JObject<'static>>, Global<JObject<'static>>), NativeWidgetError> {
let listener = self.new_listener(slot_id)?;
let mut click_set = false;
let mut checked_change_set = false;
if click {
self.set_on_click_listener(view, &listener)?;
click_set = true;
}
if checked_change {
if let Err(error) = self.set_on_checked_change_listener(view, &listener) {
self.unwind_partial_attach(view, click_set, checked_change_set, false, &error);
return Err(error);
}
checked_change_set = true;
}
if seek_bar_change && let Err(error) = self.set_on_seek_bar_change_listener(view, &listener)
{
self.unwind_partial_attach(view, click_set, checked_change_set, false, &error);
return Err(error);
}
let retained = match self.retain(view) {
Ok(view_global) => self
.retain(&listener)
.map(|listener_global| (view_global, listener_global)),
Err(error) => Err(error),
};
if let Err(error) = &retained {
self.unwind_partial_attach(view, click, checked_change, seek_bar_change, error);
}
retained
}
pub(crate) fn disarm_listener(
&mut self,
listener: &JObject<'_>,
) -> Result<(), NativeWidgetError> {
self.call_void(listener, jni_str!("disarm"), jni_sig!("()V"), &[])
}
fn unwind_partial_attach(
&mut self,
view: &JObject<'_>,
click_set: bool,
checked_change_set: bool,
seek_bar_change_set: bool,
original: &NativeWidgetError,
) {
if let Err(unwind_error) =
self.detach_listener(view, click_set, checked_change_set, seek_bar_change_set)
{
log::warn!(
"android native-widgets: attach_listener failed ({original}) and unwinding the \
interface(s) it had already set also failed: {unwind_error}"
);
}
}
fn detach_listener(
&mut self,
view: &JObject<'_>,
click: bool,
checked_change: bool,
seek_bar_change: bool,
) -> Result<(), NativeWidgetError> {
let none = JObject::null();
if click {
self.set_on_click_listener(view, &none)?;
}
if checked_change {
self.set_on_checked_change_listener(view, &none)?;
}
if seek_bar_change {
self.set_on_seek_bar_change_listener(view, &none)?;
}
Ok(())
}
pub(crate) fn with_frame<T>(
&mut self,
capacity: usize,
f: impl FnOnce(&mut NativeCtx<'_, '_>) -> Result<T, NativeWidgetError>,
) -> Result<T, NativeWidgetError> {
let activity = self.activity;
let context = self.context;
self.env.with_local_frame(capacity, |env| {
let activity = activity.map(|a| env.new_local_ref(a)).transpose()?;
let context = context.map(|c| env.new_local_ref(c)).transpose()?;
let mut inner = NativeCtx {
env,
activity: activity.as_ref(),
context: context.as_ref(),
};
f(&mut inner)
})
}
fn class_loader(&mut self) -> Result<&'static Global<JObject<'static>>, NativeWidgetError> {
if let Some(loader) = CLASS_LOADER.get() {
return Ok(loader);
}
let context = self.context()?;
let loader = run_jni(self.env, "Context.getClassLoader", |env| {
let loader = env
.call_method(
context,
jni_str!("getClassLoader"),
jni_sig!("()Ljava/lang/ClassLoader;"),
&[],
)?
.l()?;
env.new_global_ref(&loader)
})?;
Ok(CLASS_LOADER.get_or_init(|| loader))
}
}
fn slot_id_to_jlong(slot_id: SlotId) -> i64 {
slot_id as i64
}
pub(crate) fn run_jni<'local, T>(
env: &mut Env<'local>,
op: &str,
f: impl FnOnce(&mut Env<'local>) -> Result<T, jni::errors::Error>,
) -> Result<T, NativeWidgetError> {
let result = f(env);
if env.exception_check() {
return Err(take_pending_exception(env, op));
}
result.map_err(|e| NativeWidgetError::Platform(format!("android native-widgets: {op}: {e}")))
}
fn take_pending_exception(env: &mut Env<'_>, op: &str) -> NativeWidgetError {
let Some(throwable) = env.exception_occurred() else {
env.exception_clear();
return NativeWidgetError::Platform(format!(
"android native-widgets: {op}: JNI reported an exception with no throwable"
));
};
env.exception_clear();
let class_name = match env.get_object_class(&throwable) {
Ok(class) => match class.get_name(env) {
Ok(name) => name.to_string(),
Err(_) => "<unknown exception class>".to_string(),
},
Err(_) => "<unknown exception class>".to_string(),
};
let message = match throwable.get_message(env) {
Ok(message) => message.to_string(),
Err(_) => "<no message>".to_string(),
};
if env.exception_check() {
env.exception_clear();
}
NativeWidgetError::Platform(format!(
"android native-widgets: {op}: {class_name}: {message}"
))
}