mirror of
https://github.com/rust-mobile/android-activity.git
synced 2026-07-10 16:54:10 +00:00
1ece2ad87d
Update example (copy from na-mainloop)
263 lines
10 KiB
Markdown
263 lines
10 KiB
Markdown
# 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`][ndk_concepts]
|
|
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.
|
|
|
|
[`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
|
|
|
|
### 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
|
|
```rust
|
|
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:?}");
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
```
|
|
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][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.
|
|
|
|
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.
|
|
|
|
[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
|
|
|
|
# Native Activity
|
|
|
|
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.
|
|
|
|
[NativeActivity]: https://developer.android.com/reference/android/app/NativeActivity
|
|
|
|
# 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](https://github.com/rib/winit/tree/agdk-game-activity)
|
|
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:
|
|
|
|
```rust
|
|
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...");
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|