Use InputMethodManager#showSoftInput to show_soft_input

This updates `AndroidApp::show/hide_soft_input` to be implemented
manually with JNI (instead of `ANativeActivity_show/hideSoftInput`) so
that we can pass the root, decor view to
`InputMethodManager.showSoftInput` instead of the private
`mNativeContentView` created by `NativeActivity`.

Unlike the private `mNativeContentView`, the root decor view is
considered to be the current "served" view for a vanilla
`NativeActivity`-based application.

Co-authored-by: Robert Bragg <robert@sixbynine.org>
This commit is contained in:
Jan Češpivo
2024-12-28 18:06:07 +01:00
committed by Robert Bragg
parent c1d00b9191
commit c3ed6ba77d
6 changed files with 122 additions and 26 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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:?}");
};
+1 -10
View File
@@ -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 {
+2
View File
@@ -395,6 +395,8 @@ mod util;
mod jni_utils;
mod sdk;
mod waker;
pub use waker::AndroidAppWaker;
+50 -15
View File
@@ -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::<Activity>(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::<Activity>(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:?}");
}
}
+67
View File
@@ -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(())
}