Update README

This commit is contained in:
Robert Bragg
2022-11-12 01:55:37 +00:00
parent fab31d3408
commit 9640893477
+71 -185
View File
@@ -3,57 +3,55 @@
`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`][ndk_concepts]
for C/C++ applications.
for C/C++ applications and is an alternative to the [ndk-glue] crate.
`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 your crate as a `cdylib` library via
the `onCreate` method of your Android `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.
`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.
So far it supports [`NativeActivity`] or [`GameActivity`] (from the
[Android Game Development Kit][agdk]) and there's also interest in supporting a first-party
`RustActivity` base class that could be better tailored to the needs of Rust
applications.
[`Activity`]: https://developer.android.com/reference/android/app/Activity
[`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
[ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
[`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity
[ndk-glue]: https://crates.io/crates/ndk-glue
[agdk]: https://developer.android.com/games/agdk
### Example
```
cargo init --lib --name=example
```
# Example
Cargo.toml
```
[dependencies]
log = "0.4"
android_logger = "0.11"
android-activity = { git = "https://github.com/rib/android-activity/", features = [ "native-activity" ] }
android-activity = { version = "0.4", features = [ "native-activity" ] }
[lib]
crate_type = ["cdylib"]
```
_Note: that you will need to either specify the **"native-activity"** feature or **"game-activity"** feature to identify which `Activity` base class your application is based on_
lib.rs
```rust
use log::info;
use android_activity::{PollEvent, MainEvent};
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(
android_logger::Config::default().with_min_level(log::Level::Info)
);
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::Wake => { log::info!("Early wake up"); },
PollEvent::Timeout => { log::info!("Hello, World!"); },
PollEvent::Main(main_event) => {
info!("Main event: {:?}", main_event);
log::info!("Main event: {:?}", main_event);
match main_event {
MainEvent::Destroy => { return; }
_ => {}
@@ -63,7 +61,7 @@ fn android_main(app: AndroidApp) {
}
app.input_events(|event| {
info!("Input Event: {event:?}");
log::info!("Input Event: {event:?}");
InputStatus::Unhandled
});
});
@@ -78,186 +76,74 @@ cargo apk run
adb logcat example:V *:S
```
# Game Activity
# Full Examples
Originally the aim was to enable support for building Rust applications based on the
[GameActivity] class provided by [Google's Android Game Development Kit][agdk]
which can also facilitate integration with additional AGDK libraries including:
1. [Game Text Input](https://developer.android.com/games/agdk/add-support-for-text-input): a library
to help fullscreen native applications utilize the Android soft keyboard.
2. [Game Controller Library, aka 'Paddleboat'](https://developer.android.com/games/sdk/game-controller):
a native library designed to help support access to game controller inputs.
3. [Frame Pacing Library, aka ' Swappy'](https://developer.android.com/games/sdk/frame-pacing): a library
that helps OpenGL and Vulkan games achieve smooth rendering and correct frame pacing on Android.
4. [Memory Advice API](https://developer.android.com/games/sdk/memory-advice/overview): an API to
help applications monitor their own memory usage to stay within safe limits for the system.
5. [Oboe audio library](https://developer.android.com/games/sdk/oboe): a low-latency audio API for native
applications.
See [this collection of examples](https://github.com/rib/android-activity/tree/main/examples) (based on both `GameActivity` and `NativeActivity`).
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.
Each example is a standalone project that may also be a convenient templates for starting a new project.
[GameActivity]: https://developer.android.com/games/agdk/integrate-game-activity
[agdk]: https://developer.android.com/games/agdk
[AppCompatActivity]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
For the examples based on middleware frameworks (Winit and or Egui) they also aim to demonstrate how it's possible to write portable code that will run on Android and other systems.
# Native Activity
# Should I use NativeActivity or GameActivity?
This project also supports [`NativeActivity`][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.
To learn more about the `NativeActivity` class that's shipped with Android see [here](https://developer.android.com/ndk/guides/concepts#naa).
[NativeActivity]: https://developer.android.com/reference/android/app/NativeActivity
To learn more about the `GameActivity` class that's part of the [Android Game Developer's Kit][agdk] and also see a comparison with `NativeActivity` see [here](https://developer.android.com/games/agdk/game-activity)
# Design
Generally speaking, if unsure, `NativeActivity` may be more convenient to start with since you may not need to compile/link any Java or Kotlin code.
## Compatibility
It's expected that the `GameActivity` backend will gain more sophisticated input handling features over time (such as for supporting input via onscreen keyboards or game controllers) and only `GameActivity` is based on the [`AppCompatActivity`] subclass which you may want in some situations to help with compatibility across devices.
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.
Even if you start out using `NativeActivity` for the convenience, it's likely that most moderately complex applications will eventually need to define their own `Activity` subclass (either subclassing `NativeActivity` or `GameActivity`) which will require compiling at least a small amount of Java or Kotlin code. This is generally due to Android's design which directs numerous events via the `Activity` class which can only be processed by overloading some associated Activity method.
For example, it makes it possible to have a [Winit backend](https://github.com/rib/winit/tree/agdk-game-activity)
that supports Android applications running with different `Activity` classes.
# Switching from ndk-glue to android-activity
## API Summary
## Winit-based applications
Firstly; if you have a [Winit](https://crates.io/crates/winit) based application and also have an explicit dependency on `ndk-glue` your application will need to remove its dependency on `ndk-glue` for the 0.28 release of Winit which will be based on android-activity (Since glue crates, due to their nature, can't be compatible with alternative glue crates).
Winit-based applications can follow the [Android README](https://github.com/rust-windowing/winit#android) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[no_mangle]fn android_main(app: AndroidApp)` entry point.
### `android_main` entrypoint
The glue crates define a standard entrypoint ABI for your `cdylib` that looks like:
See the [Android README](https://github.com/rust-windowing/winit#android) for more details and also see the [Winit-based examples here](https://github.com/rib/android-activity/tree/main/examples).
```rust
## Middleware crates (i.e. not applications)
If you have a crate that would be considered a middleware library (for example using JNI to support access to Bluetooth, or Android's Camera APIs) then the crate should almost certainly remove any dependence on a specific glue crate because this imposes a strict compatibility constraint that means the crate can only be used by applications that use that exact same glue crate version.
Middleware libraries can instead look at using the [ndk-context](https://crates.io/crates/ndk-context) crate as a means for being able to use JNI without making any assumptions about the applications overall architecture. This way a middleware crate can work with alternative glue crates (including `ndk-glue` and `android-activity`) as well as work with embedded use cases (i.e. non-native applications that may just embed a dynamic library written in Rust to implement certain native functions).
## Other, non-Winit-based applications
The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be:
1. Remove `ndk-glue` from your Cargo.toml
2. Add a dependency on `android-activity`, like `android-activity = { version="0.4", features = [ "native-activity" ] }`
3. Optionally add a dependency on `android_logger = "0.11.0"`
4. Update the `main` entry point to look like this:
```
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_
[`Configuration`]: https://developer.android.com/reference/android/content/res/Configuration
### 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:
```rust
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...");
}
}
},
);
}
}
```
See this minimal [NativeActivity Mainloop](https://github.com/rib/android-activity/tree/main/examples/na-mainloop) for more details about how to poll for events.
There is is no `#[ndk_glue::main]` replacement considering that `android_main()` entry point needs to be passed an `AndroidApp` argument which isn't compatible with a traditional `main()` function. Having an Android specific entry point also gives a place to initialize Android logging and handle other Android specific details (such as building an event loop based on the `app` argument)
## Design Summary / Motivation behind android-activity
Prior to working on android-activity, the existing glue crates available for building standalone Rust applications on Android were found to have a number of technical limitations that this crate aimed to solve:
1. **Support alternative Activity classes**: Prior glue crates were based on `NativeActivity` and their API precluded supporting alternatives. In particular there was an interest in the [`GameActivity`] class in conjunction with it's [`GameTextInput`] library that can facilitate onscreen keyboard support. This also allows building applications based on the standard [`AppCompatActivity`] base class which isn't possible with `NativeActivity`. Finally there was interest in paving the way towards supporting a first-party `RustActivity` that could be best tailored towards the needs of Rust applications on Android.
2. **Encapsulate IPC + synchronization between the native thread and the JVM thread**: For example with `ndk-glue` the application itself needs to avoid race conditions between the native and Java thread by following a locking convention) and it wasn't clear how this would extend to support other requests (like state saving) that also require synchronization.
3. **Avoid static global state**: Keeping in mind the possibility of supporting applications with multiple native activities there was interest in having an API that didn't rely on global statics to track top-level state. Instead of having global getters for state then `android-activity` passes an explicit `app: AndroidApp` argument to the entry point that encapsulates the state connected with a single `Activity`.
[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input
[`AppCompatActivity`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity