Skip to content

Commit af331e3

Browse files
committed
Rework input_events API and expose KeyCharacterMap bindings
With the way events are delivered via an `InputQueue` with `NativeActivity` there is no direct access to the underlying KeyEvent and MotionEvent Java objects and no `ndk` API that supports the equivalent of `KeyEvent.getUnicodeChar()` What `getUnicodeChar` does under the hood though is to do lookups into a `KeyCharacterMap` for the corresponding `InputDevice` based on the event's `key_code` and `meta_state` - which are things we can do via some JNI bindings for `KeyCharacterMap`. Although it's still awkward to expose an API like `key_event.get_unicode_char()` we can instead provide an API that lets you look up a `KeyCharacterMap` for any `device_id` and applications can then use that for character mapping. This approach is also more general than the `getUnicodeChar` utility since it exposes other useful state, such as being able to check what kind of keyboard input events are coming from (such as a full physical keyboard vs a virtual / 'predictive' keyboard) For consistency this exposes the same API through the game-activity backend, even though the game-activity backend is technically able to support unicode lookups via `getUnicodeChar` (since it has access to the Java `KeyEvent` object). This highlighted a need to be able to use other `AndroidApp` APIs while processing input, which wasn't possible with the `.input_events()` API design because the `AndroidApp` held a lock over the backend while iterating events. This changes `input_events()` to `input_events_iter()` which now returns a form of lending iterator and instead of taking a callback that gets called repeatedly by `input_events()` a similar callback is now passed to `iter.next(callback)`. The API isn't as ergonomic as I would have liked, considering that lending iterators aren't a standard feature for Rust yet but also since we still want to have the handling for each individual event go via a callback that can report whether an event was "handled". I think the slightly awkward ergonomics are acceptable though considering that the API will generally be used as an implementation detail within middleware frameworks like Winit. Since this is the first example where we're creating non-trivial Java bindings for an Android SDK API this adds some JNI utilities and establishes a pattern for how we can implement a class binding. It's an implementation detail but with how I wrote the binding I tried to keep in mind the possibility of creating a procmacro later that would generate some of the JNI boilerplate involved.
1 parent 6f72dde commit af331e3

File tree

15 files changed

+1555
-231
lines changed

15 files changed

+1555
-231
lines changed

android-activity/CHANGELOG.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,108 @@
1-
<!-- markdownlint-disable MD022 MD024 MD032 -->
1+
<!-- markdownlint-disable MD022 MD024 MD032 MD033 -->
22

