use crate::{ClipboardOptions, Error};
use jni::objects::{JObject, JString, JValue};
use jni::sys::jobject;
use jni::JNIEnv;
pub(crate) struct ClipboardImpl;
impl ClipboardImpl {
pub(crate) fn new(_opts: &ClipboardOptions) -> Result<Self, Error> {
Ok(Self)
}
pub(crate) async fn get_text(&mut self) -> Result<String, Error> {
with_jni_env(get_text_jni)
}
pub(crate) async fn set_text(&mut self, text: &str) -> Result<(), Error> {
let text = text.to_owned();
with_jni_env(move |env, ctx| set_text_jni(env, ctx, &text))
}
}
fn with_jni_env<T, F>(f: F) -> Result<T, Error>
where
F: FnOnce(&mut JNIEnv<'_>, &JObject<'_>) -> Result<T, Error>,
{
let android_ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(android_ctx.vm().cast()) }
.map_err(|e| Error::platform("android: JavaVM::from_raw", e))?;
let mut env = vm
.attach_current_thread()
.map_err(|e| Error::platform("android: attach_current_thread", e))?;
let context = unsafe { JObject::from_raw(android_ctx.context() as jobject) };
let result = f(&mut env, &context);
std::mem::forget(context);
result
}
fn get_clipboard_manager<'local, 'ctx>(
env: &mut JNIEnv<'local>,
context: &JObject<'ctx>,
) -> Result<JObject<'local>, Error> {
let context_class = env
.find_class("android/content/Context")
.map_err(|e| Error::platform("android: find_class(Context)", e))?;
let service_field = env
.get_static_field(context_class, "CLIPBOARD_SERVICE", "Ljava/lang/String;")
.map_err(|e| Error::platform("android: get CLIPBOARD_SERVICE", e))?;
let service_name = service_field
.l()
.map_err(|e| Error::platform("android: extract CLIPBOARD_SERVICE", e))?;
let manager = env
.call_method(
context,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[JValue::Object(&service_name)],
)
.map_err(|e| Error::platform("android: getSystemService", e))?
.l()
.map_err(|e| Error::platform("android: getSystemService result", e))?;
if manager.is_null() {
return Err(Error::NotSupported);
}
Ok(manager)
}
fn get_text_jni<'local, 'ctx>(
env: &mut JNIEnv<'local>,
context: &JObject<'ctx>,
) -> Result<String, Error> {
let manager = get_clipboard_manager(env, context)?;
let clip = env
.call_method(
manager,
"getPrimaryClip",
"()Landroid/content/ClipData;",
&[],
)
.map_err(|e| Error::platform("android: getPrimaryClip", e))?
.l()
.map_err(|e| Error::platform("android: getPrimaryClip result", e))?;
if clip.is_null() {
return Err(Error::PermissionDenied(
"getPrimaryClip returned null (app may lack focus)",
));
}
let count = env
.call_method(&clip, "getItemCount", "()I", &[])
.map_err(|e| Error::platform("android: getItemCount", e))?
.i()
.map_err(|e| Error::platform("android: getItemCount result", e))?;
if count <= 0 {
return Err(Error::Unavailable("clipboard is empty"));
}
let item = env
.call_method(
clip,
"getItemAt",
"(I)Landroid/content/ClipData$Item;",
&[JValue::Int(0)],
)
.map_err(|e| Error::platform("android: getItemAt(0)", e))?
.l()
.map_err(|e| Error::platform("android: getItemAt result", e))?;
let char_seq = env
.call_method(
item,
"coerceToText",
"(Landroid/content/Context;)Ljava/lang/CharSequence;",
&[JValue::Object(context)],
)
.map_err(|e| Error::platform("android: coerceToText", e))?
.l()
.map_err(|e| Error::platform("android: coerceToText result", e))?;
if char_seq.is_null() {
return Err(Error::Unavailable("clipboard item has no text"));
}
let jstring_obj = env
.call_method(char_seq, "toString", "()Ljava/lang/String;", &[])
.map_err(|e| Error::platform("android: toString", e))?
.l()
.map_err(|e| Error::platform("android: toString result", e))?;
let jstring = JString::from(jstring_obj);
let rust_string = env
.get_string(&jstring)
.map_err(|e| Error::platform("android: get_string", e))?
.to_string_lossy()
.into_owned();
Ok(rust_string)
}
fn set_text_jni<'local, 'ctx>(
env: &mut JNIEnv<'local>,
context: &JObject<'ctx>,
text: &str,
) -> Result<(), Error> {
let manager = get_clipboard_manager(env, context)?;
let clipdata_class = env
.find_class("android/content/ClipData")
.map_err(|e| Error::platform("android: find_class(ClipData)", e))?;
let label = env
.new_string("clipawl")
.map_err(|e| Error::platform("android: new_string(label)", e))?;
let value = env
.new_string(text)
.map_err(|e| Error::platform("android: new_string(text)", e))?;
let label_obj = JObject::from(label);
let value_obj = JObject::from(value);
let clipdata = env
.call_static_method(
clipdata_class,
"newPlainText",
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
&[JValue::Object(&label_obj), JValue::Object(&value_obj)],
)
.map_err(|e| Error::platform("android: newPlainText", e))?
.l()
.map_err(|e| Error::platform("android: newPlainText result", e))?;
env.call_method(
manager,
"setPrimaryClip",
"(Landroid/content/ClipData;)V",
&[JValue::Object(&clipdata)],
)
.map_err(|e| Error::platform("android: setPrimaryClip", e))?;
Ok(())
}