Skip to main content

android_activity/native_activity/
mod.rs

1use std::collections::HashMap;
2use std::marker::PhantomData;
3use std::panic::AssertUnwindSafe;
4use std::ptr;
5use std::sync::{Arc, Mutex, RwLock, Weak};
6use std::time::Duration;
7
8use jni::objects::JObject;
9use jni::JavaVM;
10use libc::c_void;
11use log::{error, trace};
12use ndk::input_queue::InputQueue;
13use ndk::{asset::AssetManager, native_window::NativeWindow};
14
15use crate::error::InternalResult;
16use crate::main_callbacks::MainCallbacks;
17use crate::sdk::{Activity, Context, InputMethodManager};
18use crate::{
19    util, AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect,
20    WindowManagerFlags,
21};
22
23pub mod input;
24use crate::input::{
25    device_key_character_map, Axis, ImeOptions, InputType, KeyCharacterMap, TextInputAction,
26    TextInputState, TextSpan,
27};
28
29mod glue;
30use self::glue::NativeActivityGlue;
31
32pub const LOOPER_ID_MAIN: libc::c_int = 1;
33pub const LOOPER_ID_INPUT: libc::c_int = 2;
34//pub const LOOPER_ID_USER: ::std::os::raw::c_uint = 3;
35
36/// An interface for saving application state during [MainEvent::SaveState] events
37///
38/// This interface is only available temporarily while handling a [MainEvent::SaveState] event.
39#[derive(Debug)]
40pub struct StateSaver<'a> {
41    app: &'a AndroidAppInner,
42}
43
44impl<'a> StateSaver<'a> {
45    /// Stores the given `state` such that it will be available to load the next
46    /// time that the application resumes.
47    pub fn store(&self, state: &'a [u8]) {
48        self.app.native_activity.set_saved_state(state);
49    }
50}
51
52/// An interface for loading application state during [MainEvent::Resume] events
53///
54/// This interface is only available temporarily while handling a [MainEvent::Resume] event.
55#[derive(Debug)]
56pub struct StateLoader<'a> {
57    app: &'a AndroidAppInner,
58}
59impl StateLoader<'_> {
60    /// Returns whatever state was saved during the last [MainEvent::SaveState] event or `None`
61    pub fn load(&self) -> Option<Vec<u8>> {
62        self.app.native_activity.saved_state()
63    }
64}
65
66impl AndroidApp {
67    pub(crate) fn new(
68        jvm: JavaVM,
69        main_looper: ndk::looper::ForeignLooper,
70        main_callbacks: MainCallbacks,
71        app_asset_manager: AssetManager,
72        native_activity: NativeActivityGlue,
73        jni_activity: &JObject,
74    ) -> Self {
75        jvm.with_local_frame(10, |env| -> jni::errors::Result<_> {
76            if let Err(err) = crate::sdk::jni_init(env) {
77                panic!("Failed to init JNI bindings: {err:?}");
78            };
79
80            let looper = unsafe {
81                let ptr = ndk_sys::ALooper_prepare(
82                    ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
83                );
84                ndk::looper::ForeignLooper::from_ptr(ptr::NonNull::new(ptr).unwrap())
85            };
86
87            // The global reference in `ANativeActivity` is only guaranteed to be valid until
88            // `onDestroy` returns, so we create our own global reference that we can guarantee will
89            // remain valid until `AndroidApp` is dropped.
90            let activity = env
91                .new_global_ref(jni_activity)
92                .expect("Failed to create global ref for Activity instance");
93
94            let app = Self {
95                inner: Arc::new(RwLock::new(AndroidAppInner {
96                    jvm: jvm.clone(),
97                    main_looper,
98                    main_callbacks,
99                    app_asset_manager,
100                    native_activity,
101                    activity,
102                    looper,
103                    key_maps: Mutex::new(HashMap::new()),
104                    input_receiver: Mutex::new(None),
105                })),
106            };
107
108            {
109                let guard = app.inner.write().unwrap();
110
111                let main_fd = guard.native_activity.cmd_read_fd();
112                unsafe {
113                    ndk_sys::ALooper_addFd(
114                        guard.looper.ptr().as_ptr(),
115                        main_fd,
116                        LOOPER_ID_MAIN,
117                        ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
118                        None,
119                        //&mut guard.cmd_poll_source as *mut _ as *mut _);
120                        ptr::null_mut(),
121                    );
122                }
123            }
124
125            Ok(app)
126        })
127        .expect("Failed to create AndroidApp instance")
128    }
129}
130
131#[derive(Debug)]
132pub(crate) struct AndroidAppInner {
133    pub(crate) jvm: JavaVM,
134
135    pub(crate) native_activity: NativeActivityGlue,
136
137    activity: jni::refs::Global<jni::objects::JObject<'static>>,
138
139    main_callbacks: MainCallbacks,
140
141    /// Looper associated with the Rust `android_main` thread
142    looper: ndk::looper::ForeignLooper,
143
144    /// Looper associated with the activity's Java main thread, sometimes called
145    /// the UI thread.
146    main_looper: ndk::looper::ForeignLooper,
147
148    /// A table of `KeyCharacterMap`s per `InputDevice` ID
149    /// these are used to be able to map key presses to unicode
150    /// characters
151    key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
152
153    /// While an app is reading input events it holds an
154    /// InputReceiver reference which we track to ensure
155    /// we don't hand out more than one receiver at a time
156    input_receiver: Mutex<Option<Weak<InputReceiver>>>,
157
158    /// An `AAssetManager` wrapper for the `Application` `AssetManager`
159    /// Note: `AAssetManager_fromJava` specifies that the pointer is only valid
160    /// while we hold a global reference to the `AssetManager` Java object
161    /// to ensure it is not garbage collected. This AssetManager comes from
162    /// a OnceLock initialization that leaks a single global JNI reference
163    /// to guarantee that it remains valid for the lifetime of the process.
164    app_asset_manager: AssetManager,
165}
166
167impl AndroidAppInner {
168    pub(crate) fn activity_as_ptr(&self) -> *mut c_void {
169        // Note: The global reference in `ANativeActivity::clazz` (misnomer for instance reference)
170        // is only guaranteed to be valid until `onDestroy` returns, so we have our own global
171        // reference that we can instead guarantee will remain valid until `AndroidApp` is dropped.
172        self.activity.as_raw() as *mut c_void
173    }
174
175    pub(crate) fn looper_as_ptr(&self) -> *mut ndk_sys::ALooper {
176        self.looper.ptr().as_ptr()
177    }
178
179    pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
180        self.main_looper.clone()
181    }
182
183    pub fn native_window(&self) -> Option<NativeWindow> {
184        self.native_activity.mutex.lock().unwrap().window.clone()
185    }
186
187    pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
188    where
189        F: FnMut(PollEvent<'_>),
190    {
191        trace!("poll_events");
192
193        unsafe {
194            let mut fd: i32 = 0;
195            let mut events: i32 = 0;
196            let mut source: *mut c_void = ptr::null_mut();
197
198            let timeout_milliseconds = if let Some(timeout) = timeout {
199                timeout.as_millis() as i32
200            } else {
201                -1
202            };
203
204            trace!("Calling ALooper_pollOnce, timeout = {timeout_milliseconds}");
205            assert_eq!(
206                ndk_sys::ALooper_forThread(),
207                self.looper_as_ptr(),
208                "Application tried to poll events from non-main thread"
209            );
210            let id = ndk_sys::ALooper_pollOnce(
211                timeout_milliseconds,
212                &mut fd,
213                &mut events,
214                &mut source as *mut *mut c_void,
215            );
216            trace!("pollOnce id = {id}");
217            match id {
218                ndk_sys::ALOOPER_POLL_WAKE => {
219                    trace!("ALooper_pollOnce returned POLL_WAKE");
220                    callback(PollEvent::Wake);
221                }
222                ndk_sys::ALOOPER_POLL_CALLBACK => {
223                    // ALooper_pollOnce is documented to handle all callback sources internally so it should
224                    // never return a _CALLBACK source id...
225                    error!("Spurious ALOOPER_POLL_CALLBACK from ALooper_pollOnce() (ignored)");
226                }
227                ndk_sys::ALOOPER_POLL_TIMEOUT => {
228                    trace!("ALooper_pollOnce returned POLL_TIMEOUT");
229                    callback(PollEvent::Timeout);
230                }
231                ndk_sys::ALOOPER_POLL_ERROR => {
232                    // If we have an IO error with our pipe to the main Java thread that's surely
233                    // not something we can recover from
234                    panic!("ALooper_pollOnce returned POLL_ERROR");
235                }
236                id if id >= 0 => {
237                    match id {
238                        LOOPER_ID_MAIN => {
239                            trace!("ALooper_pollOnce returned ID_MAIN");
240                            if let Some(ipc_cmd) = self.native_activity.read_cmd() {
241                                let main_cmd = match ipc_cmd {
242                                    // We don't forward info about the AInputQueue to apps since it's
243                                    // an implementation details that's also not compatible with
244                                    // GameActivity
245                                    glue::AppCmd::InputQueueChanged => None,
246
247                                    glue::AppCmd::InitWindow => Some(MainEvent::InitWindow {}),
248                                    glue::AppCmd::TermWindow => Some(MainEvent::TerminateWindow {}),
249                                    glue::AppCmd::WindowResized => {
250                                        Some(MainEvent::WindowResized {})
251                                    }
252                                    glue::AppCmd::WindowRedrawNeeded => {
253                                        Some(MainEvent::RedrawNeeded {})
254                                    }
255                                    glue::AppCmd::ContentRectChanged => {
256                                        Some(MainEvent::ContentRectChanged {})
257                                    }
258                                    glue::AppCmd::GainedFocus => Some(MainEvent::GainedFocus),
259                                    glue::AppCmd::LostFocus => Some(MainEvent::LostFocus),
260                                    glue::AppCmd::ConfigChanged => {
261                                        Some(MainEvent::ConfigChanged {})
262                                    }
263                                    glue::AppCmd::LowMemory => Some(MainEvent::LowMemory),
264                                    glue::AppCmd::Start => Some(MainEvent::Start),
265                                    glue::AppCmd::Resume => Some(MainEvent::Resume {
266                                        loader: StateLoader { app: self },
267                                    }),
268                                    glue::AppCmd::SaveState => Some(MainEvent::SaveState {
269                                        saver: StateSaver { app: self },
270                                    }),
271                                    glue::AppCmd::Pause => Some(MainEvent::Pause),
272                                    glue::AppCmd::Stop => Some(MainEvent::Stop),
273                                    glue::AppCmd::Destroy => Some(MainEvent::Destroy),
274                                };
275
276                                trace!("Calling pre_exec_cmd({ipc_cmd:#?})");
277                                self.native_activity.pre_exec_cmd(
278                                    ipc_cmd,
279                                    self.looper_as_ptr(),
280                                    LOOPER_ID_INPUT,
281                                );
282
283                                if let Some(main_cmd) = main_cmd {
284                                    trace!("Invoking callback for ID_MAIN command = {main_cmd:?}");
285                                    callback(PollEvent::Main(main_cmd));
286                                }
287
288                                trace!("Calling post_exec_cmd({ipc_cmd:#?})");
289                                self.native_activity.post_exec_cmd(ipc_cmd);
290                            }
291                        }
292                        LOOPER_ID_INPUT => {
293                            trace!("ALooper_pollOnce returned ID_INPUT");
294
295                            // To avoid spamming the application with event loop iterations notifying them of
296                            // input events then we only send one `InputAvailable` per iteration of input
297                            // handling. We re-attach the looper when the application calls
298                            // `AndroidApp::input_events()`
299                            self.native_activity.detach_input_queue_from_looper();
300                            callback(PollEvent::Main(MainEvent::InputAvailable))
301                        }
302                        _ => {
303                            error!("Ignoring spurious ALooper event source: id = {id}, fd = {fd}, events = {events:?}, data = {source:?}");
304                        }
305                    }
306                }
307                _ => {
308                    error!("Spurious ALooper_pollOnce return value {id} (ignored)");
309                }
310            }
311        }
312    }
313
314    pub fn create_waker(&self) -> AndroidAppWaker {
315        // Safety: we know that the looper is a valid, non-null pointer
316        unsafe { AndroidAppWaker::new(self.looper_as_ptr()) }
317    }
318
319    pub fn run_on_java_main_thread<F>(&self, f: Box<F>)
320    where
321        F: FnOnce() + Send + 'static,
322    {
323        self.main_callbacks.run_on_java_main_thread(f);
324    }
325
326    pub fn config(&self) -> ConfigurationRef {
327        self.native_activity.config()
328    }
329
330    pub fn content_rect(&self) -> Rect {
331        self.native_activity.content_rect()
332    }
333
334    pub fn asset_manager(&self) -> AssetManager {
335        // Safety: While constructing the AndroidApp we do a OnceLock initialization
336        // where we get the Application AssetManager and leak a single global JNI
337        // reference that guarantees it will not be garbage collected, so we can
338        // safely return the corresponding AAssetManager here.
339        unsafe { AssetManager::from_ptr(self.app_asset_manager.ptr()) }
340    }
341
342    pub fn set_window_flags(
343        &self,
344        add_flags: WindowManagerFlags,
345        remove_flags: WindowManagerFlags,
346    ) {
347        let guard = self.native_activity.mutex.lock().unwrap();
348        let na = guard.activity;
349        if na.is_null() {
350            log::error!("Can't set window flags after NativeActivity has been destroyed");
351            return;
352        }
353
354        let na_mut = na;
355        unsafe {
356            ndk_sys::ANativeActivity_setWindowFlags(
357                na_mut.cast(),
358                add_flags.bits(),
359                remove_flags.bits(),
360            );
361        }
362    }
363
364    // TODO: move into a trait
365    pub fn show_soft_input(&self, show_implicit: bool) {
366        let guard = self.native_activity.mutex.lock().unwrap();
367        let na = guard.activity;
368        if na.is_null() {
369            log::error!("Can't show soft input after NativeActivity has been destroyed");
370            return;
371        }
372
373        // Note: `.attach_current_thread()` will also handle catching any Java exceptions that
374        // might be thrown by the JNI calls we make.
375        let res = self
376            .jvm
377            .attach_current_thread(|env| -> jni::errors::Result<()> {
378                let activity = env.as_cast::<Activity>(self.activity.as_ref())?;
379
380                let ims = Context::INPUT_METHOD_SERVICE(env)?;
381                let im_manager = activity.as_context().get_system_service(env, ims)?;
382                let im_manager = InputMethodManager::cast_local(env, im_manager)?;
383                let jni_window = activity.get_window(env)?;
384                let view = jni_window.get_decor_view(env)?;
385                let flags = if show_implicit {
386                    ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32
387                } else {
388                    0
389                };
390                im_manager.show_soft_input(env, view, flags)?;
391                Ok(())
392            });
393        if let Err(err) = res {
394            log::warn!("Failed to show soft input: {err:?}");
395        }
396    }
397
398    // TODO: move into a trait
399    pub fn hide_soft_input(&self, hide_implicit_only: bool) {
400        let guard = self.native_activity.mutex.lock().unwrap();
401        let na = guard.activity;
402        if na.is_null() {
403            log::error!("Can't hide soft input after NativeActivity has been destroyed");
404            return;
405        }
406
407        // Note: `.attach_current_thread()` will also handle catching any Java exceptions that
408        // might be thrown by the JNI calls we make.
409        let res = self
410            .jvm
411            .attach_current_thread(|env| -> jni::errors::Result<()> {
412                let activity = env.as_cast::<Activity>(self.activity.as_ref())?;
413
414                let ims = Context::INPUT_METHOD_SERVICE(env)?;
415                let imm_obj = activity.as_context().get_system_service(env, ims)?;
416                let imm = InputMethodManager::cast_local(env, imm_obj)?;
417
418                let window = activity.get_window(env)?;
419                let decor = window.get_decor_view(env)?;
420                let token = decor.get_window_token(env)?;
421
422                // HIDE_IMPLICIT_ONLY == 1, HIDE_NOT_ALWAYS == 2
423                let flags = if hide_implicit_only { 1 } else { 0 };
424
425                let _hidden = imm.hide_soft_input_from_window(env, token, flags)?;
426                Ok(())
427            });
428
429        if let Err(err) = res {
430            error!("Failed to hide soft input: {err:?}");
431        }
432    }
433
434    // TODO: move into a trait
435    pub fn text_input_state(&self) -> TextInputState {
436        TextInputState {
437            text: String::new(),
438            selection: TextSpan { start: 0, end: 0 },
439            compose_region: None,
440        }
441    }
442
443    // TODO: move into a trait
444    pub fn set_text_input_state(&self, _state: TextInputState) {
445        // NOP: Unsupported
446    }
447
448    // TODO: move into a trait
449    pub fn set_ime_editor_info(
450        &self,
451        _input_type: InputType,
452        _action: TextInputAction,
453        _options: ImeOptions,
454    ) {
455        // NOP: Unsupported
456    }
457
458    pub fn device_key_character_map(&self, device_id: i32) -> InternalResult<KeyCharacterMap> {
459        let mut guard = self.key_maps.lock().unwrap();
460
461        let key_map = match guard.entry(device_id) {
462            std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
463            std::collections::hash_map::Entry::Vacant(vacant) => {
464                let character_map = device_key_character_map(self.jvm.clone(), device_id)?;
465                vacant.insert(character_map.clone());
466                character_map
467            }
468        };
469
470        Ok(key_map)
471    }
472
473    pub fn enable_motion_axis(&self, _axis: Axis) {
474        // NOP - The InputQueue API doesn't let us optimize which axis values are read
475    }
476
477    pub fn disable_motion_axis(&self, _axis: Axis) {
478        // NOP - The InputQueue API doesn't let us optimize which axis values are read
479    }
480
481    pub fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
482        let mut guard = self.input_receiver.lock().unwrap();
483
484        if let Some(receiver) = &*guard {
485            if receiver.strong_count() > 0 {
486                return Err(crate::error::InternalAppError::InputUnavailable);
487            }
488        }
489        *guard = None;
490
491        // Get the InputQueue for the NativeActivity (if there is one) and also ensure
492        // the queue is re-attached to our event Looper (so new input events will again
493        // trigger a wake up)
494        let queue = self
495            .native_activity
496            .looper_attached_input_queue(self.looper_as_ptr(), LOOPER_ID_INPUT);
497
498        // Note: we don't treat it as an error if there is no queue, so if applications
499        // iterate input before a queue has been created (e.g. before onStart) then
500        // it will simply behave like there are no events available currently.
501        let receiver = Arc::new(InputReceiver { queue });
502
503        *guard = Some(Arc::downgrade(&receiver));
504        Ok(receiver)
505    }
506
507    pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
508        let guard = self.native_activity.mutex.lock().unwrap();
509        let na = guard.activity;
510        if na.is_null() {
511            log::error!("Can't get internal data path after NativeActivity has been destroyed");
512            return None;
513        }
514        unsafe { util::try_get_path_from_ptr((*na).internalDataPath) }
515    }
516
517    pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
518        let guard = self.native_activity.mutex.lock().unwrap();
519        let na = guard.activity;
520        if na.is_null() {
521            log::error!("Can't get external data path after NativeActivity has been destroyed");
522            return None;
523        }
524        unsafe { util::try_get_path_from_ptr((*na).externalDataPath) }
525    }
526
527    pub fn obb_path(&self) -> Option<std::path::PathBuf> {
528        let guard = self.native_activity.mutex.lock().unwrap();
529        let na = guard.activity;
530        if na.is_null() {
531            log::error!("Can't get OBB path after NativeActivity has been destroyed");
532            return None;
533        }
534        unsafe { util::try_get_path_from_ptr((*na).obbPath) }
535    }
536}
537
538#[derive(Debug)]
539pub(crate) struct InputReceiver {
540    queue: Option<InputQueue>,
541}
542
543impl From<Arc<InputReceiver>> for InputIteratorInner<'_> {
544    fn from(receiver: Arc<InputReceiver>) -> Self {
545        Self {
546            receiver,
547            _lifetime: PhantomData,
548        }
549    }
550}
551
552pub(crate) struct InputIteratorInner<'a> {
553    // Held to maintain exclusive access to buffered input events
554    receiver: Arc<InputReceiver>,
555    _lifetime: PhantomData<&'a ()>,
556}
557
558impl InputIteratorInner<'_> {
559    pub(crate) fn next<F>(&self, callback: F) -> bool
560    where
561        F: FnOnce(&input::InputEvent) -> InputStatus,
562    {
563        let Some(queue) = &self.receiver.queue else {
564            log::trace!("no queue available for events");
565            return false;
566        };
567
568        // Note: we basically ignore errors from event() currently. Looking at the source code for
569        // Android's InputQueue, the only error that can be returned here is 'WOULD_BLOCK', which we
570        // want to just treat as meaning the queue is empty.
571        //
572        // ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
573        //
574        if let Ok(Some(ndk_event)) = queue.event() {
575            log::trace!("queue: got event: {ndk_event:?}");
576
577            if let Some(ndk_event) = queue.pre_dispatch(ndk_event) {
578                let event = match ndk_event {
579                    ndk::event::InputEvent::MotionEvent(e) => {
580                        input::InputEvent::MotionEvent(input::MotionEvent::new(e))
581                    }
582                    ndk::event::InputEvent::KeyEvent(e) => {
583                        input::InputEvent::KeyEvent(input::KeyEvent::new(e))
584                    }
585                    _ => todo!("NDK added a new type"),
586                };
587
588                // `finish_event` needs to be called for each event otherwise
589                // the app would likely get an ANR
590                let result = std::panic::catch_unwind(AssertUnwindSafe(|| callback(&event)));
591
592                let ndk_event = match event {
593                    input::InputEvent::MotionEvent(e) => {
594                        ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
595                    }
596                    input::InputEvent::KeyEvent(e) => {
597                        ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
598                    }
599                    _ => unreachable!(),
600                };
601
602                let handled = match result {
603                    Ok(handled) => handled,
604                    Err(payload) => {
605                        log::error!("Calling `finish_event` after panic in input event handler, to try and avoid being killed via an ANR");
606                        queue.finish_event(ndk_event, false);
607                        std::panic::resume_unwind(payload);
608                    }
609                };
610
611                log::trace!("queue: finishing event");
612                queue.finish_event(ndk_event, handled == InputStatus::Handled);
613            }
614
615            true
616        } else {
617            log::trace!("queue: no more events");
618            false
619        }
620    }
621}