Compare commits

...

7 Commits

Author SHA1 Message Date
Marijn Suijten 946dcdc919 Revert "input: Replace open-coded types with ndk::event definitions (#163)"
This reverts commit 51d05d48c8 for
backwards compatibility with the existing `0.6` releases.
2026-01-11 21:12:26 +00:00
Robert Bragg c18a4dd135 game-activty: ignore APP_CMD_SOFTWARE_KB_VIS_CHANGED w/o panic
APP_CMD_SOFTWARE_KB_VIS_CHANGED in the GameActivity backend is
intended for notifying the android_main thread that the soft keyboard
visibility has changed.

There's currently no Rust event / API for this, and so it wasn't being
handled in poll_events but that was leading to a unreachable panic when
GameActivity would send this APP_CMD when showing soft keyboards.

We don't currently plan to expose any public API / event for this since
it's based on monitoring IME insets and applications should instead be able
to check insets after getting InsetsChanged events.

For the sake of minimizing patches to the upstream GameActivity code
this makes it so poll_events can ignore this APP_CMD as a NOOP.
2026-01-11 21:10:27 +00:00
Robert Bragg 4827e0b17b Add support for InputEvent::TextAction events
This exposes IME actions via an InputEvent::TextAction event so that
it's possible to recognise when text entry via an input method is
finished.

This adds an `TextInputAction` enum to represent the action key on a soft
keyboard, such as "Done".

For example, this makes it possible to emit Ime::Commit events in Winit.
2026-01-11 21:10:27 +00:00
Robert Bragg a8637e3f25 Import android-games-sdk changes for 4.0.0
This imports the SDK from commit 8fa58b0e145ec28e726fa2b1c7e7a52af925ca35, from:
https://github.com/rust-mobile/android-games-sdk/commits/android-activity-4.0.0

This includes one "notify android_main of editor actions" patch which will make
it possible to forward editor actions and support IME Commit events in Winit)

# notify android_main of editor actions

This adds a pendingEditorActions member to android_app that is set via
onEditorAction and the android_main thread is notified via notifyInput
instead of re-instating APP_CMD_EDITOR_ACTION.

The idea is that the android_main thread should check for
android_app->pendingEditorActions whenever input events are polled/iterated.

# FFI bindings update

