-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
12 changed files
with
829 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>) | ||
|
||
## 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 <http://www.apache.org/licenses/LICENSE-2.0>) | ||
|
||
## 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 <http://opensource.org/licenses/MIT>) | ||
- Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>) | ||
|
||
at your option. | ||
at your option. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T> = std::result::Result<T, AppError>; | ||
|
||
// 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<jni::errors::Error>` | ||
#[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<T> = std::result::Result<T, InternalAppError>; | ||
|
||
impl From<jni::errors::Error> for InternalAppError { | ||
fn from(value: jni::errors::Error) -> Self { | ||
InternalAppError::JvmError(value) | ||
} | ||
} | ||
impl From<jni::errors::JniError> for InternalAppError { | ||
fn from(value: jni::errors::JniError) -> Self { | ||
InternalAppError::JniError(value) | ||
} | ||
} | ||
|
||
impl From<InternalAppError> 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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.