android.rs
1 /* This file is part of DarkFi (https://dark.fi) 2 * 3 * Copyright (C) 2020-2025 Dyne.org foundation 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation, either version 3 of the 8 * License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Affero General Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 use miniquad::native::android::{self, ndk_sys, ndk_utils}; 20 use parking_lot::Mutex as SyncMutex; 21 use std::{collections::HashMap, path::PathBuf, sync::LazyLock}; 22 23 use crate::AndroidSuggestEvent; 24 25 macro_rules! call_mainactivity_int_method { 26 ($method:expr, $sig:expr $(, $args:expr)*) => {{ 27 unsafe { 28 let env = android::attach_jni_env(); 29 ndk_utils::call_int_method!(env, android::ACTIVITY, $method, $sig $(, $args)*) 30 } 31 }}; 32 } 33 macro_rules! call_mainactivity_str_method { 34 ($method:expr) => {{ 35 unsafe { 36 let env = android::attach_jni_env(); 37 let text = ndk_utils::call_object_method!( 38 env, 39 android::ACTIVITY, 40 $method, 41 "()Ljava/lang/String;" 42 ); 43 ndk_utils::get_utf_str!(env, text) 44 } 45 }}; 46 } 47 macro_rules! call_mainactivity_float_method { 48 ($method:expr) => {{ 49 unsafe { 50 let env = android::attach_jni_env(); 51 ndk_utils::call_method!(CallFloatMethod, env, android::ACTIVITY, $method, "()F") 52 } 53 }}; 54 } 55 macro_rules! call_mainactivity_bool_method { 56 ($method:expr) => {{ 57 unsafe { 58 let env = android::attach_jni_env(); 59 ndk_utils::call_method!(CallBooleanMethod, env, android::ACTIVITY, $method, "()Z") != 60 0u8 61 } 62 }}; 63 } 64 65 struct GlobalData { 66 senders: HashMap<usize, async_channel::Sender<AndroidSuggestEvent>>, 67 next_id: usize, 68 } 69 70 fn send(id: usize, ev: AndroidSuggestEvent) { 71 let globals = &GLOBALS.lock(); 72 let Some(sender) = globals.senders.get(&id) else { 73 warn!(target: "android", "Unknown composer_id={id} discard ev: {ev:?}"); 74 return 75 }; 76 let _ = sender.try_send(ev); 77 } 78 79 unsafe impl Send for GlobalData {} 80 unsafe impl Sync for GlobalData {} 81 82 static GLOBALS: LazyLock<SyncMutex<GlobalData>> = 83 LazyLock::new(|| SyncMutex::new(GlobalData { senders: HashMap::new(), next_id: 0 })); 84 85 #[no_mangle] 86 pub unsafe extern "C" fn Java_darkfi_darkfi_1app_MainActivity_onInitEdit( 87 _env: *mut ndk_sys::JNIEnv, 88 _: ndk_sys::jobject, 89 id: ndk_sys::jint, 90 ) { 91 assert!(id >= 0); 92 let id = id as usize; 93 send(id, AndroidSuggestEvent::Init); 94 } 95 96 #[no_mangle] 97 pub unsafe extern "C" fn Java_autosuggest_InvisibleInputView_onCreateInputConnect( 98 _env: *mut ndk_sys::JNIEnv, 99 _: ndk_sys::jobject, 100 id: ndk_sys::jint, 101 ) { 102 assert!(id >= 0); 103 let id = id as usize; 104 send(id, AndroidSuggestEvent::CreateInputConnect); 105 } 106 107 #[no_mangle] 108 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose( 109 env: *mut ndk_sys::JNIEnv, 110 _: ndk_sys::jobject, 111 id: ndk_sys::jint, 112 text: ndk_sys::jobject, 113 cursor_pos: ndk_sys::jint, 114 is_commit: ndk_sys::jboolean, 115 ) { 116 assert!(id >= 0); 117 let id = id as usize; 118 let text = ndk_utils::get_utf_str!(env, text); 119 send( 120 id, 121 AndroidSuggestEvent::Compose { 122 text: text.to_string(), 123 cursor_pos, 124 is_commit: is_commit == 1, 125 }, 126 ); 127 } 128 #[no_mangle] 129 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion( 130 _env: *mut ndk_sys::JNIEnv, 131 _: ndk_sys::jobject, 132 id: ndk_sys::jint, 133 start: ndk_sys::jint, 134 end: ndk_sys::jint, 135 ) { 136 assert!(id >= 0); 137 let id = id as usize; 138 send(id, AndroidSuggestEvent::ComposeRegion { start: start as usize, end: end as usize }); 139 } 140 #[no_mangle] 141 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onFinishCompose( 142 _env: *mut ndk_sys::JNIEnv, 143 _: ndk_sys::jobject, 144 id: ndk_sys::jint, 145 ) { 146 assert!(id >= 0); 147 let id = id as usize; 148 send(id, AndroidSuggestEvent::FinishCompose); 149 } 150 #[no_mangle] 151 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onDeleteSurroundingText( 152 _env: *mut ndk_sys::JNIEnv, 153 _: ndk_sys::jobject, 154 id: ndk_sys::jint, 155 left: ndk_sys::jint, 156 right: ndk_sys::jint, 157 ) { 158 assert!(id >= 0); 159 let id = id as usize; 160 send( 161 id, 162 AndroidSuggestEvent::DeleteSurroundingText { left: left as usize, right: right as usize }, 163 ); 164 } 165 166 pub fn create_composer(sender: async_channel::Sender<AndroidSuggestEvent>) -> usize { 167 let composer_id = { 168 let mut globals = GLOBALS.lock(); 169 let id = globals.next_id; 170 globals.next_id += 1; 171 globals.senders.insert(id, sender); 172 id 173 }; 174 unsafe { 175 let env = android::attach_jni_env(); 176 ndk_utils::call_void_method!(env, android::ACTIVITY, "createComposer", "(I)V", composer_id); 177 } 178 composer_id 179 } 180 181 pub fn focus(id: usize) -> Option<()> { 182 let is_success = unsafe { 183 let env = android::attach_jni_env(); 184 185 ndk_utils::call_bool_method!(env, android::ACTIVITY, "focus", "(I)Z", id as i32) 186 }; 187 if is_success == 0u8 { 188 None 189 } else { 190 Some(()) 191 } 192 } 193 pub fn unfocus(id: usize) -> Option<()> { 194 let is_success = unsafe { 195 let env = android::attach_jni_env(); 196 197 ndk_utils::call_bool_method!(env, android::ACTIVITY, "unfocus", "(I)Z", id as i32) 198 }; 199 if is_success == 0u8 { 200 None 201 } else { 202 Some(()) 203 } 204 } 205 206 pub fn set_text(id: usize, text: &str) -> Option<()> { 207 let ctext = std::ffi::CString::new(text).unwrap(); 208 let is_success = unsafe { 209 let env = android::attach_jni_env(); 210 211 let new_string_utf = (**env).NewStringUTF.unwrap(); 212 let jtext = new_string_utf(env, ctext.as_ptr()); 213 let delete_local_ref = (**env).DeleteLocalRef.unwrap(); 214 215 let res = ndk_utils::call_bool_method!( 216 env, 217 android::ACTIVITY, 218 "setText", 219 "(ILjava/lang/String;)Z", 220 id as i32, 221 jtext 222 ); 223 delete_local_ref(env, jtext); 224 res 225 }; 226 if is_success == 0u8 { 227 None 228 } else { 229 Some(()) 230 } 231 } 232 233 pub fn set_selection(id: usize, select_start: usize, select_end: usize) -> Option<()> { 234 //trace!(target: "android", "set_selection({id}, {select_start}, {select_end})"); 235 let is_success = unsafe { 236 let env = android::attach_jni_env(); 237 ndk_utils::call_bool_method!( 238 env, 239 android::ACTIVITY, 240 "setSelection", 241 "(III)Z", 242 id as i32, 243 select_start as i32, 244 select_end as i32 245 ) 246 }; 247 if is_success == 0u8 { 248 None 249 } else { 250 Some(()) 251 } 252 } 253 254 pub fn commit_text(id: usize, text: &str) -> Option<()> { 255 let ctext = std::ffi::CString::new(text).unwrap(); 256 let is_success = unsafe { 257 let env = android::attach_jni_env(); 258 259 let new_string_utf = (**env).NewStringUTF.unwrap(); 260 let delete_local_ref = (**env).DeleteLocalRef.unwrap(); 261 262 let jtext = new_string_utf(env, ctext.as_ptr()); 263 264 let res = ndk_utils::call_bool_method!( 265 env, 266 android::ACTIVITY, 267 "commitText", 268 "(ILjava/lang/String;)Z", 269 id as i32, 270 jtext 271 ); 272 delete_local_ref(env, jtext); 273 res 274 }; 275 if is_success == 0u8 { 276 None 277 } else { 278 Some(()) 279 } 280 } 281 282 pub struct Editable { 283 pub buffer: String, 284 pub select_start: usize, 285 pub select_end: usize, 286 pub compose_start: Option<usize>, 287 pub compose_end: Option<usize>, 288 } 289 290 pub fn get_editable(id: usize) -> Option<Editable> { 291 //trace!(target: "android", "get_editable({id})"); 292 unsafe { 293 let env = android::attach_jni_env(); 294 let input_view = ndk_utils::call_object_method!( 295 env, 296 android::ACTIVITY, 297 "getInputView", 298 "(I)Lautosuggest/InvisibleInputView;", 299 id as i32 300 ); 301 if input_view.is_null() { 302 return None 303 } 304 305 let buffer = 306 ndk_utils::call_object_method!(env, input_view, "rawText", "()Ljava/lang/String;"); 307 assert!(!buffer.is_null()); 308 let buffer = ndk_utils::get_utf_str!(env, buffer).to_string(); 309 310 let select_start = ndk_utils::call_int_method!(env, input_view, "getSelectionStart", "()I"); 311 312 let select_end = ndk_utils::call_int_method!(env, input_view, "getSelectionEnd", "()I"); 313 314 let compose_start = ndk_utils::call_int_method!(env, input_view, "getComposeStart", "()I"); 315 316 let compose_end = ndk_utils::call_int_method!(env, input_view, "getComposeEnd", "()I"); 317 318 assert!(select_start >= 0); 319 assert!(select_end >= 0); 320 assert!(compose_start >= 0 || compose_start == compose_end); 321 assert!(compose_start <= compose_end); 322 323 Some(Editable { 324 buffer, 325 select_start: select_start as usize, 326 select_end: select_end as usize, 327 compose_start: if compose_start < 0 { None } else { Some(compose_start as usize) }, 328 compose_end: if compose_end < 0 { None } else { Some(compose_end as usize) }, 329 }) 330 } 331 } 332 333 pub fn get_appdata_path() -> PathBuf { 334 call_mainactivity_str_method!("getAppDataPath").into() 335 } 336 pub fn get_external_storage_path() -> PathBuf { 337 call_mainactivity_str_method!("getExternalStoragePath").into() 338 } 339 340 pub fn get_keyboard_height() -> usize { 341 call_mainactivity_int_method!("getKeyboardHeight", "()I") as usize 342 } 343 344 pub fn get_screen_density() -> f32 { 345 call_mainactivity_float_method!("getScreenDensity") 346 } 347 348 pub fn is_ime_visible() -> bool { 349 call_mainactivity_bool_method!("isImeVisible") 350 }