diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index ce29451..373ad87 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -61,6 +61,7 @@ fn android_on_create(state: &OnCreateState) { - *Safety* The `native-activity` backend clears its `ANativeActivity` ptr after `onDestroy` and `AndroidApp` remains safe to access after `android_main()` returns ([#234](https://github.com/rust-mobile/android-activity/pull/234)) - *Safety* `AndroidApp::activity_as_ptr()` returns a pointer to a global reference that remains valid until `AndroidApp` is dropped, instead of the `ANativeActivity`'s `clazz` pointer which is only guaranteed to be valid until `onDestroy` returns (`native-activity` backend) ([#234](https://github.com/rust-mobile/android-activity/pull/234)) - *Safety* The `game-activity` backend clears its `android_app` ptr after `onDestroy` and `AndroidApp` remains safe to access after `android_main()` returns ([#236](https://github.com/rust-mobile/android-activity/pull/236)) +- Support for `AndroidApp::show/hide_soft_input()` APIs in the `native-activity` backend ([#178](https://github.com/rust-mobile/android-activity/pull/178)) ## [0.6.0] - 2024-04-26 diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index f9b8a08..c253274 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -128,7 +128,7 @@ impl AndroidApp { ) -> 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) { + if let Err(err) = crate::sdk::jni_init(env) { panic!("Failed to init JNI bindings: {err:?}"); }; diff --git a/android-activity/src/input/sdk.rs b/android-activity/src/input/sdk.rs index a42cc32..dbe10f9 100644 --- a/android-activity/src/input/sdk.rs +++ b/android-activity/src/input/sdk.rs @@ -107,8 +107,7 @@ impl AKeyCharacterMap<'_> { } jni::bind_java_type! { - rust_type = AInputDevice, - java_type = "android.view.InputDevice", + pub(crate) AInputDevice => "android.view.InputDevice", type_map { AKeyCharacterMap => "android.view.KeyCharacterMap", }, @@ -118,14 +117,6 @@ jni::bind_java_type! { } } -// Explicitly initialize the JNI bindings so we can get and early, upfront, -// error if something is wrong. -pub fn jni_init(env: &jni::Env) -> jni::errors::Result<()> { - let _ = AKeyCharacterMapAPI::get(env, &Default::default())?; - let _ = AInputDeviceAPI::get(env, &Default::default())?; - Ok(()) -} - /// Describes the keys provided by a keyboard device and their associated labels. #[derive(Debug)] pub struct KeyCharacterMap { diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index bd09b98..589454d 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -395,6 +395,8 @@ mod util; mod jni_utils; +mod sdk; + mod waker; pub use waker::AndroidAppWaker; diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs index fab67be..6ab89d5 100644 --- a/android-activity/src/native_activity/mod.rs +++ b/android-activity/src/native_activity/mod.rs @@ -14,6 +14,7 @@ use ndk::{asset::AssetManager, native_window::NativeWindow}; use crate::error::InternalResult; use crate::main_callbacks::MainCallbacks; +use crate::sdk::{Activity, Context, InputMethodManager}; use crate::{ util, AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags, @@ -72,7 +73,7 @@ impl AndroidApp { jni_activity: &JObject, ) -> Self { jvm.with_local_frame(10, |env| -> jni::errors::Result<_> { - if let Err(err) = crate::input::jni_init(env) { + if let Err(err) = crate::sdk::jni_init(env) { panic!("Failed to init JNI bindings: {err:?}"); }; @@ -368,13 +369,29 @@ impl AndroidAppInner { log::error!("Can't show soft input after NativeActivity has been destroyed"); return; } - unsafe { - let flags = if show_implicit { - ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT - } else { - 0 - }; - ndk_sys::ANativeActivity_showSoftInput(na as *mut _, flags); + + // Note: `.attach_current_thread()` will also handle catching any Java exceptions that + // might be thrown by the JNI calls we make. + let res = self + .jvm + .attach_current_thread(|env| -> jni::errors::Result<()> { + let activity = env.as_cast::(self.activity.as_ref())?; + + let ims = Context::INPUT_METHOD_SERVICE(env)?; + let im_manager = activity.as_context().get_system_service(env, ims)?; + let im_manager = InputMethodManager::cast_local(env, im_manager)?; + let jni_window = activity.get_window(env)?; + let view = jni_window.get_decor_view(env)?; + let flags = if show_implicit { + ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32 + } else { + 0 + }; + im_manager.show_soft_input(env, view, flags)?; + Ok(()) + }); + if let Err(err) = res { + log::warn!("Failed to show soft input: {err:?}"); } } @@ -386,13 +403,31 @@ impl AndroidAppInner { log::error!("Can't hide soft input after NativeActivity has been destroyed"); return; } - unsafe { - let flags = if hide_implicit_only { - ndk_sys::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY - } else { - 0 - }; - ndk_sys::ANativeActivity_hideSoftInput(na as *mut _, flags); + + // Note: `.attach_current_thread()` will also handle catching any Java exceptions that + // might be thrown by the JNI calls we make. + let res = self + .jvm + .attach_current_thread(|env| -> jni::errors::Result<()> { + let activity = env.as_cast::(self.activity.as_ref())?; + + let ims = Context::INPUT_METHOD_SERVICE(env)?; + let imm_obj = activity.as_context().get_system_service(env, ims)?; + let imm = InputMethodManager::cast_local(env, imm_obj)?; + + let window = activity.get_window(env)?; + let decor = window.get_decor_view(env)?; + let token = decor.get_window_token(env)?; + + // HIDE_IMPLICIT_ONLY == 1, HIDE_NOT_ALWAYS == 2 + let flags = if hide_implicit_only { 1 } else { 0 }; + + let _hidden = imm.hide_soft_input_from_window(env, token, flags)?; + Ok(()) + }); + + if let Err(err) = res { + error!("Failed to hide soft input: {err:?}"); } } diff --git a/android-activity/src/sdk.rs b/android-activity/src/sdk.rs new file mode 100644 index 0000000..897ecdc --- /dev/null +++ b/android-activity/src/sdk.rs @@ -0,0 +1,67 @@ +jni::bind_java_type! { pub(crate) IBinder => "android.os.IBinder" } +jni::bind_java_type! { + pub(crate) View => "android.view.View", + type_map { + IBinder => "android.os.IBinder", + }, + methods { + fn get_window_token() -> IBinder, + } +} +jni::bind_java_type! { + pub(crate) InputMethodManager => "android.view.inputmethod.InputMethodManager", + type_map { + View => "android.view.View", + IBinder => "android.os.IBinder", + }, + methods { + fn show_soft_input(view: View, flags: i32) -> bool, + fn hide_soft_input_from_window(window_token: IBinder, flags: i32) -> bool, + } +} +jni::bind_java_type! { + pub(crate) Context => "android.content.Context", + fields { + #[allow(non_snake_case)] + static INPUT_METHOD_SERVICE: JString + }, + methods { + fn get_system_service(service_name: JString) -> JObject, + } +} +jni::bind_java_type! { + pub(crate) Window => "android.view.Window", + type_map { + View => "android.view.View", + }, + methods { + fn get_decor_view() -> View, + } +} +jni::bind_java_type! { + pub(crate) Activity => "android.app.Activity", + type_map { + Context => "android.content.Context", + Window => "android.view.Window", + }, + is_instance_of { + context: Context + }, + methods { + fn get_window() -> Window, + } +} + +// Explicitly initialize the JNI bindings so we can get and early, upfront, +// error if something is wrong. +pub(crate) fn jni_init(env: &jni::Env) -> jni::errors::Result<()> { + let _ = IBinderAPI::get(env, &Default::default())?; + let _ = ViewAPI::get(env, &Default::default())?; + let _ = InputMethodManagerAPI::get(env, &Default::default())?; + let _ = ContextAPI::get(env, &Default::default())?; + let _ = WindowAPI::get(env, &Default::default())?; + let _ = ActivityAPI::get(env, &Default::default())?; + let _ = crate::input::AKeyCharacterMapAPI::get(env, &Default::default())?; + let _ = crate::input::AInputDeviceAPI::get(env, &Default::default())?; + Ok(()) +}