mirror of
https://github.com/rust-mobile/android-activity.git
synced 2026-07-11 09:08:53 +00:00
af331e3bff
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.
224 lines
9.8 KiB
Rust
224 lines
9.8 KiB
Rust
use android_activity::{
|
|
input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
|
|
AndroidApp, InputStatus, MainEvent, PollEvent,
|
|
};
|
|
use log::info;
|
|
|
|
#[no_mangle]
|
|
fn android_main(app: AndroidApp) {
|
|
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
|
|
|
let mut quit = false;
|
|
let mut redraw_pending = true;
|
|
let mut native_window: Option<ndk::native_window::NativeWindow> = None;
|
|
|
|
let mut combining_accent = None;
|
|
|
|
while !quit {
|
|
app.poll_events(
|
|
Some(std::time::Duration::from_secs(1)), /* timeout */
|
|
|event| {
|
|
match event {
|
|
PollEvent::Wake => {
|
|
info!("Early wake up");
|
|
}
|
|
PollEvent::Timeout => {
|
|
info!("Timed out");
|
|
// Real app would probably rely on vblank sync via graphics API...
|
|
redraw_pending = true;
|
|
}
|
|
PollEvent::Main(main_event) => {
|
|
info!("Main event: {:?}", main_event);
|
|
match main_event {
|
|
MainEvent::SaveState { saver, .. } => {
|
|
saver.store("foo://bar".as_bytes());
|
|
}
|
|
MainEvent::Pause => {}
|
|
MainEvent::Resume { loader, .. } => {
|
|
if let Some(state) = loader.load() {
|
|
if let Ok(uri) = String::from_utf8(state) {
|
|
info!("Resumed with saved state = {uri:#?}");
|
|
}
|
|
}
|
|
}
|
|
MainEvent::InitWindow { .. } => {
|
|
native_window = app.native_window();
|
|
redraw_pending = true;
|
|
}
|
|
MainEvent::TerminateWindow { .. } => {
|
|
native_window = None;
|
|
}
|
|
MainEvent::WindowResized { .. } => {
|
|
redraw_pending = true;
|
|
}
|
|
MainEvent::RedrawNeeded { .. } => {
|
|
redraw_pending = true;
|
|
}
|
|
MainEvent::InputAvailable { .. } => {
|
|
redraw_pending = true;
|
|
}
|
|
MainEvent::ConfigChanged { .. } => {
|
|
info!("Config Changed: {:#?}", app.config());
|
|
}
|
|
MainEvent::LowMemory => {}
|
|
|
|
MainEvent::Destroy => quit = true,
|
|
_ => { /* ... */ }
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if redraw_pending {
|
|
if let Some(native_window) = &native_window {
|
|
redraw_pending = false;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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<char>,
|
|
) -> Option<KeyMapChar> {
|
|
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
|
|
/// on any GPU graphics APIs but we do need to at least
|
|
/// convince Android that we're drawing something and are
|
|
/// responsive, otherwise it will stop delivering input
|
|
/// events to us.
|
|
fn dummy_render(native_window: &ndk::native_window::NativeWindow) {
|
|
unsafe {
|
|
let mut buf: ndk_sys::ANativeWindow_Buffer = std::mem::zeroed();
|
|
let mut rect: ndk_sys::ARect = std::mem::zeroed();
|
|
ndk_sys::ANativeWindow_lock(
|
|
native_window.ptr().as_ptr() as _,
|
|
&mut buf as _,
|
|
&mut rect as _,
|
|
);
|
|
// Note: we don't try and touch the buffer since that
|
|
// also requires us to handle various buffer formats
|
|
ndk_sys::ANativeWindow_unlockAndPost(native_window.ptr().as_ptr() as _);
|
|
}
|
|
}
|