/ bin / app / src / android.rs
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  }