use std::sync::OnceLock;
use jni::objects::{GlobalRef, JClass, JObject, JString, JValue};
use jni::{JNIEnv, JavaVM};
struct Handle {
vm: JavaVM,
class: GlobalRef,
activity: GlobalRef,
}
unsafe impl Send for Handle {}
unsafe impl Sync for Handle {}
static HANDLE: OnceLock<Handle> = OnceLock::new();
pub(crate) fn install(env: &mut JNIEnv, loader: &JObject, activity: &JObject) {
match try_install(env, loader, activity) {
Ok(handle) => {
let _ = HANDLE.set(handle);
}
Err(err) => {
clear_exception(env);
log::warn!("clipboard unavailable ({err}) — copy and paste do nothing");
}
}
}
fn try_install(
env: &mut JNIEnv,
loader: &JObject,
activity: &JObject,
) -> Result<Handle, jni::errors::Error> {
let name = env.new_string("dev.frus.input.FrusClipboard")?;
let class = env
.call_method(
loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&name)],
)?
.l()?;
Ok(Handle {
vm: env.get_java_vm()?,
class: env.new_global_ref(&class)?,
activity: env.new_global_ref(activity)?,
})
}
fn clear_exception(env: &mut JNIEnv) {
if env.exception_check().unwrap_or(false) {
let _ = env.exception_clear();
}
}
pub(crate) fn get_text() -> Option<String> {
let handle = HANDLE.get()?;
let mut env = handle.vm.attach_current_thread_permanently().ok()?;
let class: &JClass = handle.class.as_obj().into();
let read = env.call_static_method(
class,
"getText",
"(Landroid/app/Activity;)Ljava/lang/String;",
&[JValue::Object(handle.activity.as_obj())],
);
let object = match read.and_then(|value| value.l()) {
Ok(object) => object,
Err(err) => {
clear_exception(&mut env);
log::warn!("clipboard: read failed ({err})");
return None;
}
};
if object.is_null() {
return None;
}
let text = JString::from(object);
let copied = env.get_string(&text).map(String::from);
match copied {
Ok(text) => Some(text),
Err(err) => {
clear_exception(&mut env);
log::warn!("clipboard: the text could not be read back ({err})");
None
}
}
}
pub(crate) fn set_text(text: &str) -> bool {
let Some(handle) = HANDLE.get() else {
return false;
};
let Ok(mut env) = handle.vm.attach_current_thread_permanently() else {
return false;
};
let Ok(text) = env.new_string(text) else {
clear_exception(&mut env);
return false;
};
let class: &JClass = handle.class.as_obj().into();
let written = env.call_static_method(
class,
"setText",
"(Landroid/app/Activity;Ljava/lang/String;)V",
&[
JValue::Object(handle.activity.as_obj()),
JValue::Object(&text),
],
);
match written {
Ok(_) => true,
Err(err) => {
clear_exception(&mut env);
log::warn!("clipboard: write failed ({err})");
false
}
}
}