diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md
index 35afefa..dd66796 100644
--- a/android-activity/CHANGELOG.md
+++ b/android-activity/CHANGELOG.md
@@ -1,12 +1,108 @@
-
+
# Changelog
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+### Added
+- Added `KeyEvent::meta_state()` for being able to query the state of meta keys, needed for character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
+- Added `KeyCharacterMap` JNI bindings to the corresponding Android SDK API ([#102](https://github.com/rust-mobile/android-activity/pull/102))
+- Added `AndroidApp::device_key_character_map()` for being able to get a `KeyCharacterMap` for a given `device_id` for unicode character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
+
+
+ Click here for an example of how to handle unicode character mapping:
+
+ ```rust
+ let mut combining_accent = None;
+ // Snip
+
+
+ let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
+ match map.get(key_event.key_code(), key_event.meta_state()) {
+ Ok(KeyMapChar::Unicode(unicode)) => {
+ let combined_unicode = if let Some(accent) = combining_accent {
+ match map.get_dead_char(accent, unicode) {
+ Ok(Some(key)) => {
+ info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
+ Some(key)
+ }
+ Ok(None) => None,
+ Err(err) => {
+ log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
+ None
+ }
+ }
+ } else {
+ info!("KeyEvent: Pressed '{unicode}'");
+ Some(unicode)
+ };
+ combining_accent = None;
+ combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
+ }
+ Ok(KeyMapChar::CombiningAccent(accent)) => {
+ info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
+ combining_accent = Some(accent);
+ Some(KeyMapChar::CombiningAccent(accent))
+ }
+ Ok(KeyMapChar::None) => {
+ info!("KeyEvent: Pressed non-unicode key");
+ combining_accent = None;
+ None
+ }
+ Err(err) => {
+ log::error!("KeyEvent: Failed to get key map character: {err:?}");
+ combining_accent = None;
+ None
+ }
+ }
+ } else {
+ None
+ };
+ ```
+
+
+
### Changed
- GameActivity updated to 2.0.2 (requires the corresponding 2.0.2 `.aar` release from Google) ([#88](https://github.com/rust-mobile/android-activity/pull/88))
+- `AndroidApp::input_events()` is replaced by `AndroidApp::input_events_iter()` ([#102](https://github.com/rust-mobile/android-activity/pull/102))
+
+
+ Click here for an example of how to use `input_events_iter()`:
+
+ ```rust
+ match app.input_events_iter() {
+ Ok(mut iter) => {
+ loop {
+ let read_input = iter.next(|event| {
+ let handled = match event {
+ InputEvent::KeyEvent(key_event) => {
+ // Snip
+ }
+ InputEvent::MotionEvent(motion_event) => {
+ // Snip
+ }
+ event => {
+ // Snip
+ }
+ };
+
+ handled
+ });
+
+ if !read_input {
+ break;
+ }
+ }
+ }
+ Err(err) => {
+ log::error!("Failed to get input events iterator: {err:?}");
+ }
+ }
+ ```
+
+
## [0.4.3] - 2022-07-30
### Fixed
diff --git a/android-activity/Cargo.toml b/android-activity/Cargo.toml
index 5a01bd6..9233f4c 100644
--- a/android-activity/Cargo.toml
+++ b/android-activity/Cargo.toml
@@ -38,6 +38,7 @@ native-activity = []
log = "0.4"
jni-sys = "0.3"
cesu8 = "1"
+jni = "0.21"
ndk = "0.7"
ndk-sys = "0.4"
ndk-context = "0.1"
@@ -45,6 +46,7 @@ android-properties = "0.2"
num_enum = "0.6"
bitflags = "2.0"
libc = "0.2"
+thiserror = "1"
[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
diff --git a/android-activity/LICENSE b/android-activity/LICENSE
index b4635e9..47bdfd8 100644
--- a/android-activity/LICENSE
+++ b/android-activity/LICENSE
@@ -1,12 +1,24 @@
-The third-party glue code, under the native-activity-csrc/ and game-activity-csrc/ directories
-is covered by the Apache 2.0 license only:
+# License
-Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
+## GameActivity
+The third-party glue code, under the game-activity-csrc/ directory is covered by
+the Apache 2.0 license only:
+
+Apache License, Version 2.0 (docs/LICENSE-APACHE or )
+
+## SDK Documentation
+
+Documentation for APIs that are direct bindings of Android platform APIs are covered
+by the Apache 2.0 license only:
+
+Apache License, Version 2.0 (docs/LICENSE-APACHE or )
+
+## android-activity
All other code is dual-licensed under either
-* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)
-* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
+- MIT License (docs/LICENSE-MIT or )
+- Apache License, Version 2.0 (docs/LICENSE-APACHE or )
-at your option.
\ No newline at end of file
+at your option.
diff --git a/android-activity/src/error.rs b/android-activity/src/error.rs
new file mode 100644
index 0000000..7193f5a
--- /dev/null
+++ b/android-activity/src/error.rs
@@ -0,0 +1,58 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum AppError {
+ #[error("Operation only supported from the android_main() thread: {0}")]
+ NonMainThread(String),
+
+ #[error("Java VM or JNI error, including Java exceptions")]
+ JavaError(String),
+
+ #[error("Input unavailable")]
+ InputUnavailable,
+}
+
+pub type Result = std::result::Result;
+
+// XXX: we don't want to expose jni-rs in the public API
+// so we have an internal error type that we can generally
+// use in the backends and then we can strip the error
+// in the frontend of the API.
+//
+// This way we avoid exposing a public trait implementation for
+// `From`
+#[derive(Error, Debug)]
+pub(crate) enum InternalAppError {
+ #[error("A JNI error")]
+ JniError(jni::errors::JniError),
+ #[error("A Java Exception was thrown via a JNI method call")]
+ JniException(String),
+ #[error("A Java VM error")]
+ JvmError(jni::errors::Error),
+ #[error("Input unavailable")]
+ InputUnavailable,
+}
+
+pub(crate) type InternalResult = std::result::Result;
+
+impl From for InternalAppError {
+ fn from(value: jni::errors::Error) -> Self {
+ InternalAppError::JvmError(value)
+ }
+}
+impl From for InternalAppError {
+ fn from(value: jni::errors::JniError) -> Self {
+ InternalAppError::JniError(value)
+ }
+}
+
+impl From for AppError {
+ fn from(value: InternalAppError) -> Self {
+ match value {
+ InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
+ InternalAppError::JniException(msg) => AppError::JavaError(msg),
+ InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
+ InternalAppError::InputUnavailable => AppError::InputUnavailable,
+ }
+ }
+}
diff --git a/android-activity/src/game_activity/input.rs b/android-activity/src/game_activity/input.rs
index ba442b7..29b33bf 100644
--- a/android-activity/src/game_activity/input.rs
+++ b/android-activity/src/game_activity/input.rs
@@ -16,7 +16,7 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::convert::TryInto;
-use crate::game_activity::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
+use crate::activity_impl::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
use crate::input::{Class, Source};
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
@@ -1274,6 +1274,12 @@ impl<'a> KeyEvent<'a> {
action.try_into().unwrap()
}
+ #[inline]
+ pub fn action_button(&self) -> KeyAction {
+ let action = self.ga_event.action as u32;
+ action.try_into().unwrap()
+ }
+
/// Returns the last time the key was pressed. This is on the scale of
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
///
diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs
index fd9da25..88a9fd6 100644
--- a/android-activity/src/game_activity/mod.rs
+++ b/android-activity/src/game_activity/mod.rs
@@ -1,5 +1,6 @@
#![cfg(feature = "game-activity")]
+use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
@@ -8,7 +9,8 @@ use std::ops::Deref;
use std::os::unix::prelude::*;
use std::panic::catch_unwind;
use std::ptr::NonNull;
-use std::sync::{Arc, RwLock};
+use std::sync::Weak;
+use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use std::{ptr, thread};
@@ -24,6 +26,9 @@ use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
+use crate::error::InternalResult;
+use crate::input::{KeyCharacterMap, KeyCharacterMapBinding};
+use crate::jni_utils::{self, CloneJavaVM};
use crate::util::{abort_on_panic, android_log, log_panic};
use crate::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
@@ -90,7 +95,7 @@ impl<'a> StateLoader<'a> {
if !(*app_ptr).savedState.is_null() && (*app_ptr).savedStateSize > 0 {
let buf: &mut [u8] = std::slice::from_raw_parts_mut(
(*app_ptr).savedState.cast(),
- (*app_ptr).savedStateSize as usize,
+ (*app_ptr).savedStateSize,
);
let state = buf.to_vec();
Some(state)
@@ -120,7 +125,16 @@ impl AndroidAppWaker {
}
impl AndroidApp {
- pub(crate) unsafe fn from_ptr(ptr: NonNull) -> Self {
+ 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()
@@ -128,15 +142,19 @@ impl AndroidApp {
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),
})),
}
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
struct NativeAppGlue {
ptr: NonNull,
}
@@ -150,11 +168,112 @@ impl Deref for NativeAppGlue {
unsafe impl Send for NativeAppGlue {}
unsafe impl Sync for NativeAppGlue {}
+impl NativeAppGlue {
+ // TODO: move into a trait
+ pub fn text_input_state(&self) -> TextInputState {
+ unsafe {
+ let activity = (*self.as_ptr()).activity;
+ let mut out_state = TextInputState {
+ text: String::new(),
+ selection: TextSpan { start: 0, end: 0 },
+ compose_region: None,
+ };
+ let out_ptr = &mut out_state as *mut TextInputState;
+
+ let app_ptr = self.as_ptr();
+ (*app_ptr).textInputState = 0;
+
+ // NEON WARNING:
+ //
+ // It's not clearly documented but the GameActivity API over the
+ // GameTextInput library directly exposes _modified_ UTF8 text
+ // from Java so we need to be careful to convert text to and
+ // from UTF8
+ //
+ // GameTextInput also uses a pre-allocated, fixed-sized buffer for
+ // the current text state and has shared `currentState_` that
+ // appears to have no lock to guard access from multiple threads.
+ //
+ // There's also no locking at the GameActivity level, so I'm fairly
+ // certain that `GameActivity_getTextInputState` isn't thread
+ // safe: https://issuetracker.google.com/issues/294112477
+ //
+ // Overall this is all quite gnarly - and probably a good reminder
+ // of why we want to use Rust instead of C/C++.
+ ffi::GameActivity_getTextInputState(
+ activity,
+ Some(AndroidAppInner::map_input_state_to_text_event_callback),
+ out_ptr.cast(),
+ );
+
+ out_state
+ }
+ }
+
+ // TODO: move into a trait
+ pub fn set_text_input_state(&self, state: TextInputState) {
+ unsafe {
+ let activity = (*self.as_ptr()).activity;
+ let modified_utf8 = cesu8::to_java_cesu8(&state.text);
+ let text_length = modified_utf8.len() as i32;
+ let modified_utf8_bytes = modified_utf8.as_ptr();
+ let ffi_state = ffi::GameTextInputState {
+ text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
+ text_length,
+ selection: ffi::GameTextInputSpan {
+ start: state.selection.start as i32,
+ end: state.selection.end as i32,
+ },
+ composingRegion: match state.compose_region {
+ Some(span) => {
+ // The GameText subclass of InputConnection only has a special case for removing the
+ // compose region if `start == -1` but the docs for `setComposingRegion` imply that
+ // the region should effectively be removed if any empty region is given (unlike for the
+ // selection region, it's not meaningful to maintain an empty compose region)
+ //
+ // We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
+ // to remove the compose region.
+ //
+ // NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
+ // so apart from this special-case handling in GameText's implementation of
+ // `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
+ // region that should get clamped to `(0, 0)` and then get removed.
+ if span.start == span.end {
+ ffi::GameTextInputSpan { start: -1, end: -1 }
+ } else {
+ ffi::GameTextInputSpan {
+ start: span.start as i32,
+ end: span.end as i32,
+ }
+ }
+ }
+ None => ffi::GameTextInputSpan { start: -1, end: -1 },
+ },
+ };
+ ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
+ }
+ }
+}
+
#[derive(Debug)]
pub struct AndroidAppInner {
+ pub(crate) jvm: CloneJavaVM,
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
+ key_maps: Mutex>,
+
+ /// While an app is reading input events it holds an
+ /// InputReceiver reference which we track to ensure
+ /// we don't hand out more than one receiver at a time
+ input_receiver: Mutex>>,
}
impl AndroidAppInner {
@@ -370,9 +489,9 @@ impl AndroidAppInner {
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
let text_modified_utf8 =
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
- match cesu8::from_java_cesu8(&text_modified_utf8) {
+ match cesu8::from_java_cesu8(text_modified_utf8) {
Ok(str) => {
- let len = *&str.len();
+ let len = str.len();
(*out_ptr).text = String::from(str);
let selection_start = (*state).selection.start.clamp(0, len as i32 + 1);
@@ -398,82 +517,34 @@ impl AndroidAppInner {
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
- unsafe {
- let activity = (*self.native_app.as_ptr()).activity;
- let mut out_state = TextInputState {
- text: String::new(),
- selection: TextSpan { start: 0, end: 0 },
- compose_region: None,
- };
- let out_ptr = &mut out_state as *mut TextInputState;
-
- // NEON WARNING:
- //
- // It's not clearly documented but the GameActivity API over the
- // GameTextInput library directly exposes _modified_ UTF8 text
- // from Java so we need to be careful to convert text to and
- // from UTF8
- //
- // GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
- // text state but GameTextInput doesn't actually provide it's own thread
- // safe API to safely access this state so we have to cooperate with
- // the GameActivity code that does locking when reading/writing the state
- // (I.e. we can't just punch through to the GameTextInput layer from here).
- //
- // Overall this is all quite gnarly - and probably a good reminder of why
- // we want to use Rust instead of C/C++.
- ffi::GameActivity_getTextInputState(
- activity,
- Some(AndroidAppInner::map_input_state_to_text_event_callback),
- out_ptr.cast(),
- );
-
- out_state
- }
+ self.native_app.text_input_state()
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
- unsafe {
- let activity = (*self.native_app.as_ptr()).activity;
- let modified_utf8 = cesu8::to_java_cesu8(&state.text);
- let text_length = modified_utf8.len() as i32;
- let modified_utf8_bytes = modified_utf8.as_ptr();
- let ffi_state = ffi::GameTextInputState {
- text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
- text_length,
- selection: ffi::GameTextInputSpan {
- start: state.selection.start as i32,
- end: state.selection.end as i32,
- },
- composingRegion: match state.compose_region {
- Some(span) => {
- // The GameText subclass of InputConnection only has a special case for removing the
- // compose region if `start == -1` but the docs for `setComposingRegion` imply that
- // the region should effectively be removed if any empty region is given (unlike for the
- // selection region, it's not meaningful to maintain an empty compose region)
- //
- // We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
- // to remove the compose region.
- //
- // NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
- // so apart from this special-case handling in GameText's implementation of
- // `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
- // region that should get clamped to `(0, 0)` and then get removed.
- if span.start == span.end {
- ffi::GameTextInputSpan { start: -1, end: -1 }
- } else {
- ffi::GameTextInputSpan {
- start: span.start as i32,
- end: span.end as i32,
- }
- }
- }
- None => ffi::GameTextInputSpan { start: -1, end: -1 },
- },
- };
- ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
- }
+ self.native_app.set_text_input_state(state);
+ }
+
+ pub(crate) fn device_key_character_map(
+ &self,
+ device_id: i32,
+ ) -> InternalResult {
+ let mut guard = self.key_maps.lock().unwrap();
+
+ let key_map = match guard.entry(device_id) {
+ std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
+ std::collections::hash_map::Entry::Vacant(vacant) => {
+ let character_map = jni_utils::device_key_character_map(
+ self.jvm.clone(),
+ self.key_map_binding.clone(),
+ device_id,
+ )?;
+ vacant.insert(character_map.clone());
+ character_map
+ }
+ };
+
+ Ok(key_map)
}
pub fn enable_motion_axis(&mut self, axis: Axis) {
@@ -519,49 +590,26 @@ impl AndroidAppInner {
}
}
- fn dispatch_key_and_motion_events(&self, mut callback: F)
- where
- F: FnMut(&InputEvent) -> InputStatus,
- {
- let buf = unsafe {
- let app_ptr = self.native_app.as_ptr();
- let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
- if input_buffer.is_null() {
- return;
- }
- InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
- };
+ pub(crate) fn input_events_receiver(&self) -> InternalResult> {
+ let mut guard = self.input_receiver.lock().unwrap();
- let mut keys_iter = KeyEventsLendingIterator::new(&buf);
- while let Some(key_event) = keys_iter.next() {
- callback(&InputEvent::KeyEvent(key_event));
- }
- let mut motion_iter = MotionEventsLendingIterator::new(&buf);
- while let Some(motion_event) = motion_iter.next() {
- callback(&InputEvent::MotionEvent(motion_event));
- }
- }
-
- fn dispatch_text_events(&self, mut callback: F)
- where
- F: FnMut(&InputEvent) -> InputStatus,
- {
- unsafe {
- let app_ptr = self.native_app.as_ptr();
- if (*app_ptr).textInputState != 0 {
- let state = self.text_input_state();
- callback(&InputEvent::TextEvent(state));
- (*app_ptr).textInputState = 0;
+ // Make sure we don't hand out more than one receiver at a time because
+ // turning the reciever into an interator will perform a swap_buffers
+ // for the buffered input events which shouldn't happen while we're in
+ // the middle of iterating events
+ if let Some(receiver) = &*guard {
+ if receiver.strong_count() > 0 {
+ return Err(crate::error::InternalAppError::InputUnavailable);
}
}
- }
+ *guard = None;
- pub fn input_events(&self, mut callback: F)
- where
- F: FnMut(&InputEvent) -> InputStatus,
- {
- self.dispatch_key_and_motion_events(&mut callback);
- self.dispatch_text_events(&mut callback);
+ let receiver = Arc::new(InputReceiver {
+ native_app: self.native_app.clone(),
+ });
+
+ *guard = Some(Arc::downgrade(&receiver));
+ Ok(receiver)
}
pub fn internal_data_path(&self) -> Option {
@@ -586,33 +634,28 @@ impl AndroidAppInner {
}
}
-struct MotionEventsLendingIterator<'a> {
+struct MotionEventsLendingIterator {
pos: usize,
count: usize,
- buffer: &'a InputBuffer<'a>,
}
-// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
-// via a generic trait. The iteration of motion events is entirely private
-// though so this is ok for now.
-impl<'a> MotionEventsLendingIterator<'a> {
- fn new(buffer: &'a InputBuffer<'a>) -> Self {
+impl MotionEventsLendingIterator {
+ fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.motion_events_count(),
- buffer,
}
}
- fn next(&mut self) -> Option> {
+ fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option> {
if self.pos < self.count {
// Safety:
// - This iterator currently has exclusive access to the front buffer of events
// - We know the buffer is non-null
// - `pos` is less than the number of events stored in the buffer
let ga_event = unsafe {
- (*self.buffer.ptr.as_ptr())
+ (*buffer.ptr.as_ptr())
.motionEvents
- .offset(self.pos as isize)
+ .add(self.pos)
.as_ref()
.unwrap()
};
@@ -625,33 +668,28 @@ impl<'a> MotionEventsLendingIterator<'a> {
}
}
-struct KeyEventsLendingIterator<'a> {
+struct KeyEventsLendingIterator {
pos: usize,
count: usize,
- buffer: &'a InputBuffer<'a>,
}
-// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
-// via a generic trait. The iteration of key events is entirely private
-// though so this is ok for now.
-impl<'a> KeyEventsLendingIterator<'a> {
- fn new(buffer: &'a InputBuffer<'a>) -> Self {
+impl KeyEventsLendingIterator {
+ fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.key_events_count(),
- buffer,
}
}
- fn next(&mut self) -> Option> {
+ fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option> {
if self.pos < self.count {
// Safety:
// - This iterator currently has exclusive access to the front buffer of events
// - We know the buffer is non-null
// - `pos` is less than the number of events stored in the buffer
let ga_event = unsafe {
- (*self.buffer.ptr.as_ptr())
+ (*buffer.ptr.as_ptr())
.keyEvents
- .offset(self.pos as isize)
+ .add(self.pos)
.as_ref()
.unwrap()
};
@@ -673,7 +711,7 @@ impl<'a> InputBuffer<'a> {
pub(crate) fn from_ptr(ptr: NonNull) -> InputBuffer<'a> {
Self {
ptr,
- _lifetime: PhantomData::default(),
+ _lifetime: PhantomData,
}
}
@@ -695,6 +733,118 @@ impl<'a> Drop for InputBuffer<'a> {
}
}
+/// Conceptually we can think of this like the receiver end of an
+/// input events channel.
+///
+/// After being passed back to AndroidApp it gets turned into a
+/// lending iterator for pending input events.
+///
+/// It serves two purposes:
+/// 1. It represents an exclusive access to input events (the application
+/// can only have one receiver at a time) and it's intended to support
+/// the double-buffering design for input events in GameActivity where
+/// we issue a swap_buffers before iterating events and wouldn't want
+/// another swap to be possible before finishing - especially since
+/// we want to borrow directly from the buffer while dispatching.
+/// 2. It doesn't borrow from AndroidAppInner so we can pass it back to
+/// AndroidApp which can drop its lock around AndroidAppInner and
+/// it can then be turned into a lending iterator. (We wouldn't
+/// be able to pass the iterator back to the application if it
+/// borrowed from within the lock and we need to drop the lock
+/// because otherwise the app wouldn't be able to access the AndroidApp
+/// API in any way while iterating events)
+#[derive(Debug)]
+pub(crate) struct InputReceiver {
+ // Safety: the native_app effectively has a static lifetime and it
+ // has its own internal locking when calling
+ // `android_app_swap_input_buffers`
+ native_app: NativeAppGlue,
+}
+
+impl<'a> From> for InputIteratorInner<'a> {
+ fn from(receiver: Arc) -> Self {
+ let buffered = unsafe {
+ let app_ptr = receiver.native_app.as_ptr();
+ let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
+ if input_buffer.is_null() {
+ None
+ } else {
+ let buffer = InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer));
+ let keys_iter = KeyEventsLendingIterator::new(&buffer);
+ let motion_iter = MotionEventsLendingIterator::new(&buffer);
+ Some(BufferedEvents::<'a> {
+ buffer,
+ keys_iter,
+ motion_iter,
+ })
+ }
+ };
+
+ let native_app = receiver.native_app.clone();
+ Self {
+ _receiver: receiver,
+ buffered,
+ native_app,
+ text_event_checked: false,
+ }
+ }
+}
+
+struct BufferedEvents<'a> {
+ buffer: InputBuffer<'a>,
+ keys_iter: KeyEventsLendingIterator,
+ motion_iter: MotionEventsLendingIterator,
+}
+
+pub(crate) struct InputIteratorInner<'a> {
+ // Held to maintain exclusive access to buffered input events
+ _receiver: Arc,
+
+ buffered: Option>,
+ native_app: NativeAppGlue,
+ text_event_checked: bool,
+}
+
+impl<'a> InputIteratorInner<'a> {
+ pub(crate) fn next(&mut self, callback: F) -> bool
+ where
+ F: FnOnce(&input::InputEvent) -> InputStatus,
+ {
+ if let Some(buffered) = &mut self.buffered {
+ if let Some(key_event) = buffered.keys_iter.next(&buffered.buffer) {
+ let _ = callback(&InputEvent::KeyEvent(key_event));
+ return true;
+ }
+ if let Some(motion_event) = buffered.motion_iter.next(&buffered.buffer) {
+ let _ = callback(&InputEvent::MotionEvent(motion_event));
+ return true;
+ }
+ self.buffered = None;
+ }
+
+ if !self.text_event_checked {
+ self.text_event_checked = true;
+ unsafe {
+ let app_ptr = self.native_app.as_ptr();
+
+ // XXX: It looks like the GameActivity implementation should
+ // be using atomic ops to set this flag, and require us to
+ // use atomics to check and clear it too.
+ //
+ // We currently just hope that with the lack of atomic ops that
+ // the compiler isn't reordering code so this gets flagged
+ // before the java main thread really updates the state.
+ if (*app_ptr).textInputState != 0 {
+ let state = self.native_app.text_input_state(); // Will clear .textInputState
+ let _ = callback(&InputEvent::TextEvent(state));
+ return true;
+ }
+ }
+ }
+ false
+ }
+}
+
// Rust doesn't give us a clean way to directly export symbols from C/C++
// so we rename the C/C++ symbols and re-export these JNI entrypoints from
// Rust...
@@ -789,19 +939,16 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
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
- let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
- if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
- attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
- }
-
+ jvm.attach_current_thread_permanently().unwrap();
jvm
};
unsafe {
- let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap());
+ 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
@@ -824,9 +971,9 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
// to the main thread of the process where the Java finish call will take place"
ffi::GameActivity_finish((*native_app).activity);
- if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
- detach_current_thread(jvm);
- }
+ // 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();
}
diff --git a/android-activity/src/input.rs b/android-activity/src/input.rs
index b9edf7e..f62a088 100644
--- a/android-activity/src/input.rs
+++ b/android-activity/src/input.rs
@@ -2,6 +2,10 @@ use bitflags::bitflags;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use crate::activity_impl::input::*;
+use crate::InputStatus;
+
+mod sdk;
+pub use sdk::*;
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
///
@@ -120,3 +124,22 @@ pub struct TextInputState {
/// If the resulting region is zero-sized, no region is marked (equivalent to passing `None`)
pub compose_region: Option,
}
+
+/// An exclusive, lending iterator for input events
+pub struct InputIterator<'a> {
+ pub(crate) inner: crate::activity_impl::InputIteratorInner<'a>,
+}
+
+impl<'a> InputIterator<'a> {
+ /// Reads and handles the next input event by passing it to the given `callback`
+ ///
+ /// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
+ /// handled by the application, or else [`InputStatus::Handled`]. Unhandled events may lead to a
+ /// fallback interpretation of the event.
+ pub fn next(&mut self, callback: F) -> bool
+ where
+ F: FnOnce(&crate::activity_impl::input::InputEvent) -> InputStatus,
+ {
+ self.inner.next(callback)
+ }
+}
diff --git a/android-activity/src/input/sdk.rs b/android-activity/src/input/sdk.rs
new file mode 100644
index 0000000..4d1e84b
--- /dev/null
+++ b/android-activity/src/input/sdk.rs
@@ -0,0 +1,358 @@
+use std::sync::Arc;
+
+use jni::{
+ objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
+ signature::{Primitive, ReturnType},
+ JNIEnv,
+};
+use jni_sys::jint;
+
+use crate::{
+ activity_impl::input::{Keycode, MetaState},
+ jni_utils::CloneJavaVM,
+};
+
+use crate::{
+ error::{AppError, InternalAppError},
+ jni_utils,
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum KeyboardType {
+ /// A numeric (12-key) keyboard.
+ ///
+ /// A numeric keyboard supports text entry using a multi-tap approach. It may be necessary to tap a key multiple times to generate the desired letter or symbol.
+ ///
+ /// This type of keyboard is generally designed for thumb typing.
+ Numeric,
+
+ /// A keyboard with all the letters, but with more than one letter per key.
+ ///
+ /// This type of keyboard is generally designed for thumb typing.
+ Predictive,
+
+ /// A keyboard with all the letters, and maybe some numbers.
+ ///
+ /// An alphabetic keyboard supports text entry directly but may have a condensed layout with a small form factor. In contrast to a full keyboard, some symbols may only be accessible using special on-screen character pickers. In addition, to improve typing speed and accuracy, the framework provides special affordances for alphabetic keyboards such as auto-capitalization and toggled / locked shift and alt keys.
+ ///
+ /// This type of keyboard is generally designed for thumb typing.
+ Alpha,
+
+ /// A full PC-style keyboard.
+ ///
+ /// A full keyboard behaves like a PC keyboard. All symbols are accessed directly by pressing keys on the keyboard without on-screen support or affordances such as auto-capitalization.
+ ///
+ /// This type of keyboard is generally designed for full two hand typing.
+ Full,
+
+ /// A keyboard that is only used to control special functions rather than for typing.
+ ///
+ /// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
+ SpecialFunction,
+
+ /// An unknown type of keyboard
+ Unknown(i32),
+}
+
+impl From for KeyboardType {
+ fn from(value: i32) -> Self {
+ match value {
+ 1 => KeyboardType::Numeric,
+ 2 => KeyboardType::Predictive,
+ 3 => KeyboardType::Alpha,
+ 4 => KeyboardType::Full,
+ 5 => KeyboardType::SpecialFunction,
+ unknown => KeyboardType::Unknown(unknown),
+ }
+ }
+}
+impl From for i32 {
+ fn from(value: KeyboardType) -> i32 {
+ match value {
+ KeyboardType::Numeric => 1,
+ KeyboardType::Predictive => 2,
+ KeyboardType::Alpha => 3,
+ KeyboardType::Full => 4,
+ KeyboardType::SpecialFunction => 5,
+ KeyboardType::Unknown(unknown) => unknown,
+ }
+ }
+}
+
+/// Either represents, a unicode character or combining accent from a
+/// [`KeyCharacterMap`], or `None` for non-printable keys.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum KeyMapChar {
+ None,
+ Unicode(char),
+ CombiningAccent(char),
+}
+
+// I've also tried to think here about how to we could potentially automatically
+// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
+// so have intentionally limited the `Binding` being a very thin, un-opinionated
+// wrapper based on basic JNI types.
+
+/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
+/// and can be shared with an `Arc` ref count.
+///
+/// The separation here also neatly helps us separate `InternalAppError` from
+/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
+/// public API.
+#[derive(Debug)]
+pub(crate) struct KeyCharacterMapBinding {
+ //vm: JavaVM,
+ klass: GlobalRef,
+ get_method_id: JMethodID,
+ get_dead_char_method_id: JStaticMethodID,
+ get_keyboard_type_method_id: JMethodID,
+}
+
+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)
+ }
+
+ 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(),
+ ],
+ )
+ }
+ .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(),
+ ],
+ )
+ }
+ .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),
+ &[],
+ )
+ }
+ .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
+ .i()
+ .unwrap())
+ }
+}
+
+/// Describes the keys provided by a keyboard device and their associated labels.
+#[derive(Clone, Debug)]
+pub struct KeyCharacterMap {
+ jvm: CloneJavaVM,
+ binding: Arc,
+ key_map: GlobalRef,
+}
+
+impl KeyCharacterMap {
+ pub(crate) fn new(
+ jvm: CloneJavaVM,
+ binding: Arc,
+ key_map: GlobalRef,
+ ) -> Self {
+ Self {
+ jvm,
+ binding,
+ key_map,
+ }
+ }
+
+ /// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
+ ///
+ /// Returns [`KeyMapChar::None`] if the key is not one that is used to type Unicode characters.
+ ///
+ /// Returns [`KeyMapChar::CombiningAccent`] if the key is a "dead key" that should be combined with
+ /// another to actually produce a character -- see [`KeyCharacterMap::get_dead_char`].
+ ///
+ /// # Errors
+ ///
+ /// Since this API needs to use JNI internally to call into the Android JVM it may return
+ /// 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: 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
+ //
+ // 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 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)
+ }))
+ }
+ }
+
+ /// Get the character that is produced by combining the dead key producing accent with the key producing character c.
+ ///
+ /// For example, ```get_dead_char('`', 'e')``` returns 'รจ'. `get_dead_char('^', ' ')` returns '^' and `get_dead_char('^', '^')` returns '^'.
+ ///
+ /// # Errors
+ ///
+ /// Since this API needs to use JNI internally to call into the Android JVM it may return
+ /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
+ /// is caught.
+ pub fn get_dead_char(
+ &self,
+ 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 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) })
+ })
+ }
+
+ /// Gets the keyboard type.
+ ///
+ /// Different keyboard types have different semantics. See [`KeyboardType`] for details.
+ ///
+ /// # Errors
+ ///
+ /// Since this API needs to use JNI internally to call into the Android JVM it may return
+ /// 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 err: InternalAppError = err.into();
+ err
+ })?;
+ let keyboard_type = self
+ .binding
+ .get_keyboard_type(&mut env, self.key_map.as_obj())?;
+ Ok(keyboard_type.into())
+ }
+}
diff --git a/android-activity/src/jni_utils.rs b/android-activity/src/jni_utils.rs
new file mode 100644
index 0000000..dcf6083
--- /dev/null
+++ b/android-activity/src/jni_utils.rs
@@ -0,0 +1,151 @@
+//! The JNI calls we make in this crate are often not part of a Java native
+//! method implementation and so we can't assume we have a JNI local frame that
+//! is going to unwind and free local references, and we also can't just leave
+//! exceptions to get thrown when returning to Java.
+//!
+//! 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},
+};
+
+// 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
+ }
+}
+
+/// Use with `.map_err()` to map `jni::errors::Error::JavaException` into a
+/// richer error based on the actual contents of the `JThrowable`
+///
+/// (The `jni` crate doesn't do that automatically since it's more
+/// common to let the exception get thrown when returning to Java)
+///
+/// This will also clear the exception
+pub(crate) fn clear_and_map_exception_to_err(
+ env: &mut jni::JNIEnv<'_>,
+ 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 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);
+
+ }
+ */
+
+ Ok(msg)
+ });
+
+ match result {
+ Ok(msg) => InternalAppError::JniException(msg),
+ Err(err) => InternalAppError::JniException(format!(
+ "UNKNOWN (Failed to query JThrowable: {err:?})"
+ )),
+ }
+ } else {
+ 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 e00a902..8b9c46e 100644
--- a/android-activity/src/lib.rs
+++ b/android-activity/src/lib.rs
@@ -62,6 +62,7 @@ use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
+use input::KeyCharacterMap;
use libc::c_void;
use ndk::asset::AssetManager;
use ndk::native_window::NativeWindow;
@@ -96,15 +97,12 @@ You may need to add a `[patch]` into your Cargo.toml to ensure a specific versio
android-activity is used across all of your application's crates."#
);
-#[cfg(any(feature = "native-activity", doc))]
-mod native_activity;
-#[cfg(any(feature = "native-activity", doc))]
-use native_activity as activity_impl;
+#[cfg_attr(any(feature = "native-activity", doc), path = "native_activity/mod.rs")]
+#[cfg_attr(any(feature = "game-activity", doc), path = "game_activity/mod.rs")]
+pub(crate) mod activity_impl;
-#[cfg(feature = "game-activity")]
-mod game_activity;
-#[cfg(feature = "game-activity")]
-use game_activity as activity_impl;
+pub mod error;
+use error::Result;
pub mod input;
@@ -113,6 +111,8 @@ pub use config::ConfigurationRef;
mod util;
+mod jni_utils;
+
/// A rectangle with integer edge coordinates. Used to represent window insets, for example.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Rect {
@@ -163,14 +163,14 @@ pub use activity_impl::StateSaver;
#[non_exhaustive]
#[derive(Debug)]
pub enum MainEvent<'a> {
- /// New input events are available via [`AndroidApp::input_events()`]
+ /// New input events are available via [`AndroidApp::input_events_iter()`]
///
/// _Note: Even if more input is received this event will not be resent
- /// until [`AndroidApp::input_events()`] has been called, which enables
+ /// until [`AndroidApp::input_events_iter()`] has been called, which enables
/// applications to batch up input processing without there being lots of
/// redundant event loop wake ups._
///
- /// [`AndroidApp::input_events()`]: AndroidApp::input_events
+ /// [`AndroidApp::input_events_iter()`]: AndroidApp::input_events_iter
InputAvailable,
/// Command from main thread: a new [`NativeWindow`] is ready for use. Upon
@@ -629,24 +629,139 @@ impl AndroidApp {
self.inner.read().unwrap().set_text_input_state(state);
}
- /// Query and process all out-standing input event
+ /// Get an exclusive, lending iterator over buffered input events
///
- /// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
- /// handled by the application, or else [`InputStatus::Handled`]. Unhandled events may lead to a
- /// fallback interpretation of the event.
+ /// Applications are expected to call this in-sync with their rendering or
+ /// in response to a [`MainEvent::InputAvailable`] event being delivered.
///
- /// Applications are generally either expected to call this in-sync with their rendering or
- /// in response to a [`MainEvent::InputAvailable`] event being delivered. _Note though that your
- /// application is will only be delivered a single [`MainEvent::InputAvailable`] event between calls
- /// to this API._
+ /// _**Note:** your application is will only be delivered a single
+ /// [`MainEvent::InputAvailable`] event between calls to this API._
///
- /// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
+ /// To reduce overhead, by default, only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
- pub fn input_events(&self, callback: F)
- where
- F: FnMut(&input::InputEvent) -> InputStatus,
- {
- self.inner.read().unwrap().input_events(callback)
+ ///
+ /// This isn't the most ergonomic iteration API since we can't return a standard `Iterator`:
+ /// - This API returns a lending iterator may borrow from the internal buffer
+ /// of pending events without copying them.
+ /// - For each event we want to ensure the application reports whether the
+ /// event was handled.
+ ///
+ /// # Example
+ /// Code to iterate all pending input events would look something like this:
+ ///
+ /// ```rust
+ /// match app.input_events_iter() {
+ /// Ok(mut iter) => {
+ /// loop {
+ /// let read_input = iter.next(|event| {
+ /// let handled = match event {
+ /// InputEvent::KeyEvent(key_event) => {
+ /// // Snip
+ /// }
+ /// InputEvent::MotionEvent(motion_event) => {
+ /// // Snip
+ /// }
+ /// event => {
+ /// // Snip
+ /// }
+ /// };
+ ///
+ /// handled
+ /// });
+ ///
+ /// if !read_input {
+ /// break;
+ /// }
+ /// }
+ /// }
+ /// Err(err) => {
+ /// log::error!("Failed to get input events iterator: {err:?}");
+ /// }
+ /// }
+ /// ```
+ ///
+ /// # Panics
+ ///
+ /// This must only be called from your `android_main()` thread and it may panic if called
+ /// from another thread.
+ pub fn input_events_iter(&self) -> Result {
+ let receiver = {
+ let guard = self.inner.read().unwrap();
+ guard.input_events_receiver()?
+ };
+
+ Ok(input::InputIterator {
+ inner: receiver.into(),
+ })
+ }
+
+ /// Lookup the [`KeyCharacterMap`] for the given input `device_id`
+ ///
+ /// Use [`KeyCharacterMap::get`] to map key codes + meta state into unicode characters
+ /// or dead keys that compose with the next key.
+ ///
+ /// # Example
+ ///
+ /// Code to handle unicode character mapping as well as combining dead keys could look some thing like:
+ ///
+ /// ```rust
+ /// let mut combining_accent = None;
+ /// // Snip
+ ///
+ /// let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
+ /// match map.get(key_event.key_code(), key_event.meta_state()) {
+ /// Ok(KeyMapChar::Unicode(unicode)) => {
+ /// let combined_unicode = if let Some(accent) = combining_accent {
+ /// match map.get_dead_char(accent, unicode) {
+ /// Ok(Some(key)) => {
+ /// info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
+ /// Some(key)
+ /// }
+ /// Ok(None) => None,
+ /// Err(err) => {
+ /// log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
+ /// None
+ /// }
+ /// }
+ /// } else {
+ /// info!("KeyEvent: Pressed '{unicode}'");
+ /// Some(unicode)
+ /// };
+ /// combining_accent = None;
+ /// combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
+ /// }
+ /// Ok(KeyMapChar::CombiningAccent(accent)) => {
+ /// info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
+ /// combining_accent = Some(accent);
+ /// Some(KeyMapChar::CombiningAccent(accent))
+ /// }
+ /// Ok(KeyMapChar::None) => {
+ /// info!("KeyEvent: Pressed non-unicode key");
+ /// combining_accent = None;
+ /// None
+ /// }
+ /// Err(err) => {
+ /// log::error!("KeyEvent: Failed to get key map character: {err:?}");
+ /// combining_accent = None;
+ /// None
+ /// }
+ /// }
+ /// } else {
+ /// None
+ /// };
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Since this API needs to use JNI internally to call into the Android JVM it may return
+ /// a [`error::AppError::JavaError`] in case there is a spurious JNI error or an exception
+ /// is caught.
+ pub fn device_key_character_map(&self, device_id: i32) -> Result {
+ Ok(self
+ .inner
+ .read()
+ .unwrap()
+ .device_key_character_map(device_id)?)
}
/// The user-visible SDK version of the framework
diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs
index 6682857..495c813 100644
--- a/android-activity/src/native_activity/glue.rs
+++ b/android-activity/src/native_activity/glue.rs
@@ -17,6 +17,7 @@ use log::Level;
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
use crate::{
+ jni_utils::CloneJavaVM,
util::android_log,
util::{abort_on_panic, log_panic},
ConfigurationRef,
@@ -878,24 +879,21 @@ extern "C" fn ANativeActivity_onCreate(
std::thread::spawn(move || {
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
- let jvm = unsafe {
+ let jvm = abort_on_panic(|| unsafe {
let na = activity;
- let jvm = (*na).vm;
+ 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
- let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
- if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
- attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
- }
-
+ jvm.attach_current_thread_permanently().unwrap();
jvm
- };
+ });
- let app = AndroidApp::new(rust_glue.clone());
+ let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
@@ -921,9 +919,9 @@ extern "C" fn ANativeActivity_onCreate(
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
- if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
- detach_current_thread(jvm);
- }
+ // 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();
}
diff --git a/android-activity/src/native_activity/input.rs b/android-activity/src/native_activity/input.rs
index 08db48f..2056fe0 100644
--- a/android-activity/src/native_activity/input.rs
+++ b/android-activity/src/native_activity/input.rs
@@ -326,6 +326,15 @@ impl<'a> KeyEvent<'a> {
pub fn scan_code(&self) -> i32 {
self.ndk_event.scan_code()
}
+
+ /// Returns the state of the modifiers during this key event, represented by a bitmask.
+ ///
+ /// See [the NDK
+ /// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getmetastate)
+ #[inline]
+ pub fn meta_state(&self) -> MetaState {
+ self.ndk_event.meta_state()
+ }
}
// We use our own wrapper type for input events to have better consistency
diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs
index 3f16550..fee09b9 100644
--- a/android-activity/src/native_activity/mod.rs
+++ b/android-activity/src/native_activity/mod.rs
@@ -1,15 +1,22 @@
#![cfg(any(feature = "native-activity", doc))]
+use std::collections::HashMap;
+use std::marker::PhantomData;
+use std::panic::AssertUnwindSafe;
use std::ptr;
use std::ptr::NonNull;
-use std::sync::{Arc, RwLock};
+use std::sync::{Arc, Mutex, RwLock, Weak};
use std::time::Duration;
use libc::c_void;
use log::{error, trace};
+use ndk::input_queue::InputQueue;
use ndk::{asset::AssetManager, native_window::NativeWindow};
+use crate::error::{InternalAppError, InternalResult};
+use crate::input::{KeyCharacterMap, KeyCharacterMapBinding};
use crate::input::{TextInputState, TextSpan};
+use crate::jni_utils::{self, CloneJavaVM};
use crate::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
};
@@ -79,13 +86,26 @@ impl AndroidAppWaker {
}
impl AndroidApp {
- pub(crate) fn new(native_activity: NativeActivityGlue) -> Self {
+ 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:?}");
+ }
+ };
+
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),
})),
};
@@ -122,8 +142,23 @@ unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
+ pub(crate) jvm: CloneJavaVM,
+
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
+ key_maps: Mutex>,
+
+ /// While an app is reading input events it holds an
+ /// InputReceiver reference which we track to ensure
+ /// we don't hand out more than one receiver at a time
+ input_receiver: Mutex>>,
}
impl AndroidAppInner {
@@ -356,6 +391,25 @@ impl AndroidAppInner {
// NOP: Unsupported
}
+ pub fn device_key_character_map(&self, device_id: i32) -> InternalResult {
+ let mut guard = self.key_maps.lock().unwrap();
+
+ let key_map = match guard.entry(device_id) {
+ std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
+ std::collections::hash_map::Entry::Vacant(vacant) => {
+ let character_map = jni_utils::device_key_character_map(
+ self.jvm.clone(),
+ self.key_map_binding.clone(),
+ device_id,
+ )?;
+ vacant.insert(character_map.clone());
+ character_map
+ }
+ };
+
+ Ok(key_map)
+ }
+
pub fn enable_motion_axis(&self, _axis: input::Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
@@ -364,10 +418,16 @@ impl AndroidAppInner {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
- pub fn input_events(&self, mut callback: F)
- where
- F: FnMut(&input::InputEvent) -> InputStatus,
- {
+ pub fn input_events_receiver(&self) -> InternalResult> {
+ let mut guard = self.input_receiver.lock().unwrap();
+
+ if let Some(receiver) = &*guard {
+ if receiver.strong_count() > 0 {
+ return Err(crate::error::InternalAppError::InputUnavailable);
+ }
+ }
+ *guard = None;
+
// Get the InputQueue for the NativeActivity (if there is one) and also ensure
// the queue is re-attached to our event Looper (so new input events will again
// trigger a wake up)
@@ -376,40 +436,13 @@ impl AndroidAppInner {
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
let queue = match queue {
Some(queue) => queue,
- None => return,
+ None => return Err(InternalAppError::InputUnavailable),
};
- // Note: we basically ignore errors from get_event() currently. Looking
- // at the source code for Android's InputQueue, the only error that
- // can be returned here is 'WOULD_BLOCK', which we want to just treat as
- // meaning the queue is empty.
- //
- // ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
- //
- while let Ok(Some(event)) = queue.get_event() {
- if let Some(ndk_event) = queue.pre_dispatch(event) {
- let event = match ndk_event {
- ndk::event::InputEvent::MotionEvent(e) => {
- input::InputEvent::MotionEvent(input::MotionEvent::new(e))
- }
- ndk::event::InputEvent::KeyEvent(e) => {
- input::InputEvent::KeyEvent(input::KeyEvent::new(e))
- }
- };
- let handled = callback(&event);
+ let receiver = Arc::new(InputReceiver { queue });
- let ndk_event = match event {
- input::InputEvent::MotionEvent(e) => {
- ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
- }
- input::InputEvent::KeyEvent(e) => {
- ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
- }
- _ => unreachable!(),
- };
- queue.finish_event(ndk_event, matches!(handled, InputStatus::Handled));
- }
- }
+ *guard = Some(Arc::downgrade(&receiver));
+ Ok(receiver)
}
pub fn internal_data_path(&self) -> Option {
@@ -427,3 +460,85 @@ impl AndroidAppInner {
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
}
}
+
+#[derive(Debug)]
+pub(crate) struct InputReceiver {
+ queue: InputQueue,
+}
+
+impl<'a> From> for InputIteratorInner<'a> {
+ fn from(receiver: Arc) -> Self {
+ Self {
+ receiver,
+ _lifetime: PhantomData,
+ }
+ }
+}
+
+pub(crate) struct InputIteratorInner<'a> {
+ // Held to maintain exclusive access to buffered input events
+ receiver: Arc,
+ _lifetime: PhantomData<&'a ()>,
+}
+
+impl<'a> InputIteratorInner<'a> {
+ pub(crate) fn next(&self, callback: F) -> bool
+ where
+ F: FnOnce(&input::InputEvent) -> InputStatus,
+ {
+ // Note: we basically ignore errors from get_event() currently. Looking
+ // at the source code for Android's InputQueue, the only error that
+ // can be returned here is 'WOULD_BLOCK', which we want to just treat as
+ // meaning the queue is empty.
+ //
+ // ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
+ //
+ if let Ok(Some(ndk_event)) = self.receiver.queue.get_event() {
+ log::info!("queue: got event: {ndk_event:?}");
+
+ if let Some(ndk_event) = self.receiver.queue.pre_dispatch(ndk_event) {
+ let event = match ndk_event {
+ ndk::event::InputEvent::MotionEvent(e) => {
+ input::InputEvent::MotionEvent(input::MotionEvent::new(e))
+ }
+ ndk::event::InputEvent::KeyEvent(e) => {
+ input::InputEvent::KeyEvent(input::KeyEvent::new(e))
+ }
+ };
+
+ // `finish_event` needs to be called for each event otherwise
+ // the app would likely get an ANR
+ let result = std::panic::catch_unwind(AssertUnwindSafe(|| callback(&event)));
+
+ let ndk_event = match event {
+ input::InputEvent::MotionEvent(e) => {
+ ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
+ }
+ input::InputEvent::KeyEvent(e) => {
+ ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
+ }
+ _ => unreachable!(),
+ };
+
+ let handled = match result {
+ Ok(handled) => handled,
+ Err(payload) => {
+ log::error!("Calling `finish_event` after panic in input event handler, to try and avoid being killed via an ANR");
+ self.receiver.queue.finish_event(ndk_event, false);
+ std::panic::resume_unwind(payload);
+ }
+ };
+
+ log::info!("queue: finishing event");
+ self.receiver
+ .queue
+ .finish_event(ndk_event, handled == InputStatus::Handled);
+ }
+
+ true
+ } else {
+ log::info!("queue: no more events");
+ false
+ }
+ }
+}
diff --git a/examples/agdk-mainloop/src/lib.rs b/examples/agdk-mainloop/src/lib.rs
index 166ee69..a2f94e7 100644
--- a/examples/agdk-mainloop/src/lib.rs
+++ b/examples/agdk-mainloop/src/lib.rs
@@ -1,4 +1,7 @@
-use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
+use android_activity::{
+ input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
+ AndroidApp, InputStatus, MainEvent, PollEvent,
+};
use log::info;
#[no_mangle]
@@ -9,6 +12,8 @@ fn android_main(app: AndroidApp) {
let mut redraw_pending = true;
let mut native_window: Option = None;
+ let mut combining_accent = None;
+
while !quit {
app.poll_events(
Some(std::time::Duration::from_secs(1)), /* timeout */
@@ -68,11 +73,56 @@ fn android_main(app: AndroidApp) {
if let Some(native_window) = &native_window {
redraw_pending = false;
- // Handle input
- app.input_events(|event| {
- info!("Input Event: {event:?}");
- InputStatus::Unhandled
- });
+ // Handle input, via a lending iterator
+ match app.input_events_iter() {
+ Ok(mut iter) => loop {
+ info!("Checking for next input event...");
+ if !iter.next(|event| {
+ match event {
+ InputEvent::KeyEvent(key_event) => {
+ let combined_key_char = character_map_and_combine_key(
+ &app,
+ key_event,
+ &mut combining_accent,
+ );
+ info!("KeyEvent: combined key: {combined_key_char:?}")
+ }
+ InputEvent::MotionEvent(motion_event) => {
+ println!("action = {:?}", motion_event.action());
+ match motion_event.action() {
+ MotionAction::Up => {
+ let pointer = motion_event.pointer_index();
+ let pointer =
+ motion_event.pointer_at_index(pointer);
+ let x = pointer.x();
+ let y = pointer.y();
+
+ println!("POINTER UP {x}, {y}");
+ if x < 200.0 && y < 200.0 {
+ println!("Requesting to show keyboard");
+ app.show_soft_input(true);
+ }
+ }
+ _ => {}
+ }
+ }
+ InputEvent::TextEvent(state) => {
+ info!("Input Method State: {state:?}");
+ }
+ _ => {}
+ }
+
+ info!("Input Event: {event:?}");
+ InputStatus::Unhandled
+ }) {
+ info!("No more input available");
+ break;
+ }
+ },
+ Err(err) => {
+ log::error!("Failed to get input events iterator: {err:?}");
+ }
+ }
info!("Render...");
dummy_render(native_window);
@@ -83,6 +133,73 @@ fn android_main(app: AndroidApp) {
}
}
+/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent
+///
+/// This shows how to take a `KeyEvent` and look up its corresponding `KeyCharacterMap` and
+/// use that to try and map the `key_code` + `meta_state` to a unicode character or a
+/// dead key that be combined with the next key press.
+fn character_map_and_combine_key(
+ app: &AndroidApp,
+ key_event: &KeyEvent,
+ combining_accent: &mut Option,
+) -> Option {
+ let device_id = key_event.device_id();
+
+ let key_map = match app.device_key_character_map(device_id) {
+ Ok(key_map) => key_map,
+ Err(err) => {
+ log::error!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}");
+ return None;
+ }
+ };
+
+ match key_map.get(key_event.key_code(), key_event.meta_state()) {
+ Ok(KeyMapChar::Unicode(unicode)) => {
+ // Only do dead key combining on key down
+ if key_event.action() == KeyAction::Down {
+ let combined_unicode = if let Some(accent) = combining_accent {
+ match key_map.get_dead_char(*accent, unicode) {
+ Ok(Some(key)) => {
+ info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
+ Some(key)
+ }
+ Ok(None) => None,
+ Err(err) => {
+ log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
+ None
+ }
+ }
+ } else {
+ info!("KeyEvent: Pressed '{unicode}'");
+ Some(unicode)
+ };
+ *combining_accent = None;
+ combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
+ } else {
+ Some(KeyMapChar::Unicode(unicode))
+ }
+ }
+ Ok(KeyMapChar::CombiningAccent(accent)) => {
+ if key_event.action() == KeyAction::Down {
+ info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
+ *combining_accent = Some(accent);
+ }
+ Some(KeyMapChar::CombiningAccent(accent))
+ }
+ Ok(KeyMapChar::None) => {
+ // Leave any combining_accent state in tact (seems to match how other
+ // Android apps work)
+ info!("KeyEvent: Pressed non-unicode key");
+ None
+ }
+ Err(err) => {
+ log::error!("KeyEvent: Failed to get key map character: {err:?}");
+ *combining_accent = None;
+ None
+ }
+ }
+}
+
/// Post a NOP frame to the window
///
/// Since this is a bare minimum test app we don't depend
diff --git a/examples/na-mainloop/src/lib.rs b/examples/na-mainloop/src/lib.rs
index 166ee69..a2f94e7 100644
--- a/examples/na-mainloop/src/lib.rs
+++ b/examples/na-mainloop/src/lib.rs
@@ -1,4 +1,7 @@
-use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
+use android_activity::{
+ input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
+ AndroidApp, InputStatus, MainEvent, PollEvent,
+};
use log::info;
#[no_mangle]
@@ -9,6 +12,8 @@ fn android_main(app: AndroidApp) {
let mut redraw_pending = true;
let mut native_window: Option = None;
+ let mut combining_accent = None;
+
while !quit {
app.poll_events(
Some(std::time::Duration::from_secs(1)), /* timeout */
@@ -68,11 +73,56 @@ fn android_main(app: AndroidApp) {
if let Some(native_window) = &native_window {
redraw_pending = false;
- // Handle input
- app.input_events(|event| {
- info!("Input Event: {event:?}");
- InputStatus::Unhandled
- });
+ // Handle input, via a lending iterator
+ match app.input_events_iter() {
+ Ok(mut iter) => loop {
+ info!("Checking for next input event...");
+ if !iter.next(|event| {
+ match event {
+ InputEvent::KeyEvent(key_event) => {
+ let combined_key_char = character_map_and_combine_key(
+ &app,
+ key_event,
+ &mut combining_accent,
+ );
+ info!("KeyEvent: combined key: {combined_key_char:?}")
+ }
+ InputEvent::MotionEvent(motion_event) => {
+ println!("action = {:?}", motion_event.action());
+ match motion_event.action() {
+ MotionAction::Up => {
+ let pointer = motion_event.pointer_index();
+ let pointer =
+ motion_event.pointer_at_index(pointer);
+ let x = pointer.x();
+ let y = pointer.y();
+
+ println!("POINTER UP {x}, {y}");
+ if x < 200.0 && y < 200.0 {
+ println!("Requesting to show keyboard");
+ app.show_soft_input(true);
+ }
+ }
+ _ => {}
+ }
+ }
+ InputEvent::TextEvent(state) => {
+ info!("Input Method State: {state:?}");
+ }
+ _ => {}
+ }
+
+ info!("Input Event: {event:?}");
+ InputStatus::Unhandled
+ }) {
+ info!("No more input available");
+ break;
+ }
+ },
+ Err(err) => {
+ log::error!("Failed to get input events iterator: {err:?}");
+ }
+ }
info!("Render...");
dummy_render(native_window);
@@ -83,6 +133,73 @@ fn android_main(app: AndroidApp) {
}
}
+/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent
+///
+/// This shows how to take a `KeyEvent` and look up its corresponding `KeyCharacterMap` and
+/// use that to try and map the `key_code` + `meta_state` to a unicode character or a
+/// dead key that be combined with the next key press.
+fn character_map_and_combine_key(
+ app: &AndroidApp,
+ key_event: &KeyEvent,
+ combining_accent: &mut Option,
+) -> Option {
+ let device_id = key_event.device_id();
+
+ let key_map = match app.device_key_character_map(device_id) {
+ Ok(key_map) => key_map,
+ Err(err) => {
+ log::error!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}");
+ return None;
+ }
+ };
+
+ match key_map.get(key_event.key_code(), key_event.meta_state()) {
+ Ok(KeyMapChar::Unicode(unicode)) => {
+ // Only do dead key combining on key down
+ if key_event.action() == KeyAction::Down {
+ let combined_unicode = if let Some(accent) = combining_accent {
+ match key_map.get_dead_char(*accent, unicode) {
+ Ok(Some(key)) => {
+ info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
+ Some(key)
+ }
+ Ok(None) => None,
+ Err(err) => {
+ log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
+ None
+ }
+ }
+ } else {
+ info!("KeyEvent: Pressed '{unicode}'");
+ Some(unicode)
+ };
+ *combining_accent = None;
+ combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
+ } else {
+ Some(KeyMapChar::Unicode(unicode))
+ }
+ }
+ Ok(KeyMapChar::CombiningAccent(accent)) => {
+ if key_event.action() == KeyAction::Down {
+ info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
+ *combining_accent = Some(accent);
+ }
+ Some(KeyMapChar::CombiningAccent(accent))
+ }
+ Ok(KeyMapChar::None) => {
+ // Leave any combining_accent state in tact (seems to match how other
+ // Android apps work)
+ info!("KeyEvent: Pressed non-unicode key");
+ None
+ }
+ Err(err) => {
+ log::error!("KeyEvent: Failed to get key map character: {err:?}");
+ *combining_accent = None;
+ None
+ }
+ }
+}
+
/// Post a NOP frame to the window
///
/// Since this is a bare minimum test app we don't depend