use std::sync::{Mutex, OnceLock};
use jni::objects::{GlobalRef, JClass, JObject, JString, JValue};
use jni::sys::{jboolean, jint, JNI_TRUE};
use jni::{JNIEnv, JavaVM};
use winit::platform::android::activity::AndroidApp;
pub(crate) use crate::ime::ImeEvent;
static QUEUE: Mutex<Vec<ImeEvent>> = Mutex::new(Vec::new());
static WAKER: OnceLock<Mutex<winit::platform::android::activity::AndroidAppWaker>> =
OnceLock::new();
static BRIDGE: OnceLock<Bridge> = OnceLock::new();
struct Bridge {
vm: JavaVM,
class: GlobalRef,
activity: GlobalRef,
}
unsafe impl Send for Bridge {}
unsafe impl Sync for Bridge {}
#[derive(Default)]
struct EditorState {
chars: Vec<char>,
cursor: usize,
selection: Option<(usize, usize)>,
}
static EDITOR: Mutex<Option<EditorState>> = Mutex::new(None);
pub(crate) fn set_editor_state(text: &str, cursor: usize, selection: Option<(usize, usize)>) {
let chars: Vec<char> = text.chars().collect();
let cursor = cursor.min(chars.len());
*EDITOR.lock().unwrap() = Some(EditorState {
chars,
cursor,
selection,
});
}
pub(crate) fn clear_editor_state() {
*EDITOR.lock().unwrap() = None;
}
static BRIDGE_DEX: &[u8] = include_bytes!("../assets/frus_input.dex");
pub(crate) fn install(app: &AndroidApp) {
if BRIDGE.get().is_some() {
return;
}
match try_install(app) {
Ok(bridge) => {
let _ = BRIDGE.set(bridge);
let _ = WAKER.set(Mutex::new(app.create_waker()));
log::info!("input bridge installed (a real InputConnection)");
}
Err(err) => log::warn!("input bridge unavailable ({err}) — falling back to key mode"),
}
}
fn try_install(app: &AndroidApp) -> Result<Bridge, jni::errors::Error> {
let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr() as *mut jni::sys::JavaVM) }?;
let mut env = vm.attach_current_thread_permanently()?;
let activity = unsafe { JObject::from_raw(app.activity_as_ptr() as jni::sys::jobject) };
let buffer =
unsafe { env.new_direct_byte_buffer(BRIDGE_DEX.as_ptr() as *mut u8, BRIDGE_DEX.len()) }?;
let parent = env
.call_method(
&activity,
"getClassLoader",
"()Ljava/lang/ClassLoader;",
&[],
)?
.l()?;
let loader = env.new_object(
"dalvik/system/InMemoryDexClassLoader",
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
&[JValue::Object(&buffer), JValue::Object(&parent)],
)?;
crate::android_system_bars::install(&mut env, &loader, &activity);
crate::android_clipboard::install(&mut env, &loader, &activity);
let name = env.new_string("dev.frus.input.FrusTextBridge")?;
let class_obj = env
.call_method(
&loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&name)],
)?
.l()?;
let class: JClass = class_obj.into();
use jni::NativeMethod;
let method = |name: &str, sig: &str, ptr: *mut std::ffi::c_void| NativeMethod {
name: name.into(),
sig: sig.into(),
fn_ptr: ptr,
};
env.register_native_methods(
&class,
&[
method(
"nativeCommit",
"(Ljava/lang/String;)V",
native_commit as *mut _,
),
method(
"nativeSetComposing",
"(Ljava/lang/String;)V",
native_set_composing as *mut _,
),
method(
"nativeSetComposingRegion",
"(II)V",
native_set_composing_region as *mut _,
),
method(
"nativeFinishComposing",
"()V",
native_finish_composing as *mut _,
),
method("nativeDelete", "(II)V", native_delete as *mut _),
method("nativeEditorAction", "(I)V", native_editor_action as *mut _),
method("nativeKey", "(IZII)Z", native_key as *mut _),
method(
"nativeTextBeforeCursor",
"(I)Ljava/lang/String;",
native_text_before as *mut _,
),
method(
"nativeTextAfterCursor",
"(I)Ljava/lang/String;",
native_text_after as *mut _,
),
method(
"nativeSelectedText",
"()Ljava/lang/String;",
native_selected_text as *mut _,
),
method(
"nativeAutofill",
"(ILjava/lang/String;)V",
crate::android_autofill::native_autofill as *mut _,
),
],
)?;
env.call_static_method(
&class,
"install",
"(Landroid/app/Activity;)V",
&[JValue::Object(&activity)],
)?;
let class_ref = env.new_global_ref(&class)?;
let activity_ref = env.new_global_ref(&activity)?;
Ok(Bridge {
vm,
class: class_ref,
activity: activity_ref,
})
}
pub(crate) fn installed() -> bool {
BRIDGE.get().is_some()
}
pub(crate) fn with_bridge(
what: &str,
call: impl FnOnce(&mut JNIEnv, &JClass, &JObject) -> jni::errors::Result<()>,
) {
let Some(bridge) = BRIDGE.get() else {
return;
};
let Ok(mut env) = bridge.vm.attach_current_thread_permanently() else {
return;
};
let class: &JClass = bridge.class.as_obj().into();
if let Err(err) = call(&mut env, class, bridge.activity.as_obj()) {
let _ = env.exception_clear();
log::warn!("input bridge: {what} failed ({err})");
}
}
pub(crate) fn wake() {
if let Some(window) = WINDOW.lock().unwrap().as_ref() {
window.request_redraw();
} else if let Some(waker) = WAKER.get() {
waker.lock().unwrap().wake();
}
}
static WINDOW: Mutex<Option<std::sync::Arc<winit::window::Window>>> = Mutex::new(None);
pub(crate) fn set_window(window: Option<std::sync::Arc<winit::window::Window>>) {
*WINDOW.lock().unwrap() = window;
}
pub(crate) fn start_input(ime: frus_widgets::Ime) {
let Some(bridge) = BRIDGE.get() else {
return;
};
let input_type = ime.android_input_type();
let ime_options = ime.action.android_ime_options();
let result = bridge
.vm
.attach_current_thread_permanently()
.and_then(|mut env| {
let class: &JClass = bridge.class.as_obj().into();
env.call_static_method(
class,
"startInput",
"(Landroid/app/Activity;II)V",
&[
JValue::Object(bridge.activity.as_obj()),
JValue::Int(input_type),
JValue::Int(ime_options),
],
)
.map(|_| ())
});
if let Err(err) = result {
log::warn!("input bridge: startInput failed ({err})");
}
}
pub(crate) fn update_selection(sel_start: i32, sel_end: i32, cand_start: i32, cand_end: i32) {
let Some(bridge) = BRIDGE.get() else {
return;
};
let result = bridge
.vm
.attach_current_thread_permanently()
.and_then(|mut env| {
let class: &JClass = bridge.class.as_obj().into();
env.call_static_method(
class,
"updateSelection",
"(Landroid/app/Activity;IIII)V",
&[
JValue::Object(bridge.activity.as_obj()),
JValue::Int(sel_start),
JValue::Int(sel_end),
JValue::Int(cand_start),
JValue::Int(cand_end),
],
)
.map(|_| ())
});
if let Err(err) = result {
log::warn!("input bridge: updateSelection failed ({err})");
}
}
pub(crate) fn stop_input() {
call_bridge("stopInput");
}
fn call_bridge(method: &str) {
let Some(bridge) = BRIDGE.get() else {
return;
};
let result = bridge
.vm
.attach_current_thread_permanently()
.and_then(|mut env| {
let class: &JClass = bridge.class.as_obj().into();
env.call_static_method(
class,
method,
"(Landroid/app/Activity;)V",
&[JValue::Object(bridge.activity.as_obj())],
)
.map(|_| ())
});
if let Err(err) = result {
log::warn!("input bridge: {method} failed ({err})");
}
}
pub(crate) fn drain() -> Vec<ImeEvent> {
std::mem::take(&mut QUEUE.lock().unwrap())
}
fn push(event: ImeEvent) {
QUEUE.lock().unwrap().push(event);
wake();
}
fn jstring_to_string(env: &mut JNIEnv, text: &JString) -> String {
env.get_string(text).map(Into::into).unwrap_or_default()
}
extern "system" fn native_commit(mut env: JNIEnv, _class: JClass, text: JString) {
let text = jstring_to_string(&mut env, &text);
push(ImeEvent::Commit(text));
}
extern "system" fn native_set_composing(mut env: JNIEnv, _class: JClass, text: JString) {
let text = jstring_to_string(&mut env, &text);
push(ImeEvent::Composing(text));
}
extern "system" fn native_set_composing_region(
_env: JNIEnv,
_class: JClass,
start: jint,
end: jint,
) {
push(ImeEvent::ComposingRegion {
start: start.max(0) as u32,
end: end.max(0) as u32,
});
}
extern "system" fn native_finish_composing(_env: JNIEnv, _class: JClass) {
push(ImeEvent::FinishComposing);
}
extern "system" fn native_delete(_env: JNIEnv, _class: JClass, before: jint, after: jint) {
push(ImeEvent::Delete {
before: before.max(0) as u32,
after: after.max(0) as u32,
});
}
extern "system" fn native_editor_action(_env: JNIEnv, _class: JClass, _action: jint) {
push(ImeEvent::Action);
}
extern "system" fn native_key(
_env: JNIEnv,
_class: JClass,
code: jint,
down: jboolean,
unicode: jint,
_meta: jint,
) -> jboolean {
const KEYCODE_ENTER: jint = 66;
const KEYCODE_DEL: jint = 67;
if down != JNI_TRUE {
return matches!(code, KEYCODE_ENTER | KEYCODE_DEL) as jboolean | (unicode > 0) as jboolean;
}
let handled = matches!(code, KEYCODE_ENTER | KEYCODE_DEL) || unicode > 0;
if handled {
push(ImeEvent::Key {
code,
unicode: unicode.max(0) as u32,
});
}
handled as jboolean
}
fn to_jstring(env: &JNIEnv, text: &str) -> jni::sys::jstring {
env.new_string(text)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut())
}
extern "system" fn native_text_before(env: JNIEnv, _class: JClass, n: jint) -> jni::sys::jstring {
let guard = EDITOR.lock().unwrap();
let text = guard
.as_ref()
.map(|s| {
let n = (n.max(0) as usize).min(s.cursor);
s.chars[s.cursor - n..s.cursor].iter().collect::<String>()
})
.unwrap_or_default();
to_jstring(&env, &text)
}
extern "system" fn native_text_after(env: JNIEnv, _class: JClass, n: jint) -> jni::sys::jstring {
let guard = EDITOR.lock().unwrap();
let text = guard
.as_ref()
.map(|s| {
let end = (s.cursor + n.max(0) as usize).min(s.chars.len());
s.chars[s.cursor..end].iter().collect::<String>()
})
.unwrap_or_default();
to_jstring(&env, &text)
}
extern "system" fn native_selected_text(env: JNIEnv, _class: JClass) -> jni::sys::jstring {
let guard = EDITOR.lock().unwrap();
let text = guard
.as_ref()
.and_then(|s| {
let (a, b) = s.selection?;
let (a, b) = (a.min(s.chars.len()), b.min(s.chars.len()));
(a < b).then(|| s.chars[a..b].iter().collect::<String>())
})
.unwrap_or_default();
to_jstring(&env, &text)
}