/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2025 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use miniquad::native::android::{self, ndk_sys, ndk_utils}; use parking_lot::Mutex as SyncMutex; use std::{collections::HashMap, path::PathBuf, sync::LazyLock}; use crate::AndroidSuggestEvent; macro_rules! call_mainactivity_int_method { ($method:expr, $sig:expr $(, $args:expr)*) => {{ unsafe { let env = android::attach_jni_env(); ndk_utils::call_int_method!(env, android::ACTIVITY, $method, $sig $(, $args)*) } }}; } macro_rules! call_mainactivity_str_method { ($method:expr) => {{ unsafe { let env = android::attach_jni_env(); let text = ndk_utils::call_object_method!( env, android::ACTIVITY, $method, "()Ljava/lang/String;" ); ndk_utils::get_utf_str!(env, text) } }}; } macro_rules! call_mainactivity_float_method { ($method:expr) => {{ unsafe { let env = android::attach_jni_env(); ndk_utils::call_method!(CallFloatMethod, env, android::ACTIVITY, $method, "()F") } }}; } macro_rules! call_mainactivity_bool_method { ($method:expr) => {{ unsafe { let env = android::attach_jni_env(); ndk_utils::call_method!(CallBooleanMethod, env, android::ACTIVITY, $method, "()Z") != 0u8 } }}; } struct GlobalData { senders: HashMap>, next_id: usize, } fn send(id: usize, ev: AndroidSuggestEvent) { let globals = &GLOBALS.lock(); let Some(sender) = globals.senders.get(&id) else { warn!(target: "android", "Unknown composer_id={id} discard ev: {ev:?}"); return }; let _ = sender.try_send(ev); } unsafe impl Send for GlobalData {} unsafe impl Sync for GlobalData {} static GLOBALS: LazyLock> = LazyLock::new(|| SyncMutex::new(GlobalData { senders: HashMap::new(), next_id: 0 })); #[no_mangle] pub unsafe extern "C" fn Java_darkfi_darkfi_1app_MainActivity_onInitEdit( _env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, ) { assert!(id >= 0); let id = id as usize; send(id, AndroidSuggestEvent::Init); } #[no_mangle] pub unsafe extern "C" fn Java_autosuggest_InvisibleInputView_onCreateInputConnect( _env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, ) { assert!(id >= 0); let id = id as usize; send(id, AndroidSuggestEvent::CreateInputConnect); } #[no_mangle] pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose( env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, text: ndk_sys::jobject, cursor_pos: ndk_sys::jint, is_commit: ndk_sys::jboolean, ) { assert!(id >= 0); let id = id as usize; let text = ndk_utils::get_utf_str!(env, text); send( id, AndroidSuggestEvent::Compose { text: text.to_string(), cursor_pos, is_commit: is_commit == 1, }, ); } #[no_mangle] pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion( _env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, start: ndk_sys::jint, end: ndk_sys::jint, ) { assert!(id >= 0); let id = id as usize; send(id, AndroidSuggestEvent::ComposeRegion { start: start as usize, end: end as usize }); } #[no_mangle] pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onFinishCompose( _env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, ) { assert!(id >= 0); let id = id as usize; send(id, AndroidSuggestEvent::FinishCompose); } #[no_mangle] pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onDeleteSurroundingText( _env: *mut ndk_sys::JNIEnv, _: ndk_sys::jobject, id: ndk_sys::jint, left: ndk_sys::jint, right: ndk_sys::jint, ) { assert!(id >= 0); let id = id as usize; send( id, AndroidSuggestEvent::DeleteSurroundingText { left: left as usize, right: right as usize }, ); } pub fn create_composer(sender: async_channel::Sender) -> usize { let composer_id = { let mut globals = GLOBALS.lock(); let id = globals.next_id; globals.next_id += 1; globals.senders.insert(id, sender); id }; unsafe { let env = android::attach_jni_env(); ndk_utils::call_void_method!(env, android::ACTIVITY, "createComposer", "(I)V", composer_id); } composer_id } pub fn focus(id: usize) -> Option<()> { let is_success = unsafe { let env = android::attach_jni_env(); ndk_utils::call_bool_method!(env, android::ACTIVITY, "focus", "(I)Z", id as i32) }; if is_success == 0u8 { None } else { Some(()) } } pub fn unfocus(id: usize) -> Option<()> { let is_success = unsafe { let env = android::attach_jni_env(); ndk_utils::call_bool_method!(env, android::ACTIVITY, "unfocus", "(I)Z", id as i32) }; if is_success == 0u8 { None } else { Some(()) } } pub fn set_text(id: usize, text: &str) -> Option<()> { let ctext = std::ffi::CString::new(text).unwrap(); let is_success = unsafe { let env = android::attach_jni_env(); let new_string_utf = (**env).NewStringUTF.unwrap(); let jtext = new_string_utf(env, ctext.as_ptr()); let delete_local_ref = (**env).DeleteLocalRef.unwrap(); let res = ndk_utils::call_bool_method!( env, android::ACTIVITY, "setText", "(ILjava/lang/String;)Z", id as i32, jtext ); delete_local_ref(env, jtext); res }; if is_success == 0u8 { None } else { Some(()) } } pub fn set_selection(id: usize, select_start: usize, select_end: usize) -> Option<()> { //trace!(target: "android", "set_selection({id}, {select_start}, {select_end})"); let is_success = unsafe { let env = android::attach_jni_env(); ndk_utils::call_bool_method!( env, android::ACTIVITY, "setSelection", "(III)Z", id as i32, select_start as i32, select_end as i32 ) }; if is_success == 0u8 { None } else { Some(()) } } pub fn commit_text(id: usize, text: &str) -> Option<()> { let ctext = std::ffi::CString::new(text).unwrap(); let is_success = unsafe { let env = android::attach_jni_env(); let new_string_utf = (**env).NewStringUTF.unwrap(); let delete_local_ref = (**env).DeleteLocalRef.unwrap(); let jtext = new_string_utf(env, ctext.as_ptr()); let res = ndk_utils::call_bool_method!( env, android::ACTIVITY, "commitText", "(ILjava/lang/String;)Z", id as i32, jtext ); delete_local_ref(env, jtext); res }; if is_success == 0u8 { None } else { Some(()) } } pub struct Editable { pub buffer: String, pub select_start: usize, pub select_end: usize, pub compose_start: Option, pub compose_end: Option, } pub fn get_editable(id: usize) -> Option { //trace!(target: "android", "get_editable({id})"); unsafe { let env = android::attach_jni_env(); let input_view = ndk_utils::call_object_method!( env, android::ACTIVITY, "getInputView", "(I)Lautosuggest/InvisibleInputView;", id as i32 ); if input_view.is_null() { return None } let buffer = ndk_utils::call_object_method!(env, input_view, "rawText", "()Ljava/lang/String;"); assert!(!buffer.is_null()); let buffer = ndk_utils::get_utf_str!(env, buffer).to_string(); let select_start = ndk_utils::call_int_method!(env, input_view, "getSelectionStart", "()I"); let select_end = ndk_utils::call_int_method!(env, input_view, "getSelectionEnd", "()I"); let compose_start = ndk_utils::call_int_method!(env, input_view, "getComposeStart", "()I"); let compose_end = ndk_utils::call_int_method!(env, input_view, "getComposeEnd", "()I"); assert!(select_start >= 0); assert!(select_end >= 0); assert!(compose_start >= 0 || compose_start == compose_end); assert!(compose_start <= compose_end); Some(Editable { buffer, select_start: select_start as usize, select_end: select_end as usize, compose_start: if compose_start < 0 { None } else { Some(compose_start as usize) }, compose_end: if compose_end < 0 { None } else { Some(compose_end as usize) }, }) } } pub fn get_appdata_path() -> PathBuf { call_mainactivity_str_method!("getAppDataPath").into() } pub fn get_external_storage_path() -> PathBuf { call_mainactivity_str_method!("getExternalStoragePath").into() } pub fn get_keyboard_height() -> usize { call_mainactivity_int_method!("getKeyboardHeight", "()I") as usize } pub fn get_screen_density() -> f32 { call_mainactivity_float_method!("getScreenDensity") } pub fn is_ime_visible() -> bool { call_mainactivity_bool_method!("isImeVisible") }