33
# Changelog
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
9+
### Added
10+
- 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))
11+
- Added `KeyCharacterMap` JNI bindings to the corresponding Android SDK API ([#102](https://github.com/rust-mobile/android-activity/pull/102))
12+
- 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))
13+
14+
<details>
15+
<summary>Click here for an example of how to handle unicode character mapping:</summary>
16+
17+
```rust
18+
let mut combining_accent = None;
19+
// Snip
20+
21+
22+
let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
23+
match map.get(key_event.key_code(), key_event.meta_state()) {
24+
Ok(KeyMapChar::Unicode(unicode)) => {
25+
let combined_unicode = if let Some(accent) = combining_accent {
26+
match map.get_dead_char(accent, unicode) {
27+
Ok(Some(key)) => {
28+
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
29+
Some(key)
30+
}
31+
Ok(None) => None,
32+
Err(err) => {
33+
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
34+
None
35+
}
36+
}
37+
} else {
38+
info!("KeyEvent: Pressed '{unicode}'");
39+
Some(unicode)
40+
};
41+
combining_accent = None;
42+
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
43+
}
44+
Ok(KeyMapChar::CombiningAccent(accent)) => {
45+
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
46+
combining_accent = Some(accent);
47+
Some(KeyMapChar::CombiningAccent(accent))
48+
}
49+
Ok(KeyMapChar::None) => {
50+
info!("KeyEvent: Pressed non-unicode key");
51+
combining_accent = None;
52+
None
53+
}
54+
Err(err) => {
55+
log::error!("KeyEvent: Failed to get key map character: {err:?}");
56+
combining_accent = None;
57+
None
58+
}
59+
}
60+
} else {
61+
None
62+
};
63+
```
64+
65+
</details>
66+
867
### Changed
968
- 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))
69+
- `AndroidApp::input_events()` is replaced by `AndroidApp::input_events_iter()` ([#102](https://github.com/rust-mobile/android-activity/pull/102))
70+
71+
<details>
72+
<summary>Click here for an example of how to use `input_events_iter()`:</summary>
73+
74+
```rust
75+
match app.input_events_iter() {
76+
Ok(mut iter) => {
77+
loop {
78+
let read_input = iter.next(|event| {
79+
let handled = match event {
80+
InputEvent::KeyEvent(key_event) => {
81+
// Snip
82+
}
83+
InputEvent::MotionEvent(motion_event) => {
84+
// Snip
85+
}
86+
event => {
87+
// Snip
88+
}
89+
};
90+
91+
handled
92+
});
93+
94+
if !read_input {
95+
break;
96+
}
97+
}
98+
}
99+
Err(err) => {
100+
log::error!("Failed to get input events iterator: {err:?}");
101+
}
102+
}
103+
```
104+
105+
</details>
10106

11107
## [0.4.3] - 2022-07-30
12108
### Fixed

android-activity/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ native-activity = []
3838
log = "0.4"
3939
jni-sys = "0.3"
4040
cesu8 = "1"
41+
jni = "0.21"
4142
ndk = "0.7"
4243
ndk-sys = "0.4"
4344
ndk-context = "0.1"
4445
android-properties = "0.2"
4546
num_enum = "0.6"
4647
bitflags = "2.0"
4748
libc = "0.2"
49+
thiserror = "1"
4850

4951
[build-dependencies]
5052
cc = { version = "1.0", features = ["parallel"] }

android-activity/LICENSE

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1-
The third-party glue code, under the native-activity-csrc/ and game-activity-csrc/ directories
2-
is covered by the Apache 2.0 license only:
1+
# License
32

4-
Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
3+
## GameActivity
54

5+
The third-party glue code, under the game-activity-csrc/ directory is covered by
6+
the Apache 2.0 license only:
7+
8+
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
9+
10+
## SDK Documentation
11+
12+
Documentation for APIs that are direct bindings of Android platform APIs are covered
13+
by the Apache 2.0 license only:
14+
15+
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
16+
17+
## android-activity
618

719
All other code is dual-licensed under either
820

9-
* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)
10-
* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
21+
- MIT License (docs/LICENSE-MIT or <http://opensource.org/licenses/MIT>)
22+
- Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
1123

12-
at your option.
24+
at your option.

android-activity/src/error.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum AppError {
5+
#[error("Operation only supported from the android_main() thread: {0}")]
6+
NonMainThread(String),
7+
8+
#[error("Java VM or JNI error, including Java exceptions")]
9+
JavaError(String),
10+
11+
#[error("Input unavailable")]
12+
InputUnavailable,
13+
}
14+
15+
pub type Result<T> = std::result::Result<T, AppError>;
16+
17+
// XXX: we don't want to expose jni-rs in the public API
18+
// so we have an internal error type that we can generally
19+
// use in the backends and then we can strip the error
20+
// in the frontend of the API.
21+
//
22+
// This way we avoid exposing a public trait implementation for
23+
// `From<jni::errors::Error>`
24+
#[derive(Error, Debug)]
25+
pub(crate) enum InternalAppError {
26+
#[error("A JNI error")]
27+
JniError(jni::errors::JniError),
28+
#[error("A Java Exception was thrown via a JNI method call")]
29+
JniException(String),
30+
#[error("A Java VM error")]
31+
JvmError(jni::errors::Error),
32+
#[error("Input unavailable")]
33+
InputUnavailable,
34+
}
35+
36+
pub(crate) type InternalResult<T> = std::result::Result<T, InternalAppError>;
37+
38+
impl From<jni::errors::Error> for InternalAppError {
39+
fn from(value: jni::errors::Error) -> Self {
40+
InternalAppError::JvmError(value)
41+
}
42+
}
43+
impl From<jni::errors::JniError> for InternalAppError {
44+
fn from(value: jni::errors::JniError) -> Self {
45+
InternalAppError::JniError(value)
46+
}
47+
}
48+
49+
impl From<InternalAppError> for AppError {
50+
fn from(value: InternalAppError) -> Self {
51+
match value {
52+
InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
53+
InternalAppError::JniException(msg) => AppError::JavaError(msg),
54+
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
55+
InternalAppError::InputUnavailable => AppError::InputUnavailable,
56+
}
57+
}
58+
}

android-activity/src/game_activity/input.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use num_enum::{IntoPrimitive, TryFromPrimitive};
1717
use std::convert::TryInto;
1818

19-
use crate::game_activity::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
19+
use crate::activity_impl::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
2020
use crate::input::{Class, Source};
2121

2222
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
@@ -1274,6 +1274,12 @@ impl<'a> KeyEvent<'a> {
12741274
action.try_into().unwrap()
12751275
}
12761276

1277+
#[inline]
1278+
pub fn action_button(&self) -> KeyAction {
1279+
let action = self.ga_event.action as u32;
1280+
action.try_into().unwrap()
1281+
}
1282+
12771283
/// Returns the last time the key was pressed. This is on the scale of
12781284
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
12791285
///

0 commit comments

Comments
 (0)