Also updates the FFI bindings via generate-bindings.sh
2026-01-11 21:10:23 +00:00
Robert Bragg aaf0e8b30a DROP ME: git repo patch for jni 0.22 2026-01-11 21:06:50 +00:00
Robert Bragg 46d2fa87c3 Update to jni 0.22 and jni-sys 0.4.1
This change notably starts to use the `jni::bind_java_type!` macro for the
`KeyCharacterMap` and `InputDevice` Java SDK API bindings in
`src/input/sdk.rs` which is a nice simplification.
2026-01-11 21:06:43 +00:00
Robert Bragg 249e7d1834 Bump MSRV to 1.85.0 (will be required by jni 0.22)
This re-generates the FFI bindings with `--rust-target '1.85.0'`
2026-01-11 20:44:18 +00:00
25 changed files with 7040 additions and 14291 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
# See top README for MSRV policy
rust-version: [1.73.0, stable]
rust-version: [1.85.0, stable]
steps:
- uses: actions/checkout@v4
+3
View File
@@ -3,3 +3,6 @@ resolver = "2"
members = ["android-activity"]
exclude = ["examples"]
[patch.crates-io]
jni = { git = "https://github.com/jni-rs/jni-rs.git", branch = "release-0.22" }
+7 -11
View File
@@ -6,19 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- input: Replaced custom types with their `ndk` crate equivalent.
> [!NOTE]
> These types existed because the `ndk` crate didn't provide them in an extensible way. Now that they have the `#[non_exhaustive]` flag and contain a `__Unknown(T)` variant to provide lossless conversions, and not to mention use an ABI type that matches how it is being used by most functions (when the original constants were defined in a "typeless" way), the `ndk` types are used and reexported once again.
### Added
> [!IMPORTANT]
> **Relevant breaking changes**:
> - `repr()` types for some `enum`s have changed to match the ABI type that is used by most functions that are returning or consuming this wrapper type.
> - `Source::is_xxx_class()` functions are replaced by querying `Source::class()` and comparing against variants from the returned `SourceClass` `bitflags` enum.
> - `SourceFlags::TRACKBALL` (from `Source::is_trackball_class()`) is named `SourceClass::NAVIGATION` in the `ndk`.
- rust-version bumped to 1.73.0 ([#193](https://github.com/rust-mobile/android-activity/pull/193))
- The `ndk` and `ndk-sys` crates are now re-exported under `android_activity::ndk` and `android_activity::ndk_sys` ([#194](https://github.com/rust-mobile/android-activity/pull/194))
- input: TextInputAction enum representing action button types on soft keyboards.
- input: InputEvent::TextAction event for handling action button presses from soft keyboards.
### Changed
- rust-version bumped to 1.85.0 ([#193](https://github.com/rust-mobile/android-activity/pull/193), TODO: link)
- GameActivity updated to 4.0.0 (requires the corresponding 4.0.0 `.aar` release from Google) ([#191](https://github.com/rust-mobile/android-activity/pull/191))
## [0.6.0] - 2024-04-26
+3 -7
View File
@@ -16,10 +16,7 @@ include = [
"/src",
]
# Even though we could technically still build with 1.69, 1.73 has a fix for the
# definition of the `stat` struct on Android, and so it seems worthwhile drawing
# a line under that to ensure android-activity applications have that fix.
rust-version = "1.73.0"
rust-version = "1.85.0"
[features]
# Note: we don't enable any backend by default since features
@@ -32,13 +29,12 @@ default = []
game-activity = []
native-activity = []
api-level-30 = ["ndk/api-level-30"]
api-level-33 = ["api-level-30", "ndk/api-level-33"]
[dependencies]
log = "0.4"
jni-sys = "0.3"
cesu8 = "1"
jni = "0.21"
jni = "0.22"
jni-sys = "0.4.1"
ndk-sys = "0.6.0"
ndk = { version = "0.9.0", default-features = false }
ndk-context = "0.1.1"
@@ -226,6 +226,10 @@ struct android_app {
* thread, so we can't say that this is only valid within the `APP_CMD_` handler.
*/
int editorAction;
/**
* true when editorAction has been set
*/
bool pendingEditorAction;
/**
* Current state of the app's activity. May be either APP_CMD_START,
@@ -726,9 +726,15 @@ static bool onEditorAction(GameActivity* activity, int action) {
// XXX: this is a racy design that could lose InputConnection actions if the
// application doesn't manage to look at app->editorAction before another
// action is delivered.
if (android_app->pendingEditorAction) {
LOGW("Dropping editor action %d because previous action %d not yet "
"handled",
action, android_app->editorAction);
}
android_app->editorAction = action;
// TODO: buffer these actions like other input events
//notifyInput(android_app);
android_app->pendingEditorAction = true;
notifyInput(android_app);
// TODO: buffer IME text events and editor actions like other input events
//android_app_write_cmd(android_app, APP_CMD_EDITOR_ACTION);
pthread_mutex_unlock(&android_app->mutex);
+1 -1
View File
@@ -15,7 +15,7 @@ while read ARCH && read TARGET ; do
# --module-raw-line 'use '
bindgen game-activity-ffi.h -o src/game_activity/ffi_$ARCH.rs \
--rust-target '1.73.0' \
--rust-target '1.85.0' \
--blocklist-item 'JNI\w+' \
--blocklist-item 'C?_?JNIEnv' \
--blocklist-item '_?JavaVM' \
+5
View File
@@ -27,6 +27,10 @@ pub(crate) enum InternalAppError {
JniError(jni::errors::JniError),
#[error("A Java Exception was thrown via a JNI method call")]
JniException(String),
// For internal errors that don't lead to a Java exception but are
// still JNI related.
#[error("A bad argument was passed to a JNI method: {0}")]
JniBadArgument(String),
#[error("A Java VM error")]
JvmError(jni::errors::Error),
#[error("Input unavailable")]
@@ -51,6 +55,7 @@ impl From<InternalAppError> for AppError {
match value {
InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
InternalAppError::JniException(msg) => AppError::JavaError(msg),
InternalAppError::JniBadArgument(msg) => AppError::JavaError(msg),
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
InternalAppError::InputUnavailable => AppError::InputUnavailable,
}
+1 -1
View File
@@ -12,7 +12,7 @@
#![allow(deref_nullptr)]
#![allow(dead_code)]
use jni_sys::*;
use jni::sys::*;
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t};
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -13,12 +13,10 @@
// The `Class` was also bound differently to `android-ndk-rs` considering how the class is defined
// by masking bits from the `Source`.
use ndk::event::ButtonState;
use crate::activity_impl::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
use crate::input::{
Axis, Button, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
Axis, Button, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState,
MotionAction, MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
@@ -29,6 +27,7 @@ pub enum InputEvent<'a> {
MotionEvent(MotionEvent<'a>),
KeyEvent(KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}
/// A motion event.
@@ -49,7 +48,7 @@ impl<'a> MotionEvent<'a> {
///
#[inline]
pub fn source(&self) -> Source {
let source = self.ga_event.source;
let source = self.ga_event.source as u32;
source.into()
}
@@ -65,7 +64,7 @@ impl<'a> MotionEvent<'a> {
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
#[inline]
pub fn action(&self) -> MotionAction {
let action = self.ga_event.action & ndk_sys::AMOTION_EVENT_ACTION_MASK as i32;
let action = self.ga_event.action as u32 & ndk_sys::AMOTION_EVENT_ACTION_MASK;
action.into()
}
@@ -178,7 +177,6 @@ impl<'a> MotionEvent<'a> {
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
#[inline]
// TODO: Button enum to signify only one bitflag can be set?
pub fn button_state(&self) -> ButtonState {
ButtonState(self.ga_event.buttonState as u32)
}
@@ -281,7 +279,7 @@ impl PointerImpl<'_> {
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let pointer = &self.event.ga_event.pointers[self.index];
let axis: i32 = axis.into();
let axis: u32 = axis.into();
pointer.axisValues[axis as usize]
}
@@ -300,7 +298,8 @@ impl PointerImpl<'_> {
#[inline]
pub fn tool_type(&self) -> ToolType {
let pointer = &self.event.ga_event.pointers[self.index];
pointer.toolType.into()
let tool_type = pointer.toolType as u32;
tool_type.into()
}
}
@@ -667,7 +666,7 @@ impl<'a> KeyEvent<'a> {
///
#[inline]
pub fn source(&self) -> Source {
let source = self.ga_event.source;
let source = self.ga_event.source as u32;
source.into()
}
@@ -683,13 +682,13 @@ impl<'a> KeyEvent<'a> {
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
#[inline]
pub fn action(&self) -> KeyAction {
let action = self.ga_event.action;
let action = self.ga_event.action as u32;
action.into()
}
#[inline]
pub fn action_button(&self) -> KeyAction {
let action = self.ga_event.action;
let action = self.ga_event.action as u32;
action.into()
}
@@ -719,7 +718,7 @@ impl<'a> KeyEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
#[inline]
pub fn key_code(&self) -> Keycode {
let keycode = self.ga_event.keyCode;
let keycode = self.ga_event.keyCode as u32;
keycode.into()
}
+179 -128
View File
@@ -8,10 +8,12 @@ use std::sync::Weak;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use jni::objects::JObject;
use jni::refs::Global;
use libc::c_void;
use log::{error, trace};
use jni_sys::*;
use jni::sys::*;
use ndk_sys::ALooper_wake;
use ndk_sys::{ALooper, ALooper_pollAll};
@@ -21,9 +23,11 @@ use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
use crate::error::InternalResult;
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
use crate::jni_utils::{self, CloneJavaVM};
use crate::util::{abort_on_panic, forward_stdio_to_logcat, log_panic, try_get_path_from_ptr};
use crate::input::{device_key_character_map, Axis, KeyCharacterMap, TextInputAction};
use crate::util::{
abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic,
try_get_path_from_ptr,
};
use crate::{
AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
};
@@ -119,32 +123,31 @@ impl AndroidAppWaker {
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>, jvm: CloneJavaVM) -> Self {
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>, jvm: jni::JavaVM) -> 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) {
panic!("Failed to init JNI bindings: {err:?}");
};
let key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
Ok(b) => b,
Err(err) => {
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
}
};
// 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));
// 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));
Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm,
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
key_map_binding: Arc::new(key_map_binding),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
}
Ok(Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm: jvm.clone(),
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
})
})
.expect("Failed to create AndroidApp instance")
}
}
@@ -174,9 +177,6 @@ impl NativeAppGlue {
};
let out_ptr = &mut out_state as *mut TextInputState;
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
// NEON WARNING:
//
// It's not clearly documented but the GameActivity API over the
@@ -204,6 +204,14 @@ impl NativeAppGlue {
}
}
pub fn take_text_input_state(&self) -> TextInputState {
unsafe {
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
}
self.text_input_state()
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
unsafe {
@@ -247,18 +255,27 @@ impl NativeAppGlue {
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
}
}
pub fn take_pending_editor_action(&self) -> Option<i32> {
unsafe {
let app_ptr = self.as_ptr();
if (*app_ptr).pendingEditorAction {
(*app_ptr).pendingEditorAction = false;
Some((*app_ptr).editorAction)
} else {
None
}
}
}
}
#[derive(Debug)]
pub struct AndroidAppInner {
pub(crate) jvm: CloneJavaVM,
pub(crate) jvm: jni::JavaVM,
native_app: NativeAppGlue,
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
@@ -351,46 +368,61 @@ impl AndroidAppInner {
let cmd = match cmd_i as ffi::NativeAppGlueAppCmd {
//NativeAppGlueAppCmd_UNUSED_APP_CMD_INPUT_CHANGED => AndroidAppMainEvent::InputChanged,
ffi::NativeAppGlueAppCmd_APP_CMD_INIT_WINDOW => {
MainEvent::InitWindow {}
Some(MainEvent::InitWindow {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_TERM_WINDOW => {
MainEvent::TerminateWindow {}
Some(MainEvent::TerminateWindow {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_RESIZED => {
MainEvent::WindowResized {}
Some(MainEvent::WindowResized {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_REDRAW_NEEDED => {
MainEvent::RedrawNeeded {}
Some(MainEvent::RedrawNeeded {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_CONTENT_RECT_CHANGED => {
MainEvent::ContentRectChanged {}
Some(MainEvent::ContentRectChanged {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_GAINED_FOCUS => {
MainEvent::GainedFocus
Some(MainEvent::GainedFocus)
}
ffi::NativeAppGlueAppCmd_APP_CMD_LOST_FOCUS => {
MainEvent::LostFocus
Some(MainEvent::LostFocus)
}
ffi::NativeAppGlueAppCmd_APP_CMD_CONFIG_CHANGED => {
MainEvent::ConfigChanged {}
Some(MainEvent::ConfigChanged {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_LOW_MEMORY => {
MainEvent::LowMemory
Some(MainEvent::LowMemory)
}
ffi::NativeAppGlueAppCmd_APP_CMD_START => {
Some(MainEvent::Start)
}
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => {
Some(MainEvent::Resume {
loader: StateLoader { app: self },
})
}
ffi::NativeAppGlueAppCmd_APP_CMD_START => MainEvent::Start,
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => MainEvent::Resume {
loader: StateLoader { app: self },
},
ffi::NativeAppGlueAppCmd_APP_CMD_SAVE_STATE => {
MainEvent::SaveState {
Some(MainEvent::SaveState {
saver: StateSaver { app: self },
}
})
}
ffi::NativeAppGlueAppCmd_APP_CMD_PAUSE => {
Some(MainEvent::Pause)
}
ffi::NativeAppGlueAppCmd_APP_CMD_STOP => Some(MainEvent::Stop),
ffi::NativeAppGlueAppCmd_APP_CMD_DESTROY => {
Some(MainEvent::Destroy)
}
ffi::NativeAppGlueAppCmd_APP_CMD_PAUSE => MainEvent::Pause,
ffi::NativeAppGlueAppCmd_APP_CMD_STOP => MainEvent::Stop,
ffi::NativeAppGlueAppCmd_APP_CMD_DESTROY => MainEvent::Destroy,
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_INSETS_CHANGED => {
MainEvent::InsetsChanged {}
Some(MainEvent::InsetsChanged {})
}
ffi::NativeAppGlueAppCmd_APP_CMD_SOFTWARE_KB_VIS_CHANGED => {
// NOOP: we ignore these events because they are driven by a
// potentially-unreliable heuristic (based on watching for
// inset changes) and we don't currently have a public event
// for exposing this state.
None
}
_ => unreachable!(),
};
@@ -399,30 +431,35 @@ impl AndroidAppInner {
trace!("Calling android_app_pre_exec_cmd({cmd_i})");
ffi::android_app_pre_exec_cmd(native_app.as_ptr(), cmd_i);
match cmd {
MainEvent::ConfigChanged { .. } => {
self.config.replace(Configuration::clone_from_ptr(
NonNull::new_unchecked((*native_app.as_ptr()).config),
));
}
MainEvent::InitWindow { .. } => {
let win_ptr = (*native_app.as_ptr()).window;
// It's important that we use ::clone_from_ptr() here
// because NativeWindow has a Drop implementation that
// will unconditionally _release() the native window
*self.native_window.write().unwrap() =
Some(NativeWindow::clone_from_ptr(
NonNull::new(win_ptr).unwrap(),
));
}
MainEvent::TerminateWindow { .. } => {
*self.native_window.write().unwrap() = None;
}
_ => {}
}
trace!("Invoking callback for ID_MAIN command = {:?}", cmd);
callback(PollEvent::Main(cmd));
if let Some(cmd) = cmd {
match cmd {
MainEvent::ConfigChanged { .. } => {
self.config.replace(Configuration::clone_from_ptr(
NonNull::new_unchecked(
(*native_app.as_ptr()).config,
),
));
}
MainEvent::InitWindow { .. } => {
let win_ptr = (*native_app.as_ptr()).window;
// It's important that we use ::clone_from_ptr() here
// because NativeWindow has a Drop implementation that
// will unconditionally _release() the native window
*self.native_window.write().unwrap() =
Some(NativeWindow::clone_from_ptr(
NonNull::new(win_ptr).unwrap(),
));
}
MainEvent::TerminateWindow { .. } => {
*self.native_window.write().unwrap() = None;
}
_ => {}
}
trace!("Invoking callback for ID_MAIN command = {:?}", cmd);
callback(PollEvent::Main(cmd));
}
trace!("Calling android_app_post_exec_cmd({cmd_i})");
ffi::android_app_post_exec_cmd(native_app.as_ptr(), cmd_i);
@@ -533,11 +570,7 @@ impl AndroidAppInner {
let key_map = match guard.entry(device_id) {
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
std::collections::hash_map::Entry::Vacant(vacant) => {
let character_map = jni_utils::device_key_character_map(
self.jvm.clone(),
self.key_map_binding.clone(),
device_id,
)?;
let character_map = device_key_character_map(self.jvm.clone(), device_id)?;
vacant.insert(character_map.clone());
character_map
}
@@ -547,11 +580,13 @@ impl AndroidAppInner {
}
pub fn enable_motion_axis(&mut self, axis: Axis) {
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis.into()) }
let axis: u32 = axis.into();
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
}
pub fn disable_motion_axis(&mut self, axis: Axis) {
unsafe { ffi::GameActivityPointerAxes_disableAxis(axis.into()) }
let axis: u32 = axis.into();
unsafe { ffi::GameActivityPointerAxes_disableAxis(axis as i32) }
}
pub fn create_waker(&self) -> AndroidAppWaker {
@@ -782,7 +817,8 @@ impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
_receiver: receiver,
buffered,
native_app,
text_event_checked: false,
ime_text_input_state_checked: false,
ime_editor_action_checked: false,
}
}
}
@@ -799,7 +835,8 @@ pub(crate) struct InputIteratorInner<'a> {
buffered: Option<BufferedEvents<'a>>,
native_app: NativeAppGlue,
text_event_checked: bool,
ime_text_input_state_checked: bool,
ime_editor_action_checked: bool,
}
impl InputIteratorInner<'_> {
@@ -819,8 +856,10 @@ impl InputIteratorInner<'_> {
self.buffered = None;
}
if !self.text_event_checked {
self.text_event_checked = true;
// We make sure any input state changes are sent before we check
// for editor actions, so actions will apply to the latest state.
if !self.ime_text_input_state_checked {
self.ime_text_input_state_checked = true;
unsafe {
let app_ptr = self.native_app.as_ptr();
@@ -832,12 +871,21 @@ impl InputIteratorInner<'_> {
// the compiler isn't reordering code so this gets flagged
// before the java main thread really updates the state.
if (*app_ptr).textInputState != 0 {
let state = self.native_app.text_input_state(); // Will clear .textInputState
let state = self.native_app.take_text_input_state(); // Will clear .textInputState
let _ = callback(&InputEvent::TextEvent(state));
return true;
}
}
}
if !self.ime_editor_action_checked {
self.ime_editor_action_checked = true;
if let Some(action) = self.native_app.take_pending_editor_action() {
let _ = callback(&InputEvent::TextAction(TextInputAction::from(action)));
return true;
}
}
false
}
}
@@ -876,7 +924,7 @@ pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_initializeN
jasset_mgr: jobject,
saved_state: jbyteArray,
java_config: jobject,
) -> jni_sys::jlong {
) -> jlong {
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
env,
java_game_activity,
@@ -910,53 +958,56 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
abort_on_panic(|| {
let _join_log_forwarder = forward_stdio_to_logcat();
let jvm = unsafe {
let (jvm, jni_activity) = unsafe {
let jvm = (*(*native_app).activity).vm;
let activity: jobject = (*(*native_app).activity).javaGameActivity;
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
jvm.attach_current_thread_permanently().unwrap();
jvm
(jni::JavaVM::from_raw(jvm), activity)
};
// Note: At this point we can assume jni::JavaVM::singleton is initialized
unsafe {
// Name thread - this needs to happen here after attaching to a JVM thread,
// since that changes the thread name to something like "Thread-2".
let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
// 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.
//
// Calling .attach_current_thread will ensure that the `jni` crate knows about the
// attachment, as a convenience.
jvm.attach_current_thread(|env| -> jni::errors::Result<()> {
// SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance
let jni_activity = unsafe { env.as_cast_raw::<Global<JObject>>(&jni_activity)? };
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
if let Err(err) = init_android_main_thread(&jvm, &jni_activity) {
eprintln!("Failed to name Java thread and set thread context class loader: {err}");
}
// 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(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(log_panic);
unsafe {
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
// 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(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(log_panic);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ffi::GameActivity_finish((*native_app).activity);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ffi::GameActivity_finish((*native_app).activity);
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
ndk_context::release_android_context();
}
Ok(())
})
.expect("Failed to attach thread to JVM");
})
}
+860 -4
View File
@@ -1,7 +1,4 @@
pub use ndk::event::{
Axis, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction, MotionEventFlags,
Source, SourceClass, ToolType,
};
use bitflags::bitflags;
pub use crate::activity_impl::input::*;
use crate::InputStatus;
@@ -9,6 +6,238 @@ use crate::InputStatus;
mod sdk;
pub use sdk::*;
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
/// SDK integer values.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Source {
BluetoothStylus = 0x0000c002,
Dpad = 0x00000201,
/// Either a gamepad or a joystick
Gamepad = 0x00000401,
Hdmi = 0x02000001,
/// Either a gamepad or a joystick
Joystick = 0x01000010,
/// Pretty much any device with buttons. Query the keyboard type to determine
/// if it has alphabetic keys and can be used for text entry.
Keyboard = 0x00000101,
/// A pointing device, such as a mouse or trackpad
Mouse = 0x00002002,
/// A pointing device, such as a mouse or trackpad whose relative motions should be treated as navigation events
MouseRelative = 0x00020004,
/// An input device akin to a scroll wheel
RotaryEncoder = 0x00400000,
Sensor = 0x04000000,
Stylus = 0x00004002,
Touchpad = 0x00100008,
Touchscreen = 0x00001002,
TouchNavigation = 0x00200000,
Trackball = 0x00010004,
// We need to consider that the enum variants may be extended across
// different versions of Android (i.e. effectively at runtime) but at the
// same time we don't want it to be an API break to extend this enum in
// future releases of `android-activity` with new variants from the latest
// NDK/SDK.
//
// We can't just use `#[non_exhaustive]` because that only really helps
// when adding new variants in sync with android-activity releases.
//
// On the other hand we also can't rely on a catch-all `Unknown(u32)` that
// only really helps with unknown variants seen at runtime.
//
// What we aim for instead is to have a hidden catch-all variant that
// is considered (practically) unmatchable so code is forced to have
// a `unknown => {}` catch-all pattern match that will cover unknown variants
// either in the form of Rust variants added in future versions or
// in the form of an `__Unknown(u32)` integer that represents an unknown
// variant seen at runtime.
//
// Any `unknown => {}` pattern match can rely on `IntoPrimitive` to convert
// the `unknown` variant to the integer that comes from the Android SDK
// in case that values needs to be passed on, even without knowing its
// semantic meaning at compile time.
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
// ndk_sys doesn't currently have the `TRACKBALL` flag so we define our
// own internal class constants for now
bitflags! {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct SourceFlags: u32 {
const CLASS_MASK = 0x000000ff;
const BUTTON = 0x00000001;
const POINTER = 0x00000002;
const TRACKBALL = 0x00000004;
const POSITION = 0x00000008;
const JOYSTICK = 0x00000010;
const NONE = 0;
}
}
impl Source {
#[inline]
pub fn is_button_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::BUTTON)
}
#[inline]
pub fn is_pointer_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::POINTER)
}
#[inline]
pub fn is_trackball_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::TRACKBALL)
}
#[inline]
pub fn is_position_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::POSITION)
}
#[inline]
pub fn is_joystick_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::JOYSTICK)
}
}
/// A bitfield representing the state of modifier keys during an event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-25)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct MetaState(pub u32);
impl MetaState {
#[inline]
pub fn alt_on(self) -> bool {
self.0 & ndk_sys::AMETA_ALT_ON != 0
}
#[inline]
pub fn alt_left_on(self) -> bool {
self.0 & ndk_sys::AMETA_ALT_LEFT_ON != 0
}
#[inline]
pub fn alt_right_on(self) -> bool {
self.0 & ndk_sys::AMETA_ALT_RIGHT_ON != 0
}
#[inline]
pub fn shift_on(self) -> bool {
self.0 & ndk_sys::AMETA_SHIFT_ON != 0
}
#[inline]
pub fn shift_left_on(self) -> bool {
self.0 & ndk_sys::AMETA_SHIFT_LEFT_ON != 0
}
#[inline]
pub fn shift_right_on(self) -> bool {
self.0 & ndk_sys::AMETA_SHIFT_RIGHT_ON != 0
}
#[inline]
pub fn sym_on(self) -> bool {
self.0 & ndk_sys::AMETA_SYM_ON != 0
}
#[inline]
pub fn function_on(self) -> bool {
self.0 & ndk_sys::AMETA_FUNCTION_ON != 0
}
#[inline]
pub fn ctrl_on(self) -> bool {
self.0 & ndk_sys::AMETA_CTRL_ON != 0
}
#[inline]
pub fn ctrl_left_on(self) -> bool {
self.0 & ndk_sys::AMETA_CTRL_LEFT_ON != 0
}
#[inline]
pub fn ctrl_right_on(self) -> bool {
self.0 & ndk_sys::AMETA_CTRL_RIGHT_ON != 0
}
#[inline]
pub fn meta_on(self) -> bool {
self.0 & ndk_sys::AMETA_META_ON != 0
}
#[inline]
pub fn meta_left_on(self) -> bool {
self.0 & ndk_sys::AMETA_META_LEFT_ON != 0
}
#[inline]
pub fn meta_right_on(self) -> bool {
self.0 & ndk_sys::AMETA_META_RIGHT_ON != 0
}
#[inline]
pub fn caps_lock_on(self) -> bool {
self.0 & ndk_sys::AMETA_CAPS_LOCK_ON != 0
}
#[inline]
pub fn num_lock_on(self) -> bool {
self.0 & ndk_sys::AMETA_NUM_LOCK_ON != 0
}
#[inline]
pub fn scroll_lock_on(self) -> bool {
self.0 & ndk_sys::AMETA_SCROLL_LOCK_ON != 0
}
}
impl From<ndk::event::MetaState> for MetaState {
fn from(value: ndk::event::MetaState) -> Self {
Self(value.0)
}
}
/// A motion action.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-29)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
/// SDK integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum MotionAction {
Down = ndk_sys::AMOTION_EVENT_ACTION_DOWN,
Up = ndk_sys::AMOTION_EVENT_ACTION_UP,
Move = ndk_sys::AMOTION_EVENT_ACTION_MOVE,
Cancel = ndk_sys::AMOTION_EVENT_ACTION_CANCEL,
Outside = ndk_sys::AMOTION_EVENT_ACTION_OUTSIDE,
PointerDown = ndk_sys::AMOTION_EVENT_ACTION_POINTER_DOWN,
PointerUp = ndk_sys::AMOTION_EVENT_ACTION_POINTER_UP,
HoverMove = ndk_sys::AMOTION_EVENT_ACTION_HOVER_MOVE,
Scroll = ndk_sys::AMOTION_EVENT_ACTION_SCROLL,
HoverEnter = ndk_sys::AMOTION_EVENT_ACTION_HOVER_ENTER,
HoverExit = ndk_sys::AMOTION_EVENT_ACTION_HOVER_EXIT,
ButtonPress = ndk_sys::AMOTION_EVENT_ACTION_BUTTON_PRESS,
ButtonRelease = ndk_sys::AMOTION_EVENT_ACTION_BUTTON_RELEASE,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Identifies buttons that are associated with motion events.
///
/// See [the NDK
@@ -40,6 +269,606 @@ pub enum Button {
__Unknown(u32),
}
/// An axis of a motion event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-32)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
/// SDK integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Axis {
X = ndk_sys::AMOTION_EVENT_AXIS_X,
Y = ndk_sys::AMOTION_EVENT_AXIS_Y,
Pressure = ndk_sys::AMOTION_EVENT_AXIS_PRESSURE,
Size = ndk_sys::AMOTION_EVENT_AXIS_SIZE,
TouchMajor = ndk_sys::AMOTION_EVENT_AXIS_TOUCH_MAJOR,
TouchMinor = ndk_sys::AMOTION_EVENT_AXIS_TOUCH_MINOR,
ToolMajor = ndk_sys::AMOTION_EVENT_AXIS_TOOL_MAJOR,
ToolMinor = ndk_sys::AMOTION_EVENT_AXIS_TOOL_MINOR,
Orientation = ndk_sys::AMOTION_EVENT_AXIS_ORIENTATION,
Vscroll = ndk_sys::AMOTION_EVENT_AXIS_VSCROLL,
Hscroll = ndk_sys::AMOTION_EVENT_AXIS_HSCROLL,
Z = ndk_sys::AMOTION_EVENT_AXIS_Z,
Rx = ndk_sys::AMOTION_EVENT_AXIS_RX,
Ry = ndk_sys::AMOTION_EVENT_AXIS_RY,
Rz = ndk_sys::AMOTION_EVENT_AXIS_RZ,
HatX = ndk_sys::AMOTION_EVENT_AXIS_HAT_X,
HatY = ndk_sys::AMOTION_EVENT_AXIS_HAT_Y,
Ltrigger = ndk_sys::AMOTION_EVENT_AXIS_LTRIGGER,
Rtrigger = ndk_sys::AMOTION_EVENT_AXIS_RTRIGGER,
Throttle = ndk_sys::AMOTION_EVENT_AXIS_THROTTLE,
Rudder = ndk_sys::AMOTION_EVENT_AXIS_RUDDER,
Wheel = ndk_sys::AMOTION_EVENT_AXIS_WHEEL,
Gas = ndk_sys::AMOTION_EVENT_AXIS_GAS,
Brake = ndk_sys::AMOTION_EVENT_AXIS_BRAKE,
Distance = ndk_sys::AMOTION_EVENT_AXIS_DISTANCE,
Tilt = ndk_sys::AMOTION_EVENT_AXIS_TILT,
Scroll = ndk_sys::AMOTION_EVENT_AXIS_SCROLL,
RelativeX = ndk_sys::AMOTION_EVENT_AXIS_RELATIVE_X,
RelativeY = ndk_sys::AMOTION_EVENT_AXIS_RELATIVE_Y,
Generic1 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_1,
Generic2 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_2,
Generic3 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_3,
Generic4 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_4,
Generic5 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_5,
Generic6 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_6,
Generic7 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_7,
Generic8 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_8,
Generic9 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_9,
Generic10 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_10,
Generic11 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_11,
Generic12 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_12,
Generic13 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_13,
Generic14 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_14,
Generic15 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_15,
Generic16 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_16,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// The tool type of a pointer.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-48)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum ToolType {
/// Unknown tool type.
///
/// This constant is used when the tool type is not known or is not relevant, such as for a trackball or other non-pointing device.
Unknown = ndk_sys::AMOTION_EVENT_TOOL_TYPE_UNKNOWN,
/// The tool is a finger.
Finger = ndk_sys::AMOTION_EVENT_TOOL_TYPE_FINGER,
/// The tool is a stylus.
Stylus = ndk_sys::AMOTION_EVENT_TOOL_TYPE_STYLUS,
/// The tool is a mouse.
Mouse = ndk_sys::AMOTION_EVENT_TOOL_TYPE_MOUSE,
/// The tool is an eraser or a stylus being used in an inverted posture.
Eraser = ndk_sys::AMOTION_EVENT_TOOL_TYPE_ERASER,
/// The tool is a palm and should be rejected
Palm = ndk_sys::AMOTION_EVENT_TOOL_TYPE_PALM,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// A bitfield representing the state of buttons during a motion event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-33)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ButtonState(pub u32);
impl ButtonState {
#[inline]
pub fn primary(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_PRIMARY != 0
}
#[inline]
pub fn secondary(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_SECONDARY != 0
}
#[inline]
pub fn teriary(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_TERTIARY != 0
}
#[inline]
pub fn back(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_BACK != 0
}
#[inline]
pub fn forward(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_FORWARD != 0
}
#[inline]
pub fn stylus_primary(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_STYLUS_PRIMARY != 0
}
#[inline]
pub fn stylus_secondary(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_BUTTON_STYLUS_SECONDARY != 0
}
}
impl From<ndk::event::ButtonState> for ButtonState {
fn from(value: ndk::event::ButtonState) -> Self {
Self(value.0)
}
}
/// A bitfield representing which edges were touched by a motion event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-31)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct EdgeFlags(pub u32);
impl EdgeFlags {
#[inline]
pub fn top(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_EDGE_FLAG_TOP != 0
}
#[inline]
pub fn bottom(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_EDGE_FLAG_BOTTOM != 0
}
#[inline]
pub fn left(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_EDGE_FLAG_LEFT != 0
}
#[inline]
pub fn right(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_EDGE_FLAG_RIGHT != 0
}
}
impl From<ndk::event::EdgeFlags> for EdgeFlags {
fn from(value: ndk::event::EdgeFlags) -> Self {
Self(value.0)
}
}
/// Flags associated with this [`MotionEvent`].
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-30)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct MotionEventFlags(pub u32);
impl MotionEventFlags {
#[inline]
pub fn window_is_obscured(self) -> bool {
self.0 & ndk_sys::AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED != 0
}
}
impl From<ndk::event::MotionEventFlags> for MotionEventFlags {
fn from(value: ndk::event::MotionEventFlags) -> Self {
Self(value.0)
}
}
/// Key actions.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-27)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum KeyAction {
Down = ndk_sys::AKEY_EVENT_ACTION_DOWN,
Up = ndk_sys::AKEY_EVENT_ACTION_UP,
Multiple = ndk_sys::AKEY_EVENT_ACTION_MULTIPLE,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Key codes.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-39)
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Keycode {
Unknown = ndk_sys::AKEYCODE_UNKNOWN,
SoftLeft = ndk_sys::AKEYCODE_SOFT_LEFT,
SoftRight = ndk_sys::AKEYCODE_SOFT_RIGHT,
Home = ndk_sys::AKEYCODE_HOME,
Back = ndk_sys::AKEYCODE_BACK,
Call = ndk_sys::AKEYCODE_CALL,
Endcall = ndk_sys::AKEYCODE_ENDCALL,
Keycode0 = ndk_sys::AKEYCODE_0,
Keycode1 = ndk_sys::AKEYCODE_1,
Keycode2 = ndk_sys::AKEYCODE_2,
Keycode3 = ndk_sys::AKEYCODE_3,
Keycode4 = ndk_sys::AKEYCODE_4,
Keycode5 = ndk_sys::AKEYCODE_5,
Keycode6 = ndk_sys::AKEYCODE_6,
Keycode7 = ndk_sys::AKEYCODE_7,
Keycode8 = ndk_sys::AKEYCODE_8,
Keycode9 = ndk_sys::AKEYCODE_9,
Star = ndk_sys::AKEYCODE_STAR,
Pound = ndk_sys::AKEYCODE_POUND,
DpadUp = ndk_sys::AKEYCODE_DPAD_UP,
DpadDown = ndk_sys::AKEYCODE_DPAD_DOWN,
DpadLeft = ndk_sys::AKEYCODE_DPAD_LEFT,
DpadRight = ndk_sys::AKEYCODE_DPAD_RIGHT,
DpadCenter = ndk_sys::AKEYCODE_DPAD_CENTER,
VolumeUp = ndk_sys::AKEYCODE_VOLUME_UP,
VolumeDown = ndk_sys::AKEYCODE_VOLUME_DOWN,
Power = ndk_sys::AKEYCODE_POWER,
Camera = ndk_sys::AKEYCODE_CAMERA,
Clear = ndk_sys::AKEYCODE_CLEAR,
A = ndk_sys::AKEYCODE_A,
B = ndk_sys::AKEYCODE_B,
C = ndk_sys::AKEYCODE_C,
D = ndk_sys::AKEYCODE_D,
E = ndk_sys::AKEYCODE_E,
F = ndk_sys::AKEYCODE_F,
G = ndk_sys::AKEYCODE_G,
H = ndk_sys::AKEYCODE_H,
I = ndk_sys::AKEYCODE_I,
J = ndk_sys::AKEYCODE_J,
K = ndk_sys::AKEYCODE_K,
L = ndk_sys::AKEYCODE_L,
M = ndk_sys::AKEYCODE_M,
N = ndk_sys::AKEYCODE_N,
O = ndk_sys::AKEYCODE_O,
P = ndk_sys::AKEYCODE_P,
Q = ndk_sys::AKEYCODE_Q,
R = ndk_sys::AKEYCODE_R,
S = ndk_sys::AKEYCODE_S,
T = ndk_sys::AKEYCODE_T,
U = ndk_sys::AKEYCODE_U,
V = ndk_sys::AKEYCODE_V,
W = ndk_sys::AKEYCODE_W,
X = ndk_sys::AKEYCODE_X,
Y = ndk_sys::AKEYCODE_Y,
Z = ndk_sys::AKEYCODE_Z,
Comma = ndk_sys::AKEYCODE_COMMA,
Period = ndk_sys::AKEYCODE_PERIOD,
AltLeft = ndk_sys::AKEYCODE_ALT_LEFT,
AltRight = ndk_sys::AKEYCODE_ALT_RIGHT,
ShiftLeft = ndk_sys::AKEYCODE_SHIFT_LEFT,
ShiftRight = ndk_sys::AKEYCODE_SHIFT_RIGHT,
Tab = ndk_sys::AKEYCODE_TAB,
Space = ndk_sys::AKEYCODE_SPACE,
Sym = ndk_sys::AKEYCODE_SYM,
Explorer = ndk_sys::AKEYCODE_EXPLORER,
Envelope = ndk_sys::AKEYCODE_ENVELOPE,
Enter = ndk_sys::AKEYCODE_ENTER,
Del = ndk_sys::AKEYCODE_DEL,
Grave = ndk_sys::AKEYCODE_GRAVE,
Minus = ndk_sys::AKEYCODE_MINUS,
Equals = ndk_sys::AKEYCODE_EQUALS,
LeftBracket = ndk_sys::AKEYCODE_LEFT_BRACKET,
RightBracket = ndk_sys::AKEYCODE_RIGHT_BRACKET,
Backslash = ndk_sys::AKEYCODE_BACKSLASH,
Semicolon = ndk_sys::AKEYCODE_SEMICOLON,
Apostrophe = ndk_sys::AKEYCODE_APOSTROPHE,
Slash = ndk_sys::AKEYCODE_SLASH,
At = ndk_sys::AKEYCODE_AT,
Num = ndk_sys::AKEYCODE_NUM,
Headsethook = ndk_sys::AKEYCODE_HEADSETHOOK,
Focus = ndk_sys::AKEYCODE_FOCUS,
Plus = ndk_sys::AKEYCODE_PLUS,
Menu = ndk_sys::AKEYCODE_MENU,
Notification = ndk_sys::AKEYCODE_NOTIFICATION,
Search = ndk_sys::AKEYCODE_SEARCH,
MediaPlayPause = ndk_sys::AKEYCODE_MEDIA_PLAY_PAUSE,
MediaStop = ndk_sys::AKEYCODE_MEDIA_STOP,
MediaNext = ndk_sys::AKEYCODE_MEDIA_NEXT,
MediaPrevious = ndk_sys::AKEYCODE_MEDIA_PREVIOUS,
MediaRewind = ndk_sys::AKEYCODE_MEDIA_REWIND,
MediaFastForward = ndk_sys::AKEYCODE_MEDIA_FAST_FORWARD,
Mute = ndk_sys::AKEYCODE_MUTE,
PageUp = ndk_sys::AKEYCODE_PAGE_UP,
PageDown = ndk_sys::AKEYCODE_PAGE_DOWN,
Pictsymbols = ndk_sys::AKEYCODE_PICTSYMBOLS,
SwitchCharset = ndk_sys::AKEYCODE_SWITCH_CHARSET,
ButtonA = ndk_sys::AKEYCODE_BUTTON_A,
ButtonB = ndk_sys::AKEYCODE_BUTTON_B,
ButtonC = ndk_sys::AKEYCODE_BUTTON_C,
ButtonX = ndk_sys::AKEYCODE_BUTTON_X,
ButtonY = ndk_sys::AKEYCODE_BUTTON_Y,
ButtonZ = ndk_sys::AKEYCODE_BUTTON_Z,
ButtonL1 = ndk_sys::AKEYCODE_BUTTON_L1,
ButtonR1 = ndk_sys::AKEYCODE_BUTTON_R1,
ButtonL2 = ndk_sys::AKEYCODE_BUTTON_L2,
ButtonR2 = ndk_sys::AKEYCODE_BUTTON_R2,
ButtonThumbl = ndk_sys::AKEYCODE_BUTTON_THUMBL,
ButtonThumbr = ndk_sys::AKEYCODE_BUTTON_THUMBR,
ButtonStart = ndk_sys::AKEYCODE_BUTTON_START,
ButtonSelect = ndk_sys::AKEYCODE_BUTTON_SELECT,
ButtonMode = ndk_sys::AKEYCODE_BUTTON_MODE,
Escape = ndk_sys::AKEYCODE_ESCAPE,
ForwardDel = ndk_sys::AKEYCODE_FORWARD_DEL,
CtrlLeft = ndk_sys::AKEYCODE_CTRL_LEFT,
CtrlRight = ndk_sys::AKEYCODE_CTRL_RIGHT,
CapsLock = ndk_sys::AKEYCODE_CAPS_LOCK,
ScrollLock = ndk_sys::AKEYCODE_SCROLL_LOCK,
MetaLeft = ndk_sys::AKEYCODE_META_LEFT,
MetaRight = ndk_sys::AKEYCODE_META_RIGHT,
Function = ndk_sys::AKEYCODE_FUNCTION,
Sysrq = ndk_sys::AKEYCODE_SYSRQ,
Break = ndk_sys::AKEYCODE_BREAK,
MoveHome = ndk_sys::AKEYCODE_MOVE_HOME,
MoveEnd = ndk_sys::AKEYCODE_MOVE_END,
Insert = ndk_sys::AKEYCODE_INSERT,
Forward = ndk_sys::AKEYCODE_FORWARD,
MediaPlay = ndk_sys::AKEYCODE_MEDIA_PLAY,
MediaPause = ndk_sys::AKEYCODE_MEDIA_PAUSE,
MediaClose = ndk_sys::AKEYCODE_MEDIA_CLOSE,
MediaEject = ndk_sys::AKEYCODE_MEDIA_EJECT,
MediaRecord = ndk_sys::AKEYCODE_MEDIA_RECORD,
F1 = ndk_sys::AKEYCODE_F1,
F2 = ndk_sys::AKEYCODE_F2,
F3 = ndk_sys::AKEYCODE_F3,
F4 = ndk_sys::AKEYCODE_F4,
F5 = ndk_sys::AKEYCODE_F5,
F6 = ndk_sys::AKEYCODE_F6,
F7 = ndk_sys::AKEYCODE_F7,
F8 = ndk_sys::AKEYCODE_F8,
F9 = ndk_sys::AKEYCODE_F9,
F10 = ndk_sys::AKEYCODE_F10,
F11 = ndk_sys::AKEYCODE_F11,
F12 = ndk_sys::AKEYCODE_F12,
NumLock = ndk_sys::AKEYCODE_NUM_LOCK,
Numpad0 = ndk_sys::AKEYCODE_NUMPAD_0,
Numpad1 = ndk_sys::AKEYCODE_NUMPAD_1,
Numpad2 = ndk_sys::AKEYCODE_NUMPAD_2,
Numpad3 = ndk_sys::AKEYCODE_NUMPAD_3,
Numpad4 = ndk_sys::AKEYCODE_NUMPAD_4,
Numpad5 = ndk_sys::AKEYCODE_NUMPAD_5,
Numpad6 = ndk_sys::AKEYCODE_NUMPAD_6,
Numpad7 = ndk_sys::AKEYCODE_NUMPAD_7,
Numpad8 = ndk_sys::AKEYCODE_NUMPAD_8,
Numpad9 = ndk_sys::AKEYCODE_NUMPAD_9,
NumpadDivide = ndk_sys::AKEYCODE_NUMPAD_DIVIDE,
NumpadMultiply = ndk_sys::AKEYCODE_NUMPAD_MULTIPLY,
NumpadSubtract = ndk_sys::AKEYCODE_NUMPAD_SUBTRACT,
NumpadAdd = ndk_sys::AKEYCODE_NUMPAD_ADD,
NumpadDot = ndk_sys::AKEYCODE_NUMPAD_DOT,
NumpadComma = ndk_sys::AKEYCODE_NUMPAD_COMMA,
NumpadEnter = ndk_sys::AKEYCODE_NUMPAD_ENTER,
NumpadEquals = ndk_sys::AKEYCODE_NUMPAD_EQUALS,
NumpadLeftParen = ndk_sys::AKEYCODE_NUMPAD_LEFT_PAREN,
NumpadRightParen = ndk_sys::AKEYCODE_NUMPAD_RIGHT_PAREN,
VolumeMute = ndk_sys::AKEYCODE_VOLUME_MUTE,
Info = ndk_sys::AKEYCODE_INFO,
ChannelUp = ndk_sys::AKEYCODE_CHANNEL_UP,
ChannelDown = ndk_sys::AKEYCODE_CHANNEL_DOWN,
ZoomIn = ndk_sys::AKEYCODE_ZOOM_IN,
ZoomOut = ndk_sys::AKEYCODE_ZOOM_OUT,
Tv = ndk_sys::AKEYCODE_TV,
Window = ndk_sys::AKEYCODE_WINDOW,
Guide = ndk_sys::AKEYCODE_GUIDE,
Dvr = ndk_sys::AKEYCODE_DVR,
Bookmark = ndk_sys::AKEYCODE_BOOKMARK,
Captions = ndk_sys::AKEYCODE_CAPTIONS,
Settings = ndk_sys::AKEYCODE_SETTINGS,
TvPower = ndk_sys::AKEYCODE_TV_POWER,
TvInput = ndk_sys::AKEYCODE_TV_INPUT,
StbPower = ndk_sys::AKEYCODE_STB_POWER,
StbInput = ndk_sys::AKEYCODE_STB_INPUT,
AvrPower = ndk_sys::AKEYCODE_AVR_POWER,
AvrInput = ndk_sys::AKEYCODE_AVR_INPUT,
ProgRed = ndk_sys::AKEYCODE_PROG_RED,
ProgGreen = ndk_sys::AKEYCODE_PROG_GREEN,
ProgYellow = ndk_sys::AKEYCODE_PROG_YELLOW,
ProgBlue = ndk_sys::AKEYCODE_PROG_BLUE,
AppSwitch = ndk_sys::AKEYCODE_APP_SWITCH,
Button1 = ndk_sys::AKEYCODE_BUTTON_1,
Button2 = ndk_sys::AKEYCODE_BUTTON_2,
Button3 = ndk_sys::AKEYCODE_BUTTON_3,
Button4 = ndk_sys::AKEYCODE_BUTTON_4,
Button5 = ndk_sys::AKEYCODE_BUTTON_5,
Button6 = ndk_sys::AKEYCODE_BUTTON_6,
Button7 = ndk_sys::AKEYCODE_BUTTON_7,
Button8 = ndk_sys::AKEYCODE_BUTTON_8,
Button9 = ndk_sys::AKEYCODE_BUTTON_9,
Button10 = ndk_sys::AKEYCODE_BUTTON_10,
Button11 = ndk_sys::AKEYCODE_BUTTON_11,
Button12 = ndk_sys::AKEYCODE_BUTTON_12,
Button13 = ndk_sys::AKEYCODE_BUTTON_13,
Button14 = ndk_sys::AKEYCODE_BUTTON_14,
Button15 = ndk_sys::AKEYCODE_BUTTON_15,
Button16 = ndk_sys::AKEYCODE_BUTTON_16,
LanguageSwitch = ndk_sys::AKEYCODE_LANGUAGE_SWITCH,
MannerMode = ndk_sys::AKEYCODE_MANNER_MODE,
Keycode3dMode = ndk_sys::AKEYCODE_3D_MODE,
Contacts = ndk_sys::AKEYCODE_CONTACTS,
Calendar = ndk_sys::AKEYCODE_CALENDAR,
Music = ndk_sys::AKEYCODE_MUSIC,
Calculator = ndk_sys::AKEYCODE_CALCULATOR,
ZenkakuHankaku = ndk_sys::AKEYCODE_ZENKAKU_HANKAKU,
Eisu = ndk_sys::AKEYCODE_EISU,
Muhenkan = ndk_sys::AKEYCODE_MUHENKAN,
Henkan = ndk_sys::AKEYCODE_HENKAN,
KatakanaHiragana = ndk_sys::AKEYCODE_KATAKANA_HIRAGANA,
Yen = ndk_sys::AKEYCODE_YEN,
Ro = ndk_sys::AKEYCODE_RO,
Kana = ndk_sys::AKEYCODE_KANA,
Assist = ndk_sys::AKEYCODE_ASSIST,
BrightnessDown = ndk_sys::AKEYCODE_BRIGHTNESS_DOWN,
BrightnessUp = ndk_sys::AKEYCODE_BRIGHTNESS_UP,
MediaAudioTrack = ndk_sys::AKEYCODE_MEDIA_AUDIO_TRACK,
Sleep = ndk_sys::AKEYCODE_SLEEP,
Wakeup = ndk_sys::AKEYCODE_WAKEUP,
Pairing = ndk_sys::AKEYCODE_PAIRING,
MediaTopMenu = ndk_sys::AKEYCODE_MEDIA_TOP_MENU,
Keycode11 = ndk_sys::AKEYCODE_11,
Keycode12 = ndk_sys::AKEYCODE_12,
LastChannel = ndk_sys::AKEYCODE_LAST_CHANNEL,
TvDataService = ndk_sys::AKEYCODE_TV_DATA_SERVICE,
VoiceAssist = ndk_sys::AKEYCODE_VOICE_ASSIST,
TvRadioService = ndk_sys::AKEYCODE_TV_RADIO_SERVICE,
TvTeletext = ndk_sys::AKEYCODE_TV_TELETEXT,
TvNumberEntry = ndk_sys::AKEYCODE_TV_NUMBER_ENTRY,
TvTerrestrialAnalog = ndk_sys::AKEYCODE_TV_TERRESTRIAL_ANALOG,
TvTerrestrialDigital = ndk_sys::AKEYCODE_TV_TERRESTRIAL_DIGITAL,
TvSatellite = ndk_sys::AKEYCODE_TV_SATELLITE,
TvSatelliteBs = ndk_sys::AKEYCODE_TV_SATELLITE_BS,
TvSatelliteCs = ndk_sys::AKEYCODE_TV_SATELLITE_CS,
TvSatelliteService = ndk_sys::AKEYCODE_TV_SATELLITE_SERVICE,
TvNetwork = ndk_sys::AKEYCODE_TV_NETWORK,
TvAntennaCable = ndk_sys::AKEYCODE_TV_ANTENNA_CABLE,
TvInputHdmi1 = ndk_sys::AKEYCODE_TV_INPUT_HDMI_1,
TvInputHdmi2 = ndk_sys::AKEYCODE_TV_INPUT_HDMI_2,
TvInputHdmi3 = ndk_sys::AKEYCODE_TV_INPUT_HDMI_3,
TvInputHdmi4 = ndk_sys::AKEYCODE_TV_INPUT_HDMI_4,
TvInputComposite1 = ndk_sys::AKEYCODE_TV_INPUT_COMPOSITE_1,
TvInputComposite2 = ndk_sys::AKEYCODE_TV_INPUT_COMPOSITE_2,
TvInputComponent1 = ndk_sys::AKEYCODE_TV_INPUT_COMPONENT_1,
TvInputComponent2 = ndk_sys::AKEYCODE_TV_INPUT_COMPONENT_2,
TvInputVga1 = ndk_sys::AKEYCODE_TV_INPUT_VGA_1,
TvAudioDescription = ndk_sys::AKEYCODE_TV_AUDIO_DESCRIPTION,
TvAudioDescriptionMixUp = ndk_sys::AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP,
TvAudioDescriptionMixDown = ndk_sys::AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN,
TvZoomMode = ndk_sys::AKEYCODE_TV_ZOOM_MODE,
TvContentsMenu = ndk_sys::AKEYCODE_TV_CONTENTS_MENU,
TvMediaContextMenu = ndk_sys::AKEYCODE_TV_MEDIA_CONTEXT_MENU,
TvTimerProgramming = ndk_sys::AKEYCODE_TV_TIMER_PROGRAMMING,
Help = ndk_sys::AKEYCODE_HELP,
NavigatePrevious = ndk_sys::AKEYCODE_NAVIGATE_PREVIOUS,
NavigateNext = ndk_sys::AKEYCODE_NAVIGATE_NEXT,
NavigateIn = ndk_sys::AKEYCODE_NAVIGATE_IN,
NavigateOut = ndk_sys::AKEYCODE_NAVIGATE_OUT,
StemPrimary = ndk_sys::AKEYCODE_STEM_PRIMARY,
Stem1 = ndk_sys::AKEYCODE_STEM_1,
Stem2 = ndk_sys::AKEYCODE_STEM_2,
Stem3 = ndk_sys::AKEYCODE_STEM_3,
DpadUpLeft = ndk_sys::AKEYCODE_DPAD_UP_LEFT,
DpadDownLeft = ndk_sys::AKEYCODE_DPAD_DOWN_LEFT,
DpadUpRight = ndk_sys::AKEYCODE_DPAD_UP_RIGHT,
DpadDownRight = ndk_sys::AKEYCODE_DPAD_DOWN_RIGHT,
MediaSkipForward = ndk_sys::AKEYCODE_MEDIA_SKIP_FORWARD,
MediaSkipBackward = ndk_sys::AKEYCODE_MEDIA_SKIP_BACKWARD,
MediaStepForward = ndk_sys::AKEYCODE_MEDIA_STEP_FORWARD,
MediaStepBackward = ndk_sys::AKEYCODE_MEDIA_STEP_BACKWARD,
SoftSleep = ndk_sys::AKEYCODE_SOFT_SLEEP,
Cut = ndk_sys::AKEYCODE_CUT,
Copy = ndk_sys::AKEYCODE_COPY,
Paste = ndk_sys::AKEYCODE_PASTE,
SystemNavigationUp = ndk_sys::AKEYCODE_SYSTEM_NAVIGATION_UP,
SystemNavigationDown = ndk_sys::AKEYCODE_SYSTEM_NAVIGATION_DOWN,
SystemNavigationLeft = ndk_sys::AKEYCODE_SYSTEM_NAVIGATION_LEFT,
SystemNavigationRight = ndk_sys::AKEYCODE_SYSTEM_NAVIGATION_RIGHT,
AllApps = ndk_sys::AKEYCODE_ALL_APPS,
Refresh = ndk_sys::AKEYCODE_REFRESH,
ThumbsUp = ndk_sys::AKEYCODE_THUMBS_UP,
ThumbsDown = ndk_sys::AKEYCODE_THUMBS_DOWN,
ProfileSwitch = ndk_sys::AKEYCODE_PROFILE_SWITCH,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Flags associated with [`KeyEvent`].
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-28)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct KeyEventFlags(pub u32);
impl KeyEventFlags {
#[inline]
pub fn cancelled(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_CANCELED != 0
}
#[inline]
pub fn cancelled_long_press(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_CANCELED_LONG_PRESS != 0
}
#[inline]
pub fn editor_action(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_EDITOR_ACTION != 0
}
#[inline]
pub fn fallback(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_FALLBACK != 0
}
#[inline]
pub fn from_system(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_FROM_SYSTEM != 0
}
#[inline]
pub fn keep_touch_mode(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_KEEP_TOUCH_MODE != 0
}
#[inline]
pub fn long_press(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_LONG_PRESS != 0
}
#[inline]
pub fn soft_keyboard(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_SOFT_KEYBOARD != 0
}
#[inline]
pub fn tracking(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_TRACKING != 0
}
#[inline]
pub fn virtual_hard_key(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_VIRTUAL_HARD_KEY != 0
}
#[inline]
pub fn woke_here(&self) -> bool {
self.0 & ndk_sys::AKEY_EVENT_FLAG_WOKE_HERE != 0
}
}
impl From<ndk::event::KeyEventFlags> for KeyEventFlags {
fn from(value: ndk::event::KeyEventFlags) -> Self {
Self(value.0)
}
}
/// This struct holds a span within a region of text from `start` to `end`.
///
/// The `start` index may be greater than the `end` index (swapping `start` and `end` will represent the same span)
@@ -78,6 +907,33 @@ pub struct TextInputState {
pub compose_region: Option<TextSpan>,
}
// Represents the action button on a soft keyboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(i32)]
pub enum TextInputAction {
/// Let receiver decide what logical action to perform
Unspecified = 0,
/// No action - receiver could instead interpret as an "enter" key that inserts a newline character
None = 1,
/// Navigate to the input location (such as a URL)
Go = 2,
/// Search based on the input text
Search = 3,
/// Send the input to the target
Send = 4,
/// Move to the next input field
Next = 5,
/// Indicate that input is done
Done = 6,
/// Move to the previous input field
Previous = 7,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(i32),
}
/// An exclusive, lending iterator for input events
pub struct InputIterator<'a> {
pub(crate) inner: crate::activity_impl::InputIteratorInner<'a>,
+149 -209
View File
@@ -1,21 +1,9 @@
use std::sync::Arc;
use jni::sys::jint;
use jni::{objects::Global, JavaVM};
use jni::{
objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
signature::{Primitive, ReturnType},
JNIEnv,
};
use jni_sys::jint;
use crate::{
input::{Keycode, MetaState},
jni_utils::CloneJavaVM,
};
use crate::{
error::{AppError, InternalAppError},
jni_utils,
};
use crate::error::{AppError, InternalAppError, InternalResult};
use crate::input::{Keycode, MetaState};
use crate::jni_utils;
/// An enum representing the types of keyboards that may generate key events
///
@@ -80,157 +68,86 @@ pub enum KeyMapChar {
CombiningAccent(char),
}
// I've also tried to think here about how to we could potentially automatically
// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
// so have intentionally limited the `Binding` being a very thin, un-opinionated
// wrapper based on basic JNI types.
/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
/// and can be shared with an `Arc` ref count.
///
/// The separation here also neatly helps us separate `InternalAppError` from
/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
/// public API.
#[derive(Debug)]
pub(crate) struct KeyCharacterMapBinding {
//vm: JavaVM,
klass: GlobalRef,
get_method_id: JMethodID,
get_dead_char_method_id: JStaticMethodID,
get_keyboard_type_method_id: JMethodID,
jni::bind_java_type! {
pub(crate) AKeyCharacterMap => "android.view.KeyCharacterMap",
methods {
priv fn _get(key_code: jint, meta_state: jint) -> jint,
priv static fn _get_dead_char(accent_char: jint, base_char: jint) -> jint,
priv fn _get_keyboard_type() -> jint,
}
}
impl KeyCharacterMapBinding {
pub(crate) fn new(env: &mut JNIEnv) -> Result<Self, InternalAppError> {
let binding = env.with_local_frame::<_, _, InternalAppError>(10, |env| {
let klass = env.find_class("android/view/KeyCharacterMap")?; // Creates a local ref
Ok(Self {
get_method_id: env.get_method_id(&klass, "get", "(II)I")?,
get_dead_char_method_id: env.get_static_method_id(
&klass,
"getDeadChar",
"(II)I",
)?,
get_keyboard_type_method_id: env.get_method_id(&klass, "getKeyboardType", "()I")?,
klass: env.new_global_ref(&klass)?,
})
})?;
Ok(binding)
}
pub fn get<'local>(
impl AKeyCharacterMap<'_> {
pub(crate) fn get<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
env: &'local mut jni::Env,
key_code: jint,
meta_state: jint,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_method_id` remains valid
// - we know that the signature of KeyCharacterMap::get is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
let unicode = unsafe {
env.call_method_unchecked(
key_map,
self.get_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(key_code).as_jni(),
JValue::Int(meta_state).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
self._get(env, key_code, meta_state)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
pub fn get_dead_char(
&self,
env: &mut JNIEnv,
pub(crate) fn get_dead_char(
env: &mut jni::Env,
accent_char: jint,
base_char: jint,
) -> Result<jint, InternalAppError> {
// Safety:
// - we know `get_dead_char_method_id` remains valid
// - we know that KeyCharacterMap::getDeadKey is a static method
// - we know that the signature of KeyCharacterMap::getDeadKey is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
// Urgh, it's pretty terrible that there's no ergonomic/safe way to get a JClass reference from a GlobalRef
// Safety: we don't do anything that would try to delete the JClass as if it were a real local reference
let klass = unsafe { JClass::from_raw(self.klass.as_obj().as_raw()) };
let unicode = unsafe {
env.call_static_method_unchecked(
&klass,
self.get_dead_char_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(accent_char).as_jni(),
JValue::Int(base_char).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
Self::_get_dead_char(env, accent_char, base_char)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
pub fn get_keyboard_type<'local>(
pub(crate) fn get_keyboard_type<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
env: &'local mut jni::Env,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_keyboard_type_method_id` remains valid
// - we know that the signature of KeyCharacterMap::getKeyboardType is `() -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
Ok(unsafe {
env.call_method_unchecked(
key_map,
self.get_keyboard_type_method_id,
ReturnType::Primitive(Primitive::Int),
&[],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
.i()
.unwrap())
self._get_keyboard_type(env)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
}
jni::bind_java_type! {
rust_type = AInputDevice,
java_type = "android.view.InputDevice",
type_map {
AKeyCharacterMap => "android.view.KeyCharacterMap",
},
methods {
static fn get_device(id: jint) -> AInputDevice,
fn get_key_character_map() -> AKeyCharacterMap,
}
}
// 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(Clone, Debug)]
#[derive(Debug)]
pub struct KeyCharacterMap {
jvm: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
jvm: JavaVM,
key_map: Global<AKeyCharacterMap<'static>>,
}
impl Clone for KeyCharacterMap {
fn clone(&self) -> Self {
let jvm = self.jvm.clone();
jvm.attach_current_thread(|env| -> jni::errors::Result<_> {
Ok(Self {
jvm: jvm.clone(),
key_map: env.new_global_ref(&self.key_map)?,
})
})
.expect("Failed to attach thread to JVM and clone key map")
}
}
impl KeyCharacterMap {
pub(crate) fn new(
jvm: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
) -> Self {
Self {
jvm,
binding,
key_map,
}
pub(crate) fn new(jvm: JavaVM, key_map: Global<AKeyCharacterMap<'static>>) -> Self {
Self { jvm, key_map }
}
/// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
@@ -246,40 +163,37 @@ impl KeyCharacterMap {
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result<KeyMapChar, AppError> {
let key_code = key_code.into();
let key_code: u32 = key_code.into();
let key_code = key_code as i32;
let meta_state = meta_state.0 as i32;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let vm = self.jvm.clone();
vm.attach_current_thread(|env| -> InternalResult<_> {
let unicode = self.key_map.get(env, key_code, meta_state)?;
let unicode = unicode as u32;
const COMBINING_ACCENT: u32 = 0x80000000;
const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
if unicode == 0 {
Ok(KeyMapChar::None)
} else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
let accent = unicode & COMBINING_ACCENT_MASK;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::CombiningAccent(unsafe {
char::from_u32_unchecked(accent)
}))
} else {
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::Unicode(unsafe {
char::from_u32_unchecked(unicode)
}))
}
})
.map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let unicode = self
.binding
.get(&mut env, self.key_map.as_obj(), key_code, meta_state)?;
let unicode = unicode as u32;
const COMBINING_ACCENT: u32 = 0x80000000;
const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
if unicode == 0 {
Ok(KeyMapChar::None)
} else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
let accent = unicode & COMBINING_ACCENT_MASK;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::CombiningAccent(unsafe {
char::from_u32_unchecked(accent)
}))
} else {
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::Unicode(unsafe {
char::from_u32_unchecked(unicode)
}))
}
err.into()
})
}
/// Get the character that is produced by combining the dead key producing accent with the key producing character c.
@@ -295,28 +209,24 @@ impl KeyCharacterMap {
accent_char: char,
base_char: char,
) -> Result<Option<char>, AppError> {
let accent_char = accent_char as jni_sys::jint;
let base_char = base_char as jni_sys::jint;
let accent_char = accent_char as jni::sys::jint;
let base_char = base_char as jni::sys::jint;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let vm = self.jvm.clone();
vm.attach_current_thread(|env| -> InternalResult<_> {
let unicode = AKeyCharacterMap::get_dead_char(env, accent_char, base_char)?;
let unicode = unicode as u32;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(if unicode == 0 {
None
} else {
Some(unsafe { char::from_u32_unchecked(unicode) })
})
})
.map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let unicode = self
.binding
.get_dead_char(&mut env, accent_char, base_char)?;
let unicode = unicode as u32;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(if unicode == 0 {
None
} else {
Some(unsafe { char::from_u32_unchecked(unicode) })
err.into()
})
}
@@ -330,19 +240,49 @@ impl KeyCharacterMap {
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let vm = self.jvm.clone();
vm.attach_current_thread(|env| -> InternalResult<_> {
let keyboard_type = self.key_map.get_keyboard_type(env)?;
let keyboard_type = keyboard_type as u32;
Ok(keyboard_type.into())
})
.map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let keyboard_type = self
.binding
.get_keyboard_type(&mut env, self.key_map.as_obj())?;
let keyboard_type = keyboard_type as u32;
Ok(keyboard_type.into())
err.into()
})
}
}
fn device_key_character_map_with_env(
env: &mut jni::Env<'_>,
device_id: i32,
) -> jni::errors::Result<KeyCharacterMap> {
let device = AInputDevice::get_device(env, device_id)?;
if device.is_null() {
// This isn't really an error from a JNI perspective but we would only expect
// this to return null for a device ID of zero or an invalid device ID.
log::error!("No input device with id {}", device_id);
return Err(jni::errors::Error::WrongObjectType);
}
let character_map = device.get_key_character_map(env)?;
let character_map = env.new_global_ref(character_map)?;
Ok(KeyCharacterMap::new(
env.get_java_vm().clone(),
character_map,
))
}
pub(crate) fn device_key_character_map(
jvm: JavaVM,
device_id: i32,
) -> InternalResult<KeyCharacterMap> {
jvm.attach_current_thread(|env| {
if device_id == 0 {
return Err(InternalAppError::JniBadArgument(
"Can't get key character map for non-physical device_id 0".into(),
));
}
device_key_character_map_with_env(env, device_id)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
})
}
+28 -111
View File
@@ -5,46 +5,21 @@
//!
//! These utilities help us check + clear exceptions and map them into Rust Errors.
use std::{ops::Deref, sync::Arc};
use crate::error::InternalAppError;
use jni::{
objects::{JObject, JString},
JavaVM,
};
use crate::{
error::{InternalAppError, InternalResult},
input::{KeyCharacterMap, KeyCharacterMapBinding},
};
// TODO: JavaVM should implement Clone
#[derive(Debug)]
pub(crate) struct CloneJavaVM {
pub jvm: JavaVM,
}
impl Clone for CloneJavaVM {
fn clone(&self) -> Self {
Self {
jvm: unsafe { JavaVM::from_raw(self.jvm.get_java_vm_pointer()).unwrap() },
}
}
}
impl CloneJavaVM {
pub unsafe fn from_raw(jvm: *mut jni_sys::JavaVM) -> InternalResult<Self> {
Ok(Self {
jvm: JavaVM::from_raw(jvm)?,
})
}
}
unsafe impl Send for CloneJavaVM {}
unsafe impl Sync for CloneJavaVM {}
impl Deref for CloneJavaVM {
type Target = JavaVM;
fn deref(&self) -> &Self::Target {
&self.jvm
fn try_get_stack_trace(
env: &mut jni::Env<'_>,
throwable: &jni::objects::JThrowable,
) -> jni::errors::Result<String> {
let stack_trace = throwable.get_stack_trace(env)?;
let len = stack_trace.len(env)?;
let mut trace = String::new();
for i in 0..len {
let element = stack_trace.get_element(env, i)?;
let element_jstr = element.try_to_string(env)?;
trace.push_str(&format!("{i}: {element_jstr}\n"));
}
Ok(trace)
}
/// Use with `.map_err()` to map `jni::errors::Error::JavaException` into a
@@ -55,41 +30,28 @@ impl Deref for CloneJavaVM {
///
/// This will also clear the exception
pub(crate) fn clear_and_map_exception_to_err(
env: &mut jni::JNIEnv<'_>,
env: &mut jni::Env<'_>,
err: jni::errors::Error,
) -> InternalAppError {
if matches!(err, jni::errors::Error::JavaException) {
let result = env.with_local_frame::<_, _, InternalAppError>(5, |env| {
let e = env.exception_occurred()?;
assert!(!e.is_null()); // should only be called after receiving a JavaException Result
env.exception_clear()?;
let class = env.get_object_class(&e)?;
//let get_stack_trace_method = env.get_method_id(&class, "getStackTrace", "()[Ljava/lang/StackTraceElement;")?;
let get_message_method =
env.get_method_id(&class, "getMessage", "()Ljava/lang/String;")?;
let msg = unsafe {
env.call_method_unchecked(
&e,
get_message_method,
jni::signature::ReturnType::Object,
&[],
)?
.l()
.unwrap()
let Some(e) = env.exception_occurred() else {
// should only be called after receiving a JavaException Result
unreachable!("JNI Error was JavaException but no exception was set");
};
let msg = unsafe { JString::from_raw(JObject::into_raw(msg)) };
let msg = env.get_string(&msg)?;
let msg: String = msg.into();
// TODO: get Java backtrace:
/*
if let JValue::Object(elements) = env.call_method_unchecked(&e, get_stack_trace_method, jni::signature::ReturnType::Array, &[])? {
let elements = env.auto_local(elements);
env.exception_clear();
let msg = e.get_message(env)?;
let mut msg: String = msg.to_string();
match try_get_stack_trace(env, &e) {
Ok(stack_trace) => {
msg.push_str("stack trace:\n");
msg.push_str(&stack_trace);
}
Err(err) => {
msg.push_str(&format!("\nfailed to get stack trace: {err:?}"));
}
}
*/
Ok(msg)
});
@@ -104,48 +66,3 @@ pub(crate) fn clear_and_map_exception_to_err(
err.into()
}
}
pub(crate) fn device_key_character_map(
jvm: CloneJavaVM,
key_map_binding: Arc<KeyCharacterMapBinding>,
device_id: i32,
) -> InternalResult<KeyCharacterMap> {
// Don't really need to 'attach' since this should be called from the app's main thread that
// should already be attached, but the redundancy should be fine
//
// Attach 'permanently' to avoid any chance of detaching the thread from the VM
let mut env = jvm.attach_current_thread_permanently()?;
// We don't want to accidentally leak any local references while we
// aren't going to be returning from here back to the JVM, to unwind, so
// we make a local frame
let character_map = env.with_local_frame::<_, _, jni::errors::Error>(10, |env| {
let input_device_class = env.find_class("android/view/InputDevice")?; // Creates a local ref
let device = env
.call_static_method(
input_device_class,
"getDevice",
"(I)Landroid/view/InputDevice;",
&[device_id.into()],
)?
.l()?; // Creates a local ref
let character_map = env
.call_method(
&device,
"getKeyCharacterMap",
"()Landroid/view/KeyCharacterMap;",
&[],
)?
.l()?;
let character_map = env.new_global_ref(character_map)?;
Ok(character_map)
})?;
Ok(KeyCharacterMap::new(
jvm.clone(),
key_map_binding,
character_map,
))
}
+15 -8
View File
@@ -566,7 +566,7 @@ impl AndroidApp {
/// with the [`jni`] crate (or similar crates) to make JNI calls that bridge
/// between native Rust code and Java/Kotlin code running within the JVM.
///
/// If you use the [`jni`] crate you can wrap this as a [`JavaVM`] via:
/// If you use the [`jni`] crate you can could this as a [`JavaVM`] via:
/// ```no_run
/// # use jni::JavaVM;
/// # let app: android_activity::AndroidApp = todo!();
@@ -581,24 +581,28 @@ impl AndroidApp {
/// Returns a JNI object reference for this application's JVM `Activity` as a pointer
///
/// If you use the [`jni`] crate you can wrap this as an object reference via:
/// If you use the [`jni`] crate you can cast this as a `JObject` reference via:
/// ```no_run
/// # use jni::objects::JObject;
/// # let app: android_activity::AndroidApp = todo!();
/// let activity = unsafe { JObject::from_raw(app.activity_as_ptr().cast()) };
/// # use jni::refs::Global;
/// # fn use_jni(env: &jni::Env, app: &android_activity::AndroidApp) -> jni::errors::Result<()> {
/// let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
/// // SAFETY: The pointer is valid as long as `app` is valid.
/// let activity = unsafe { env.as_cast_raw::<Global<JObject>>(&raw_activity_global)? };
/// # Ok(()) }
/// ```
///
/// # JNI Safety
///
/// Note that the object reference will be a JNI global reference, not a
/// local reference and it should not be deleted. Don't wrap the reference
/// in an [`AutoLocal`] which would try to explicitly delete the reference
/// when dropped. Similarly, don't wrap the reference as a [`GlobalRef`]
/// in an [`Auto`] which would try to explicitly delete the reference
/// when dropped. Similarly, don't wrap the reference as a [`Global`]
/// which would also try to explicitly delete the reference when dropped.
///
/// [`jni`]: https://crates.io/crates/jni
/// [`AutoLocal`]: https://docs.rs/jni/latest/jni/objects/struct.AutoLocal.html
/// [`GlobalRef`]: https://docs.rs/jni/latest/jni/objects/struct.GlobalRef.html
/// [`Auto`]: https://docs.rs/jni/latest/jni/refs/struct.Auto.html
/// [`Global`]: https://docs.rs/jni/latest/jni/refs/struct.Global.html
pub fn activity_as_ptr(&self) -> *mut c_void {
self.inner.read().unwrap().activity_as_ptr()
}
@@ -855,6 +859,9 @@ impl AndroidApp {
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`error::AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
///
/// This API should not be called with a `device_id` of `0`, since that indicates a non-physical
/// device and will result in a [`error::AppError::JavaError`].
pub fn device_key_character_map(&self, device_id: i32) -> Result<KeyCharacterMap> {
Ok(self
.inner
+71 -60
View File
@@ -9,11 +9,11 @@ use std::{
sync::{Arc, Condvar, Mutex, Weak},
};
use jni::{objects::JObject, refs::Global, vm::AttachConfig};
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
use crate::{
jni_utils::CloneJavaVM,
util::{abort_on_panic, forward_stdio_to_logcat, log_panic},
util::{abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic},
ConfigurationRef,
};
@@ -840,11 +840,9 @@ extern "C" fn ANativeActivity_onCreate(
abort_on_panic(|| {
let _join_log_forwarder = forward_stdio_to_logcat();
log::trace!(
eprintln!(
"Creating: {:p}, saved_state = {:p}, save_state_size = {}",
activity,
saved_state,
saved_state_size
activity, saved_state, saved_state_size
);
// Conceptually we associate a glue reference with the JVM main thread, and another
@@ -858,60 +856,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 _;
let jvm = abort_on_panic(|| unsafe {
let na = activity;
let jvm: *mut jni_sys::JavaVM = (*na).vm;
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
jvm.attach_current_thread_permanently().unwrap();
jvm
});
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
unsafe {
// Name thread - this needs to happen here after attaching to a JVM thread,
// since that changes the thread name to something like "Thread-2".
let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
// 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(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(log_panic);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
rust_glue.notify_main_thread_stopped_running();
rust_glue_entry(rust_glue, activity);
});
// Wait for thread to start.
@@ -924,3 +869,69 @@ extern "C" fn ANativeActivity_onCreate(
}
})
}
fn rust_glue_entry(rust_glue: NativeActivityGlue, activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
let (jvm, jni_activity) = unsafe {
let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast();
let jni_activity: jni::sys::jobject = (*activity).clazz as _; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), jni_activity.cast());
(jni::JavaVM::from_raw(jvm), jni_activity)
};
// Note: At this point we can assume jni::JavaVM::singleton is initialized
// Since this is a newly spawned thread then the JVM hasn't been attached to the
// thread yet.
//
// For compatibility we attach before calling the applications main function to
// allow it to assume the thread is attached before making JNI calls.
jvm.attach_current_thread_with_config(
|| AttachConfig::new().name("android_main"),
Some(16),
|env| -> jni::errors::Result<()> {
// SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance
let jni_activity = unsafe { env.as_cast_raw::<Global<JObject>>(&jni_activity)? };
if let Err(err) = init_android_main_thread(&jvm, &jni_activity) {
eprintln!(
"Failed to name Java thread and set thread context class loader: {err}"
);
}
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
unsafe {
// 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(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(log_panic);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
ndk_context::release_android_context();
}
rust_glue.notify_main_thread_stopped_running();
Ok(())
},
)
.expect("Failed to attach thread to JVM");
})
}
+50 -21
View File
@@ -1,10 +1,8 @@
use std::marker::PhantomData;
use ndk::event::ButtonState;
use crate::input::{
Axis, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction, MotionEventFlags, Pointer,
PointersIter, Source, ToolType,
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
/// A motion event
@@ -32,7 +30,13 @@ impl MotionEvent<'_> {
///
#[inline]
pub fn source(&self) -> Source {
self.ndk_event.source()
// XXX: we use `AInputEvent_getSource` directly (instead of calling
// ndk_event.source()) since we have our own `Source` enum that we
// share between backends, which may also capture unknown variants
// added in new versions of Android.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.into()
}
/// Get the device id associated with the event.
@@ -47,7 +51,13 @@ impl MotionEvent<'_> {
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
#[inline]
pub fn action(&self) -> MotionAction {
self.ndk_event.action()
// XXX: we use `AMotionEvent_getAction` directly since we have our own
// `MotionAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action =
unsafe { ndk_sys::AMotionEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 }
& ndk_sys::AMOTION_EVENT_ACTION_MASK;
action.into()
}
/// Returns which button has been modified during a press or release action.
@@ -57,11 +67,10 @@ impl MotionEvent<'_> {
///
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionButton())
#[inline]
#[cfg(feature = "api-level-33")]
#[doc(alias = "AMotionEvent_getActionButton")]
// TODO: Button enum to signify only one bitflag can be set?
pub fn action_button(&self) -> ButtonState {
self.ndk_event.action_button()
pub fn action_button(&self) -> Button {
let action_button =
unsafe { ndk_sys::AMotionEvent_getActionButton(self.ndk_event.ptr().as_ptr()) as u32 };
action_button.into()
}
/// Returns the pointer index of an `Up` or `Down` event.
@@ -145,7 +154,7 @@ impl MotionEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state()
self.ndk_event.meta_state().into()
}
/// Returns the button state during this event, as a bitfield.
@@ -154,7 +163,7 @@ impl MotionEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
#[inline]
pub fn button_state(&self) -> ButtonState {
self.ndk_event.button_state()
self.ndk_event.button_state().into()
}
/// Returns the time of the start of this gesture, in the `java.lang.System.nanoTime()` time
@@ -173,7 +182,7 @@ impl MotionEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getedgeflags)
#[inline]
pub fn edge_flags(&self) -> EdgeFlags {
self.ndk_event.edge_flags()
self.ndk_event.edge_flags().into()
}
/// Returns the time of this event, in the `java.lang.System.nanoTime()` time base
@@ -191,7 +200,7 @@ impl MotionEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getflags)
#[inline]
pub fn flags(&self) -> MotionEventFlags {
self.ndk_event.flags()
self.ndk_event.flags().into()
}
/* Missing from GameActivity currently...
@@ -252,7 +261,8 @@ impl PointerImpl<'_> {
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let value: i32 = axis.into();
let value: u32 = axis.into();
let value = value as i32;
self.ndk_pointer.axis_value(value.into())
}
@@ -269,6 +279,7 @@ impl PointerImpl<'_> {
#[inline]
pub fn tool_type(&self) -> ToolType {
let value: i32 = self.ndk_pointer.tool_type().into();
let value = value as u32;
value.into()
}
}
@@ -320,9 +331,16 @@ impl KeyEvent<'_> {
}
/// Get the source of the event.
///
#[inline]
pub fn source(&self) -> Source {
self.ndk_event.source()
// XXX: we use `AInputEvent_getSource` directly (instead of calling
// ndk_event.source()) since we have our own `Source` enum that we
// share between backends, which may also capture unknown variants
// added in new versions of Android.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.into()
}
/// Get the device id associated with the event.
@@ -337,7 +355,11 @@ impl KeyEvent<'_> {
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
#[inline]
pub fn action(&self) -> KeyAction {
self.ndk_event.action()
// XXX: we use `AInputEvent_getAction` directly since we have our own
// `KeyAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action = unsafe { ndk_sys::AKeyEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
action.into()
}
/// Returns the last time the key was pressed. This is on the scale of
@@ -366,7 +388,12 @@ impl KeyEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
#[inline]
pub fn key_code(&self) -> Keycode {
self.ndk_event.key_code()
// XXX: we use `AInputEvent_getKeyCode` directly since we have our own
// `Keycode` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let keycode =
unsafe { ndk_sys::AKeyEvent_getKeyCode(self.ndk_event.ptr().as_ptr()) as u32 };
keycode.into()
}
/// Returns the number of repeats of a key.
@@ -393,12 +420,13 @@ impl KeyEvent<'_> {
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state()
self.ndk_event.meta_state().into()
}
}
// We use our own wrapper type for input events to have better consistency
// with GameActivity
// with GameActivity and ensure the enum can be extended without needing a
// semver bump
/// Enum of possible input events
#[derive(Debug)]
#[non_exhaustive]
@@ -406,4 +434,5 @@ pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}
+42 -52
View File
@@ -6,15 +6,15 @@ use std::ptr::NonNull;
use std::sync::{Arc, Mutex, RwLock, Weak};
use std::time::Duration;
use jni::JavaVM;
use libc::c_void;
use log::{error, trace};
use ndk::input_queue::InputQueue;
use ndk::{asset::AssetManager, native_window::NativeWindow};
use crate::error::InternalResult;
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
use crate::input::{device_key_character_map, Axis, KeyCharacterMap};
use crate::input::{TextInputState, TextSpan};
use crate::jni_utils::{self, CloneJavaVM};
use crate::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
};
@@ -84,50 +84,47 @@ impl AndroidAppWaker {
}
impl AndroidApp {
pub(crate) fn new(native_activity: NativeActivityGlue, jvm: CloneJavaVM) -> Self {
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
pub(crate) fn new(native_activity: NativeActivityGlue, jvm: JavaVM) -> 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 key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
Ok(b) => b,
Err(err) => {
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
let app = Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm: jvm.clone(),
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
};
{
let mut 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,
main_fd,
LOOPER_ID_MAIN,
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
None,
//&mut guard.cmd_poll_source as *mut _ as *mut _);
ptr::null_mut(),
);
}
}
};
let app = Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm,
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
key_map_binding: Arc::new(key_map_binding),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
};
{
let mut 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,
main_fd,
LOOPER_ID_MAIN,
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
None,
//&mut guard.cmd_poll_source as *mut _ as *mut _);
ptr::null_mut(),
);
}
}
app
Ok(app)
})
.expect("Failed to create AndroidApp instance")
}
}
@@ -140,14 +137,11 @@ unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
pub(crate) jvm: CloneJavaVM,
pub(crate) jvm: JavaVM,
pub(crate) native_activity: NativeActivityGlue,
looper: Looper,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
@@ -396,11 +390,7 @@ impl AndroidAppInner {
let key_map = match guard.entry(device_id) {
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
std::collections::hash_map::Entry::Vacant(vacant) => {
let character_map = jni_utils::device_key_character_map(
self.jvm.clone(),
self.key_map_binding.clone(),
device_id,
)?;
let character_map = device_key_character_map(self.jvm.clone(), device_id)?;
vacant.insert(character_map.clone());
character_map
}
+31
View File
@@ -1,3 +1,8 @@
use jni::{
jni_str,
objects::{JObject, JString, JThread},
vm::JavaVM,
};
use log::{error, Level};
use std::{
ffi::{CStr, CString},
@@ -111,3 +116,29 @@ pub(crate) fn abort_on_panic<R>(f: impl FnOnce() -> R) -> R {
std::process::abort();
})
}
/// Name the Java Thread + native thread "android_main" and set the Java Thread context class loader
/// so that jni code can more-easily find non-system Java classes.
pub(crate) fn init_android_main_thread(
vm: &JavaVM,
jni_activity: &JObject,
) -> jni::errors::Result<()> {
vm.with_local_frame(10, |env| -> jni::errors::Result<()> {
let activity_class = env.get_object_class(jni_activity)?;
let class_loader = activity_class.get_class_loader(env)?;
let thread = JThread::current_thread(env)?;
thread.set_context_class_loader(env, &class_loader)?;
let thread_name = JString::from_jni_str(env, jni_str!("android_main"))?;
thread.set_name(env, &thread_name)?;
// Also name native thread - this needs to happen here after attaching to a JVM thread,
// since that changes the thread name to something like "Thread-2".
unsafe {
let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
let _ = libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
}
Ok(())
})
}
+3
View File
@@ -15,3 +15,6 @@ ndk = "0.9.0"
[lib]
name="main"
crate_type=["cdylib"]
[patch.crates-io]
jni = { git = "https://github.com/jni-rs/jni-rs.git", branch = "release-0.22" }
+3
View File
@@ -182,3 +182,6 @@ label = "Application Name"
#path = "/rust-windowing/android-ndk-rs/tree/master/cargo-apk"
#path_prefix = "/rust-windowing/"
#mime_type = "image/jpeg"
[patch.crates-io]
jni = { git = "https://github.com/jni-rs/jni-rs.git", branch = "release-0.22" }