From f3e5c136929a27fd38fc91d86faa1627d36361f0 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Tue, 1 Apr 2025 12:21:19 +0100 Subject: [PATCH 1/3] Re-export 'ndk' and 'ndk_sys' crates Since we expose `ndk` types in the public API it makes sense to re-export these APIs so users of android-activity can defer to these without needing to manually sync the versions for explicit dependencies. --- android-activity/CHANGELOG.md | 36 ++++++++++++++++++----------------- android-activity/src/lib.rs | 10 ++++++++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index 3d3524b..7b005b5 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- 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)) + ## [0.6.0] - 2024-04-26 ### Changed @@ -25,32 +27,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Avoids depending on default features for `ndk` crate to avoid pulling in any `raw-window-handle` dependencies ([#142](https://github.com/rust-mobile/android-activity/pull/142)) - **Note:** Technically, this could be observed as a breaking change in case you - were depending on the `rwh_06` feature that was enabled by default in the - `ndk` crate. This could be observed via the `NativeWindow` type (exposed via - `AndroidApp::native_window()`) no longer implementing `rwh_06::HasWindowHandle`. + **Note:** Technically, this could be observed as a breaking change in case you + were depending on the `rwh_06` feature that was enabled by default in the + `ndk` crate. This could be observed via the `NativeWindow` type (exposed via + `AndroidApp::native_window()`) no longer implementing `rwh_06::HasWindowHandle`. - In the unlikely case that you were depending on the `ndk`'s `rwh_06` API - being enabled by default via `android-activity`'s `ndk` dependency, your crate - should explicitly enable the `rwh_06` feature for the `ndk` crate. + In the unlikely case that you were depending on the `ndk`'s `rwh_06` API + being enabled by default via `android-activity`'s `ndk` dependency, your crate + should explicitly enable the `rwh_06` feature for the `ndk` crate. - As far as could be seen though, it's not expected that anything was - depending on this (e.g. anything based on Winit enables the `ndk` feature - based on an equivalent `winit` feature). + As far as could be seen though, it's not expected that anything was + depending on this (e.g. anything based on Winit enables the `ndk` feature + based on an equivalent `winit` feature). - The benefit of the change is that it can help avoid a redundant - `raw-window-handle 0.6` dependency in projects that still need to use older - (non-default) `raw-window-handle` versions. (Though note that this may be - awkward to achieve in practice since other crates that depend on the `ndk` - are still likely to use default features and also pull in - `raw-window-handles 0.6`) + The benefit of the change is that it can help avoid a redundant + `raw-window-handle 0.6` dependency in projects that still need to use older + (non-default) `raw-window-handle` versions. (Though note that this may be + awkward to achieve in practice since other crates that depend on the `ndk` + are still likely to use default features and also pull in + `raw-window-handles 0.6`) - The IO thread now gets named `stdio-to-logcat` and main thread is named `android_main` ([#145](https://github.com/rust-mobile/android-activity/pull/145)) - Improved IO error handling in `stdio-to-logcat` IO loop. ([#133](https://github.com/rust-mobile/android-activity/pull/133)) ## [0.5.0] - 2023-10-16 ### Added -- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions () +- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions ([#138](https://github.com/rust-mobile/android-activity/pull/138)) ### Changed - rust-version bumped to 0.68 ([#123](https://github.com/rust-mobile/android-activity/pull/123)) diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index 45cc887..e8984fe 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -119,12 +119,17 @@ use std::sync::Arc; use std::sync::RwLock; use std::time::Duration; -use input::KeyCharacterMap; +use bitflags::bitflags; use libc::c_void; + use ndk::asset::AssetManager; use ndk::native_window::NativeWindow; -use bitflags::bitflags; +// Since we expose `ndk` types in our public API it's convenient if crates can +// defer to these re-exported APIs and avoid having to bump explicit +// dependencies when they pull in new releases of android-activity. +pub use ndk; +pub use ndk_sys; #[cfg(not(target_os = "android"))] compile_error!("android-activity only supports compiling for Android"); @@ -162,6 +167,7 @@ pub mod error; use error::Result; pub mod input; +use input::KeyCharacterMap; mod config; pub use config::ConfigurationRef; From 984b62d4d2fe70f37cede40df2f3e6fbc88681c4 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Mon, 11 Aug 2025 14:06:27 +0100 Subject: [PATCH 2/3] Revert "input: Replace open-coded types with `ndk::event` definitions (#163)" This reverts commit 51d05d48c8f59a7e9a1d41c1577870842206ce32. This was a breaking change that we can't keep on the release-0.6 branch --- android-activity/CHANGELOG.md | 48 +- android-activity/Cargo.toml | 1 - android-activity/src/game_activity/input.rs | 24 +- android-activity/src/game_activity/mod.rs | 6 +- android-activity/src/input.rs | 837 +++++++++++++++++- android-activity/src/input/sdk.rs | 6 +- android-activity/src/native_activity/input.rs | 70 +- 7 files changed, 921 insertions(+), 71 deletions(-) diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index be0c1ac..262a027 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -6,19 +6,11 @@ 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. - - > [!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`. +### Added +- 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)) +### Changed - 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)) - 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 @@ -40,32 +32,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Avoids depending on default features for `ndk` crate to avoid pulling in any `raw-window-handle` dependencies ([#142](https://github.com/rust-mobile/android-activity/pull/142)) - **Note:** Technically, this could be observed as a breaking change in case you - were depending on the `rwh_06` feature that was enabled by default in the - `ndk` crate. This could be observed via the `NativeWindow` type (exposed via - `AndroidApp::native_window()`) no longer implementing `rwh_06::HasWindowHandle`. + **Note:** Technically, this could be observed as a breaking change in case you + were depending on the `rwh_06` feature that was enabled by default in the + `ndk` crate. This could be observed via the `NativeWindow` type (exposed via + `AndroidApp::native_window()`) no longer implementing `rwh_06::HasWindowHandle`. - In the unlikely case that you were depending on the `ndk`'s `rwh_06` API - being enabled by default via `android-activity`'s `ndk` dependency, your crate - should explicitly enable the `rwh_06` feature for the `ndk` crate. + In the unlikely case that you were depending on the `ndk`'s `rwh_06` API + being enabled by default via `android-activity`'s `ndk` dependency, your crate + should explicitly enable the `rwh_06` feature for the `ndk` crate. - As far as could be seen though, it's not expected that anything was - depending on this (e.g. anything based on Winit enables the `ndk` feature - based on an equivalent `winit` feature). + As far as could be seen though, it's not expected that anything was + depending on this (e.g. anything based on Winit enables the `ndk` feature + based on an equivalent `winit` feature). - The benefit of the change is that it can help avoid a redundant - `raw-window-handle 0.6` dependency in projects that still need to use older - (non-default) `raw-window-handle` versions. (Though note that this may be - awkward to achieve in practice since other crates that depend on the `ndk` - are still likely to use default features and also pull in - `raw-window-handles 0.6`) + The benefit of the change is that it can help avoid a redundant + `raw-window-handle 0.6` dependency in projects that still need to use older + (non-default) `raw-window-handle` versions. (Though note that this may be + awkward to achieve in practice since other crates that depend on the `ndk` + are still likely to use default features and also pull in + `raw-window-handles 0.6`) - The IO thread now gets named `stdio-to-logcat` and main thread is named `android_main` ([#145](https://github.com/rust-mobile/android-activity/pull/145)) - Improved IO error handling in `stdio-to-logcat` IO loop. ([#133](https://github.com/rust-mobile/android-activity/pull/133)) ## [0.5.0] - 2023-10-16 ### Added -- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions ([#138](https://github.com/rust-mobile/android-activity/pull/138)) +- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions () ### Changed - rust-version bumped to 0.68 ([#123](https://github.com/rust-mobile/android-activity/pull/123)) diff --git a/android-activity/Cargo.toml b/android-activity/Cargo.toml index 276d1bd..ec46358 100644 --- a/android-activity/Cargo.toml +++ b/android-activity/Cargo.toml @@ -32,7 +32,6 @@ 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" diff --git a/android-activity/src/game_activity/input.rs b/android-activity/src/game_activity/input.rs index f8fac2b..66b94fe 100644 --- a/android-activity/src/game_activity/input.rs +++ b/android-activity/src/game_activity/input.rs @@ -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 @@ -49,7 +47,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 +63,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 +176,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 +278,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 +297,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 +665,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 +681,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 +717,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() } diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index 70b6941..f430daa 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -544,11 +544,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 { diff --git a/android-activity/src/input.rs b/android-activity/src/input.rs index 909f895..96c7d68 100644 --- a/android-activity/src/input.rs +++ b/android-activity/src/input.rs @@ -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` and `From` 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 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` and `From` 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` and `From` 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` and `From` 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 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 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 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` and `From` 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` and `From` 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 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) diff --git a/android-activity/src/input/sdk.rs b/android-activity/src/input/sdk.rs index 978ad5a..b40a7df 100644 --- a/android-activity/src/input/sdk.rs +++ b/android-activity/src/input/sdk.rs @@ -246,8 +246,10 @@ 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 { - let key_code = key_code.into(); - let meta_state = meta_state.0 as i32; + let key_code: u32 = key_code.into(); + let key_code = key_code as jni_sys::jint; + let meta_state: u32 = meta_state.0; + let meta_state = meta_state 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 diff --git a/android-activity/src/native_activity/input.rs b/android-activity/src/native_activity/input.rs index 1559108..96187ec 100644 --- a/android-activity/src/native_activity/input.rs +++ b/android-activity/src/native_activity/input.rs @@ -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] From 937cc699c457c4e0cbe0e4c5c113786358d5af1a Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Mon, 11 Aug 2025 15:54:30 +0100 Subject: [PATCH 3/3] Update to jni 0.22 and jni-sys 0.4 --- Cargo.toml | 4 + android-activity/Cargo.toml | 3 +- android-activity/src/game_activity/ffi.rs | 2 +- android-activity/src/game_activity/mod.rs | 156 ++++---- android-activity/src/input/sdk.rs | 381 +++++++++---------- android-activity/src/jni_utils.rs | 141 ++----- android-activity/src/lib.rs | 15 +- android-activity/src/native_activity/glue.rs | 131 ++++--- android-activity/src/native_activity/mod.rs | 89 ++--- android-activity/src/util.rs | 29 ++ 10 files changed, 443 insertions(+), 508 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5673fe4..3f7a693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,7 @@ resolver = "2" members = ["android-activity"] exclude = ["examples"] + +[patch.crates-io] +#jni = { path = "/home/rib/src/jni-rs/jni-git-dev0/crates/jni" } +jni = { git = "https://github.com/jni-rs/jni-rs.git", branch = "release-0.22" } \ No newline at end of file diff --git a/android-activity/Cargo.toml b/android-activity/Cargo.toml index ec46358..ad4aefb 100644 --- a/android-activity/Cargo.toml +++ b/android-activity/Cargo.toml @@ -35,9 +35,8 @@ api-level-30 = ["ndk/api-level-30"] [dependencies] log = "0.4" -jni-sys = "0.3" cesu8 = "1" -jni = "0.21" +jni = "0.22" ndk-sys = "0.6.0" ndk = { version = "0.9.0", default-features = false } ndk-context = "0.1.1" diff --git a/android-activity/src/game_activity/ffi.rs b/android-activity/src/game_activity/ffi.rs index 51063ff..576d489 100644 --- a/android-activity/src/game_activity/ffi.rs +++ b/android-activity/src/game_activity/ffi.rs @@ -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}; diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index f430daa..c2171e4 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -10,10 +10,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}; @@ -23,9 +25,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::{Axis, KeyCharacterMap, device_key_character_map}; +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, }; @@ -121,32 +125,31 @@ impl AndroidAppWaker { } impl AndroidApp { - pub(crate) unsafe fn from_ptr(ptr: NonNull, jvm: CloneJavaVM) -> Self { - let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp - - 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)); + pub(crate) unsafe fn from_ptr(ptr: NonNull, 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:?}"); + }; - 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), - })), - } + // 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)); + + 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") } } @@ -253,14 +256,11 @@ impl NativeAppGlue { #[derive(Debug)] pub struct AndroidAppInner { - pub(crate) jvm: CloneJavaVM, + pub(crate) jvm: jni::JavaVM, native_app: NativeAppGlue, config: ConfigurationRef, native_window: RwLock>, - /// Shared JNI bindings for the `KeyCharacterMap` class - key_map_binding: Arc, - /// A table of `KeyCharacterMap`s per `InputDevice` ID /// these are used to be able to map key presses to unicode /// characters @@ -530,9 +530,8 @@ 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( + let character_map = device_key_character_map( self.jvm.clone(), - self.key_map_binding.clone(), device_id, )?; vacant.insert(character_map.clone()); @@ -875,7 +874,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, @@ -909,53 +908,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 + + // 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::>(&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}"); + } - 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()); - - 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); + 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); - // 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"); }) } diff --git a/android-activity/src/input/sdk.rs b/android-activity/src/input/sdk.rs index b40a7df..95cc8e0 100644 --- a/android-activity/src/input/sdk.rs +++ b/android-activity/src/input/sdk.rs @@ -1,20 +1,13 @@ -use std::sync::Arc; - use jni::{ - objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue}, - signature::{Primitive, ReturnType}, - JNIEnv, + objects::{Global}, + JavaVM, }; -use jni_sys::jint; -use crate::{ - input::{Keycode, MetaState}, - jni_utils::CloneJavaVM, -}; +use crate::{error::InternalResult, jni_utils}; +use crate::input::{Keycode, MetaState}; use crate::{ error::{AppError, InternalAppError}, - jni_utils, }; /// An enum representing the types of keyboards that may generate key events @@ -80,155 +73,121 @@ 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. +// XXX: currently bind_java_type always makes the type public, which we don't want here +mod binding { + use jni::sys::jint; -/// 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, -} + use crate::{error::InternalAppError, jni_utils}; -impl KeyCharacterMapBinding { - pub(crate) fn new(env: &mut JNIEnv) -> Result { - 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) + jni::bind_java_type! { + 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, + } } - pub fn get<'local>( - &self, - env: &'local mut JNIEnv, - key_map: impl AsRef>, - key_code: jint, - meta_state: jint, - ) -> Result { - 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(), - ], - ) + impl AKeyCharacterMap<'_> { + pub(crate) fn get<'local>( + &self, + env: &'local mut jni::Env, + key_code: jint, + meta_state: jint, + ) -> Result { + self._get(env, key_code, meta_state) + .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err)) + } + + pub(crate) fn get_dead_char( + env: &mut jni::Env, + accent_char: jint, + base_char: jint, + ) -> Result { + Self::_get_dead_char(env, accent_char, base_char) + .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err)) + } + + pub(crate) fn get_keyboard_type<'local>( + &self, + env: &'local mut jni::Env, + ) -> Result { + self._get_keyboard_type(env) + .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err)) } - .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?; - Ok(unicode.i().unwrap()) } - pub fn get_dead_char( - &self, - env: &mut JNIEnv, - accent_char: jint, - base_char: jint, - ) -> Result { - // 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(), - ], - ) + jni::bind_java_type! { + rust_type = AInputDevice, + java_type = "android.view.InputDevice", + type_map { + // FIXME: bind_java_type should automatically declare Self! + AInputDevice => "android.view.InputDevice", + AKeyCharacterMap => "android.view.KeyCharacterMap", + }, + methods { + static fn get_device(id: jint) -> AInputDevice, + fn get_key_character_map() -> AKeyCharacterMap, } - .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?; - Ok(unicode.i().unwrap()) } - pub fn get_keyboard_type<'local>( - &self, - env: &'local mut JNIEnv, - key_map: impl AsRef>, - ) -> Result { - 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), - &[], - ) + /* + impl AInputDevice<'_> { + pub fn get_device<'env>( + env: &mut jni::Env<'env>, + id: i32, + ) -> Result, InternalAppError> { + AInputDevice::_get_device(env, id) + .map_err(|err| clear_and_map_exception_to_err(env, err)) + } + pub fn get_key_character_map<'local>( + &self, + env: &'local mut jni::Env, + ) -> Result, InternalAppError> { + self._get_key_character_map(env) + .map_err(|err| clear_and_map_exception_to_err(env, err)) } - .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))? - .i() - .unwrap()) + }*/ + + pub fn jni_init(env: &jni::Env) -> jni::errors::Result<()> { + let _ = AKeyCharacterMapAPI::get(env, &Default::default())?; + let _ = AInputDeviceAPI::get(env, &Default::default())?; + Ok(()) } } +// 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<()> { + binding::jni_init(env) +} + /// Describes the keys provided by a keyboard device and their associated labels. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct KeyCharacterMap { - jvm: CloneJavaVM, - binding: Arc, - key_map: GlobalRef, + jvm: JavaVM, + key_map: Global>, +} +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, - key_map: GlobalRef, + jvm: JavaVM, + key_map: Global>, ) -> Self { Self { jvm, - binding, key_map, } } @@ -247,41 +206,39 @@ impl KeyCharacterMap { /// is caught. pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result { let key_code: u32 = key_code.into(); - let key_code = key_code as jni_sys::jint; + let key_code = key_code as jni::sys::jint; let meta_state: u32 = meta_state.0; - let meta_state = meta_state 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 meta_state = meta_state as jni::sys::jint; + + 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. @@ -297,28 +254,24 @@ impl KeyCharacterMap { accent_char: char, base_char: char, ) -> Result, AppError> { - 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 accent_char = accent_char as jni::sys::jint; + let base_char = base_char as jni::sys::jint; + + let vm = self.jvm.clone(); + vm.attach_current_thread(|env| -> InternalResult<_> { + let unicode = binding::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() }) } @@ -332,19 +285,39 @@ 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 { - // 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() + }) } } + + +pub(crate) fn device_key_character_map_with_env( + env: &mut jni::Env<'_>, + device_id: i32, +) -> jni::errors::Result { + let device = binding::AInputDevice::get_device(env, device_id)?; + 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 { + jvm.attach_current_thread(|env| { + device_key_character_map_with_env(env, device_id) + .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err)) + }) +} \ No newline at end of file diff --git a/android-activity/src/jni_utils.rs b/android-activity/src/jni_utils.rs index dcf6083..95d2aaa 100644 --- a/android-activity/src/jni_utils.rs +++ b/android-activity/src/jni_utils.rs @@ -5,46 +5,23 @@ //! //! These utilities help us check + clear exceptions and map them into Rust Errors. -use std::{ops::Deref, sync::Arc}; - -use jni::{ - objects::{JObject, JString}, - JavaVM, -}; - use crate::{ - error::{InternalAppError, InternalResult}, - input::{KeyCharacterMap, KeyCharacterMapBinding}, + error::InternalAppError, }; -// 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 { - 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 { + 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 +32,30 @@ 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 +70,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, - device_id: i32, -) -> InternalResult { - // 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, - )) -} diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index 42c207c..6ae0f9a 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -556,7 +556,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!(); @@ -571,19 +571,22 @@ 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()) }; + /// # fn use_jni(env: &jni::Env, app: &android_activity::AndroidApp) -> jni::errors::Result<()> { + /// let raw_activity_global = app.activity_as_ptr(); + /// // SAFETY: The pointer is valid as long as `app` is valid. + /// let activity = unsafe { env.as_cast_raw::>(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 diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs index dab3e29..d3d25bd 100644 --- a/android-activity/src/native_activity/glue.rs +++ b/android-activity/src/native_activity/glue.rs @@ -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, }; @@ -834,11 +834,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 @@ -852,60 +850,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. @@ -918,3 +863,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::>(&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"); + }) +} diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs index 88a9fe8..88cbd3b 100644 --- a/android-activity/src/native_activity/mod.rs +++ b/android-activity/src/native_activity/mod.rs @@ -8,15 +8,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::{Axis, KeyCharacterMap, device_key_character_map}; use crate::input::{TextInputState, TextSpan}; -use crate::jni_utils::{self, CloneJavaVM}; use crate::{ util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags, }; @@ -86,50 +86,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 - - let key_map_binding = match KeyCharacterMapBinding::new(&mut env) { - Ok(b) => b, - Err(err) => { - panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}"); - } - }; + 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 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 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 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") } } @@ -142,14 +139,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, - /// A table of `KeyCharacterMap`s per `InputDevice` ID /// these are used to be able to map key presses to unicode /// characters @@ -397,9 +391,8 @@ 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( + let character_map = device_key_character_map( self.jvm.clone(), - self.key_map_binding.clone(), device_id, )?; vacant.insert(character_map.clone()); diff --git a/android-activity/src/util.rs b/android-activity/src/util.rs index b7e774d..21090f4 100644 --- a/android-activity/src/util.rs +++ b/android-activity/src/util.rs @@ -1,3 +1,6 @@ +use jni::{ + jni_str, objects::{JObject, JString, JThread}, vm::JavaVM +}; use log::{error, Level}; use std::{ ffi::{CStr, CString}, @@ -111,3 +114,29 @@ pub(crate) fn abort_on_panic(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(()) + }) +}