diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index 1fc4cd0..e7c40b0 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -107,7 +107,11 @@ impl StateLoader<'_> { } impl AndroidApp { - pub(crate) unsafe fn from_ptr(ptr: NonNull, jvm: jni::JavaVM) -> Self { + pub(crate) fn new( + ptr: NonNull, + jvm: jni::JavaVM, + main_looper: ndk::looper::ForeignLooper, + ) -> Self { // We attach to the thread before creating the AndroidApp jvm.with_local_frame(10, |env| -> jni::errors::Result<_> { if let Err(err) = crate::input::jni_init(env) { @@ -117,8 +121,9 @@ impl AndroidApp { // Note: we don't use from_ptr since we don't own the android_app.config // and need to keep in mind that the Drop handler is going to call // AConfiguration_delete() - let config = - Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config)); + let config = unsafe { + Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config)) + }; Ok(Self { inner: Arc::new(RwLock::new(AndroidAppInner { @@ -126,6 +131,7 @@ impl AndroidApp { native_app: NativeAppGlue { ptr }, config: ConfigurationRef::new(config), native_window: Default::default(), + main_looper, key_maps: Mutex::new(HashMap::new()), input_receiver: Mutex::new(None), })), @@ -279,6 +285,10 @@ pub struct AndroidAppInner { config: ConfigurationRef, native_window: RwLock>, + /// Looper associated with the activity's Java main thread, sometimes called + /// the UI thread. + main_looper: ndk::looper::ForeignLooper, + /// A table of `KeyCharacterMap`s per `InputDevice` ID /// these are used to be able to map key presses to unicode /// characters @@ -305,6 +315,10 @@ impl AndroidAppInner { self.native_window.read().unwrap().clone() } + pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper { + self.main_looper.clone() + } + pub fn poll_events(&self, timeout: Option, mut callback: F) where F: FnMut(PollEvent), @@ -981,6 +995,12 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) { }; // Note: At this point we can assume jni::JavaVM::singleton is initialized + let main_looper = unsafe { + ndk::looper::ForeignLooper::from_ptr( + std::ptr::NonNull::new((*native_app).mainLooper).unwrap(), + ) + }; + // Note: the GameActivity implementation will have already attached the main thread to the // JVM before calling _rust_glue_entry so we don't to set the thread name via // attach_current_thread_with_config since that won't actually create a new attachment. @@ -996,7 +1016,8 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) { } unsafe { - let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone()); + let app = + AndroidApp::new(NonNull::new(native_app).unwrap(), jvm.clone(), main_looper); // We want to specifically catch any panic from the application's android_main // so we can finish + destroy the Activity gracefully via the JVM catch_unwind(|| { diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index e4ffe76..fb81dfb 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -562,6 +562,22 @@ impl AndroidApp { self.inner.read().unwrap().native_window() } + /// Returns a [`ndk::looper::ForeignLooper`] associated with the Java + /// main / UI thread. + /// + /// This can be used to register file descriptors that may wake up the + /// Java main / UI thread and optionally run callbacks on that thread. + /// + /// ```ignore + /// # use ndk; + /// # let app: AndroidApp = todo!(); + /// let looper = app.java_main_looper(); + /// looper.add_fd_with_callback(todo!(), ndk::looper::FdEvent::INPUT, todo!()).unwrap(); + /// ``` + pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper { + self.inner.read().unwrap().java_main_looper().clone() + } + /// Returns a pointer to the Java Virtual Machine, for making JNI calls /// /// This returns a pointer to the Java Virtual Machine which can be used diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs index e5cd8c5..2323a05 100644 --- a/android-activity/src/native_activity/glue.rs +++ b/android-activity/src/native_activity/glue.rs @@ -845,6 +845,9 @@ extern "C" fn ANativeActivity_onCreate( activity, saved_state, saved_state_size ); + let main_looper = + ndk::looper::ForeignLooper::for_thread().expect("Failed to get Java main looper"); + // Conceptually we associate a glue reference with the JVM main thread, and another // reference with the Rust main thread let jvm_glue = NativeActivityGlue::new(activity, saved_state, saved_state_size); @@ -856,7 +859,7 @@ extern "C" fn ANativeActivity_onCreate( // Note: we drop the thread handle which will detach the thread std::thread::spawn(move || { let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _; - rust_glue_entry(rust_glue, activity); + rust_glue_entry(rust_glue, activity, main_looper); }); // Wait for thread to start. @@ -870,7 +873,11 @@ extern "C" fn ANativeActivity_onCreate( }) } -fn rust_glue_entry(rust_glue: NativeActivityGlue, activity: *mut ndk_sys::ANativeActivity) { +fn rust_glue_entry( + rust_glue: NativeActivityGlue, + activity: *mut ndk_sys::ANativeActivity, + main_looper: ndk::looper::ForeignLooper, +) { abort_on_panic(|| { let (jvm, jni_activity) = unsafe { let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast(); @@ -897,7 +904,7 @@ fn rust_glue_entry(rust_glue: NativeActivityGlue, activity: *mut ndk_sys::ANativ ); } - let app = AndroidApp::new(rust_glue.clone(), jvm.clone()); + let app = AndroidApp::new(rust_glue.clone(), jvm.clone(), main_looper); rust_glue.notify_main_thread_running(); diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs index 424aceb..becef88 100644 --- a/android-activity/src/native_activity/mod.rs +++ b/android-activity/src/native_activity/mod.rs @@ -62,34 +62,40 @@ impl StateLoader<'_> { } impl AndroidApp { - pub(crate) fn new(native_activity: NativeActivityGlue, jvm: JavaVM) -> Self { + pub(crate) fn new( + native_activity: NativeActivityGlue, + jvm: JavaVM, + main_looper: ndk::looper::ForeignLooper, + ) -> Self { jvm.with_local_frame(10, |env| -> jni::errors::Result<_> { if let Err(err) = crate::input::jni_init(env) { panic!("Failed to init JNI bindings: {err:?}"); }; + let looper = unsafe { + let ptr = ndk_sys::ALooper_prepare( + ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int, + ); + ndk::looper::ForeignLooper::from_ptr(ptr::NonNull::new(ptr).unwrap()) + }; let app = Self { inner: Arc::new(RwLock::new(AndroidAppInner { jvm: jvm.clone(), native_activity, - looper: Looper { - ptr: ptr::null_mut(), - }, + looper, + main_looper, key_maps: Mutex::new(HashMap::new()), input_receiver: Mutex::new(None), })), }; { - let mut guard = app.inner.write().unwrap(); + let guard = app.inner.write().unwrap(); let main_fd = guard.native_activity.cmd_read_fd(); unsafe { - guard.looper.ptr = ndk_sys::ALooper_prepare( - ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int, - ); ndk_sys::ALooper_addFd( - guard.looper.ptr, + guard.looper.ptr().as_ptr(), main_fd, LOOPER_ID_MAIN, ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int, @@ -106,19 +112,18 @@ impl AndroidApp { } } -#[derive(Debug)] -struct Looper { - pub ptr: *mut ndk_sys::ALooper, -} -unsafe impl Send for Looper {} -unsafe impl Sync for Looper {} - #[derive(Debug)] pub(crate) struct AndroidAppInner { pub(crate) jvm: JavaVM, pub(crate) native_activity: NativeActivityGlue, - looper: Looper, + + /// Looper associated with the Rust `android_main` thread + looper: ndk::looper::ForeignLooper, + + /// Looper associated with the activity's Java main thread, sometimes called + /// the UI thread. + main_looper: ndk::looper::ForeignLooper, /// A table of `KeyCharacterMap`s per `InputDevice` ID /// these are used to be able to map key presses to unicode @@ -145,8 +150,12 @@ impl AndroidAppInner { self.native_activity.activity } - pub(crate) fn looper(&self) -> *mut ndk_sys::ALooper { - self.looper.ptr + pub(crate) fn looper_as_ptr(&self) -> *mut ndk_sys::ALooper { + self.looper.ptr().as_ptr() + } + + pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper { + self.main_looper.clone() } pub fn native_window(&self) -> Option { @@ -173,7 +182,7 @@ impl AndroidAppInner { trace!("Calling ALooper_pollOnce, timeout = {timeout_milliseconds}"); assert_eq!( ndk_sys::ALooper_forThread(), - self.looper.ptr, + self.looper_as_ptr(), "Application tried to poll events from non-main thread" ); let id = ndk_sys::ALooper_pollOnce( @@ -245,7 +254,7 @@ impl AndroidAppInner { trace!("Calling pre_exec_cmd({ipc_cmd:#?})"); self.native_activity.pre_exec_cmd( ipc_cmd, - self.looper(), + self.looper_as_ptr(), LOOPER_ID_INPUT, ); @@ -282,7 +291,7 @@ impl AndroidAppInner { pub fn create_waker(&self) -> AndroidAppWaker { // Safety: we know that the looper is a valid, non-null pointer - unsafe { AndroidAppWaker::new(self.looper.ptr) } + unsafe { AndroidAppWaker::new(self.looper_as_ptr()) } } pub fn config(&self) -> ConfigurationRef { @@ -405,7 +414,7 @@ impl AndroidAppInner { // trigger a wake up) let queue = self .native_activity - .looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT); + .looper_attached_input_queue(self.looper_as_ptr(), LOOPER_ID_INPUT); // Note: we don't treat it as an error if there is no queue, so if applications // iterate input before a queue has been created (e.g. before onStart) then