Robert Bragg fab31d3408 Thin MotionEvent + KeyEvent types with lifetimes
This patch is addressing two notable issues:
1. MotionEvents with the GameActivity backend were extremely large and
   forced us to copy lots of redundant padding from the internal
   circular buffer of events when calling an apps `input_events()`
   callback.
2. MotionEvents + KeyEvents with the NativeActivity backend (re-exported
   from the ndk crate) simply wrapped a raw pointer but had no lifetime
   that would stop applications from keeping them too long and then
   potentially dereferencing an invalid pointer.

The common change is that `InputEvent` is now defined with a lifetime
parameter which makes it possible for Motion and Key events to hold
references that let them avoid copying large amounts of state.

The `Source` and `Class` enums were also moved out of the GameActivity
backend so they could be shared since there was some inconsistency
between the backends with these types.

Overall this doesn't, practically, affect the public API

GameActivity
============

On the GameActivity side the events will now point into the internal
circular buffer that is iterated during `input_events()` and so
the `MotionEvent` type is now a thin wrapper over a reference.

This removes the `Iterator` implementations we had internally for
iterating key/motion events because we effectively need a lending
iterator now. It's also noted that while our MSRV is 1.60 we can't use
GATs to implement the interation in terms of a LendingIterator trait.
(This is fine in practice because this iteration is a private
implementation detail and so we can have lending iteration without any
traits for now)

NativeActivity
==============

On the NativeActivity side we now have newtype wrappers around
MotionEvent and KeyEvent from the ndk crate. These newtypes add a
lifetime and implement all the same passthrough methods as the
corresponding GameActivity types.

This added more boilerplate to the NativeActivity backend but it
also improves consistency between the backends.

Fixes: #40
Fixes: #41
2022-11-11 20:44:19 +00:00
2022-08-09 22:43:22 +02:00
2022-11-10 19:28:01 +00:00
2022-11-11 20:44:18 +00:00

Overview

android-activity provides a "glue" layer for building native Rust applications on Android, supporting multiple Activity base classes. It's comparable to android_native_app_glue.c for C/C++ applications.

android-activity supports NativeActivity or GameActivity from the Android Game Development Kit and can be extended to support additional base classes.

android-activity provides a way to load a cdylib via the onCreate method of your Activity class; run an android_main() function in a separate thread from the Java main thread and marshal events (such as lifecycle events and input events) between Java and your native thread.

Example

cargo init --lib --name=example

Cargo.toml

[dependencies]
log = "0.4"
android_logger = "0.11"
android-activity = { git = "https://github.com/rib/android-activity/", features = [ "native-activity" ] }

[lib]
crate_type = ["cdylib"]

lib.rs

use log::info;
use android_activity::{PollEvent, MainEvent};

#[no_mangle]
fn android_main(app: AndroidApp) {
    android_logger::init_once(
        android_logger::Config::default().with_min_level(log::Level::Info)
    );

    loop {
        app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| {
            match event {
                PollEvent::Wake => { info!("Early wake up"); },
                PollEvent::Timeout => { info!("Hello, World!"); },
                PollEvent::Main(main_event) => {
                    info!("Main event: {:?}", main_event);
                    match main_event {
                        MainEvent::Destroy => { return; }
                        _ => {}
                    }
                },
                _ => {}
            }

            app.input_events(|event| {
                info!("Input Event: {event:?}");
                InputStatus::Unhandled
            });
        });
    }
}
rustup target add aarch64-linux-android
cargo install cargo-apk
cargo apk run
adb logcat example:V *:S

Game Activity

Originally the aim was to enable support for building Rust applications based on the GameActivity class provided by Google's Android Game Development Kit which can also facilitate integration with additional AGDK libraries including:

  1. Game Text Input: a library to help fullscreen native applications utilize the Android soft keyboard.
  2. Game Controller Library, aka 'Paddleboat': a native library designed to help support access to game controller inputs.
  3. Frame Pacing Library, aka ' Swappy': a library that helps OpenGL and Vulkan games achieve smooth rendering and correct frame pacing on Android.
  4. Memory Advice API: an API to help applications monitor their own memory usage to stay within safe limits for the system.
  5. Oboe audio library: a low-latency audio API for native applications.

Since GameActivity is based on the widely used AppCompatActivity base class, it also provides a variety of back ported Activity APIs which can make it more practical to support a wider range of devices and Android versions.

Native Activity

This project also supports NativeActivity based applications. Although NativeActivity is more limited than GameActivity and does not derive from AppCompatActivity it can sometimes still be convenient to build on NativeActivity in situations where you are using a limited/minimal build system that is not able to compile Java or Kotlin code or fetch from Maven repositories - this is because NativeActivity is included as part of the Android platform.

Design

Compatibility

All Activity classes are supported via a common API that enables you to write Activity subclass agnostic code wherever you don't depend on features that are specific to a particular subclass.

For example, it makes it possible to have a Winit backend that supports Android applications running with different Activity classes.

API Summary

android_main entrypoint

The glue crates define a standard entrypoint ABI for your cdylib that looks like:

use android_activity::AndroidApp;

#[no_mangle]
fn android_main(app: AndroidApp) {
    ...
}

There's currently no high-level macro provided for things like initializing logging or allowing the main function to return a Result<> since it's expected that different downstream frameworks may each have differing opinions on the details and may want to provide their own macros.

AndroidApp

Your android_main() function is passed an AndroidApp struct to access state about your running application and handle synchronized interaction between your native Rust application and the Activity running on the Java main thread.

For example, the AndroidApp API enables:

  1. Access to Android lifecycle events
  2. Notifications of SurfaceView lifecycle events
  3. Access to input events
  4. Ability to save and restore state each time your process stops and starts
  5. Access application Configuration state
  6. internal/external/obb filesystem paths

Note: that some of the AndroidApp APIs (such as for polling events) are only deemed safe to use from the application's main thread

Synchronized event callbacks

The AndroidApp::poll_events() API is similar to the Winit EventLoop::run API in that it takes a FnMut closure that is called for each outstanding event (such as for lifecycle events). This design ensures the glue layer can transparently handle any required synchronization with Java before and after each callback.

For example, when the Java main thread notifies the glue layer that its SurfaceView is being destroyed the Java thread will then block until it gets an explicit acknowledgement that the native application has had an opportunity to react to this notification. The glue layer will automatically release the blocked Java thread once it has delivered the corresponding event.

For example:

use android_activity::{AndroidApp, MainEvent, PollEvent, InputStatus};
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 render_state: Option<()> = Default::default();

    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 { .. } => {
                                render_state = Some(());
                                redraw_pending = true;
                            }
                            MainEvent::TerminateWindow { .. } => {
                                render_state = 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(_rs) = render_state {
                        redraw_pending = false;

                        // Handle input
                        app.input_events(|event| {
                            info!("Input Event: {event:?}");
                            InputStatus::Unhandled
                        });

                        info!("Render...");
                    }
                }
            },
        );
    }
}
S
Description
No description provided
Readme 2.1 MiB
Languages
Rust 86.9%
C 8.1%
C++ 4.8%
Shell 0.2%