diff --git a/README.md b/README.md index ec1eb6e..99a1e57 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ It's comparable to [`android_native_app_glue.c`][ndk_concepts] for C/C++ applications and is an alternative to the [ndk-glue] crate. `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()` +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. @@ -29,9 +29,9 @@ applications. [ndk-glue]: https://crates.io/crates/ndk-glue [agdk]: https://developer.android.com/games/agdk/overview -## Example +## Quick Start -Cargo.toml +**Cargo.toml:** ```toml [dependencies] @@ -47,16 +47,20 @@ _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 +**lib.rs:** ```rust +use std::sync::OnceLock; use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent}; +// - Called on a dedicated Activity main loop thread, spawned after `android_on_create` returns +// - May be called multiple times if your Activity is destroyed and recreated. +// - Note: this symbol has a "Rust" ABI (default), not "C" ABI. #[unsafe(no_mangle)] fn android_main(app: AndroidApp) { // `android_main` is tied to your `Activity` lifecycle, not your application lifecycle - // and so it may be called multiple times if your activity is destroyed and recreated. + // and so it may be called multiple times if your Activity is destroyed and recreated. // // Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state // multiple times. @@ -77,7 +81,7 @@ fn android_main(app: AndroidApp) { // be associated with any `Activity` and it's methods will effectively be no-ops. // // You should return from `android_main` and if your `Activity` gets recreated then - // a new `AndroidApp` will be passsed to a new invocation of `android_main`. + // a new `AndroidApp` will be passed to a new invocation of `android_main`. MainEvent::Destroy => { return; } _ => {} } @@ -101,11 +105,36 @@ cargo apk run adb logcat example:V *:S ``` +_Note: although `cargo apk` is convenient for this quick start example, it's +generally recommended that you should use a more-standard, Gradle-based build +system for your Android application and use something like `cargo ndk` for +building your Rust code into a `cdylib` that is then packaged via Gradle._ + +## Full Examples + +See [this collection of +examples](https://github.com/rust-mobile/rust-android-examples) (based on both +`GameActivity` and `NativeActivity`). + +Each example is a standalone Android Studio project that can serve as a +convenient template for starting a new project. + +For the examples based on middleware frameworks (Winit or Egui) they also +aim to demonstrate how it's possible to write portable code that will run on +Android and other systems. + ## Optional `android_on_create` entry point -`android-activity` also supports an optional `android_on_create` entry point that gets called from the -`Activity.onCreate()` callback before `android_main()` is called, allowing for doing some setup work on the Java main -thread before the main Rust code starts running. +`android-activity` also supports an optional `android_on_create` entry point +that gets called from the `Activity.onCreate()` callback before `android_main()` +is called. + +`android_on_create` is called from the Java main / UI thread before the +`android_main` thread is spawned. + +Considering that many Android SDK APIs (such as `android.view.View`) must be +accessed from the main thread, `android_on_create` can be a good place to do any +setup work that needs to be done on the Java main thread. For example: @@ -131,80 +160,192 @@ fn android_on_create(state: &android_activity::OnCreateState) { } ``` -## Full Examples +_(Note: there is also an `AndroidApp::run_on_java_main_thread()` method that +gives another way to run code on the Java main thread for some use cases)_ -See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`). +## Should I use NativeActivity or GameActivity? -Each example is a standalone project that may also be a convenient templates for starting a new project. +To learn more about the `NativeActivity` class that's shipped with Android see +[here](https://developer.android.com/ndk/guides/concepts#naa). -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. +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) -## Should I use NativeActivity or GameActivity? +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, but +GameActivity is likely to be the better longer-term choice, due to being based +on `AppCompatActivity` and having built in support for input methods (such as +onscreen keyboards). -To learn more about the `NativeActivity` class that's shipped with Android see [here](https://developer.android.com/ndk/guides/concepts#naa). +### 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) +- Good for: Simple apps, quick prototyping, limited text input support +- Setup: Just add the feature flag +- Limitations: No built-in input method support (can only receive physical key + events from soft keyboards that typically only allows basic ascii input) -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. +The unique advantage of the `NativeActivity` class is that it's shipped as part +of the Android OS and so you can use it without needing to compile or link any +Java or Kotlin code. -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. +`NativeActivity` is technically the only way to build a native Android +application purely in Rust without any Java or Kotlin code at all. -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. +The most significant limitation of `NativeActivity` is that it doesn't have +built-in support for input methods (such as onscreen keyboards) and so it's +often not a good choice for applications that need to support text input. -## Switching from ndk-glue to android-activity +Since some soft keyboards will deliver physical key events for basic ascii input +then `NativeActivity` can enable basic text input for prototyping but this is +unlikely to be sufficient for production applications. -### Winit-based applications +For advanced use cases, it would be possible to provide custom `InputConnection` +support in conjunction with `NativeActivity` but this isn't something that +`android-activity` provides out of the box currently. -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). +### GameActivity -Winit-based applications can follow the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) 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 `#[unsafe(no_mangle)]fn android_main(app: AndroidApp)` entry point. +- Good for: Apps needing text input, modern AndroidX features +- Setup requirements: + - Add gradle dependency: `androidx.games:games-activity:4.4.0` + - Enable the `game-activity` feature in Cargo.toml + - **Important**: Do NOT enable prefab support [details here](#don't-compile-and-link-the-upstream-gameactivity-prefab-c-glue-layer) +- Provides: IME support, AppCompatActivity features -See the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) for more details and also see the [Winit-based examples here](https://github.com/rust-mobile/rust-android-examples). +`GameActivity` has built in support for input methods via the `GameTextInput` +library and so is a better choice for applications that need to support text +input. -### Middleware crates (i.e. not applications) +`GameActivity` allows you to update the `ImeOptions` and actions associated with +the soft keyboard as well as receive IME span updates for tracking the user's +text input state. -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. +`GameActivity` is based on the [`AppCompatActivity`] class, which is a standard +Jetpack / AndroidX class that offers a lot of built-in functionality to help +with compatibility across different Android versions and devices. -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). +### Game Activity Library Version -### Other, non-Winit-based applications +`android-activity` currently supports the [`GameActivity` 4.4.0 Jetpack +library](https://developer.android.com/jetpack/androidx/releases/games) and is +backwards compatible with the previous `4.0.0` stable release. We can't +guarantee that the next 4.x stable release will be compatible, but it's fairly +likely that it will be. -The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be: +Your Android package should depend on `androidx.games:games-activity:4.4.0` from +Google's Maven repository. -1. Remove `ndk-glue` from your Cargo.toml -2. Add a dependency on `android-activity`, like `android-activity = { version="0.6", features = [ "native-activity" ] }` -3. Optionally add a dependency on `android_logger = "0.13.0"` -4. Update the `main` entry point to look like this: +Read the upstream [GameActivity getting +started](https://developer.android.com/games/agdk/game-activity/get-started) +guide for more details on how to add the GameActivity library to your project. -```rust -use android_activity::AndroidApp; +#### Don't compile and link the upstream GameActivity 'prefab' (C++ glue) layer -#[no_mangle] -fn android_main(app: AndroidApp) { - android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info)); -} -``` +**Important**: Do _not_ follow upstream instructions to enable native prefab +support for `GameActivity` that will compile and link the upstream C++ glue +layer as part of your build. The upstream glue layer is not directly compatible +with `android-activity` which provides its own native glue layer that integrates +with Rust. -See this minimal [NativeActivity Mainloop](https://github.com/rust-mobile/android-activity/tree/main/examples/na-mainloop) for more details about how to poll for events. +I.e. you do _not_ need to enable prefabs via your `build.gradle` file: -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) +```gradle +buildFeatures { + prefab true +} +``` -### Design Summary / Motivation behind android-activity +and do _not_ add a snippet like this to your `CMakeLists.txt` file: -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: +```cmake +find_package(game-activity REQUIRED CONFIG) +target_link_libraries(${PROJECT_NAME} PUBLIC log android +game-activity::game-activity_static) +``` -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`. +### Planning to Implement an Activity Subclass + +It's not possible to subclass an Activity from Rust / JNI code alone. + +Keep in mind that Android's design directs many events via the `Activity` class +which can only be processed by overloading some associated `Activity` method, so +if you want to handle those events then you will need to implement an `Activity` +subclass and overload the relevant methods. + +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. + +_At the end of the day, Android's application programming model is fundamentally +based around a Java VM running Java/Kotlin code that can optionally call into +native code (not the other way around)._ + +## 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 its [`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`. + + It's possible to write an application with `android-activity` that can + gracefully handle repeated create -> run -> destroy cycles of the `Activity` + due to its avoidance of global state. Theoretically you could even run + multiple `Activity` instances at the same (though since `NativeActivity` and + `GameActivity` were designed for fullscreen games, that only need a single + Activity, this is not a common use case). [`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input [`AppCompatActivity`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity ## MSRV -We aim to (at least) support stable releases of Rust from the last three months. Rust has a 6 week release cycle which means we will support the last three stable releases. -For example, when Rust 1.69 is released we would limit our `rust-version` to 1.67. +We aim to (at least) support stable releases of Rust from the last three months. +Rust has a 6 week release cycle which means we will support the last three +stable releases. For example, when Rust 1.69 is released we would limit our +`rust-version` to 1.67. + +We will only bump the `rust-version` at the point where we either depend on a +new features or a dependency has increased its MSRV, and we won't be greedy. In +other words we will only set the MSRV to the lowest version that's _needed_. + +MSRV updates are not considered to be inherently semver breaking (unless a new +feature is exposed in the public API) and so a `rust-version` change may happen +in patch releases. + +## Game Activity Library Versioning Policy + +Any single release of `android-activity` will support a specific version of the +Game Activity Jetpack / AndroidX library (documented above). + +The required version of the Game Activity library does not form part of our Rust +semver contract, since it doesn't affect the public Rust API of +`android-activity`. -We will only bump the `rust-version` at the point where we either depend on a new features or a dependency has increased its MSRV, and we won't be greedy. In other words we will only set the MSRV to the lowest version that's _needed_. +This means that a new patch release of `android-activity` may update the +required version of `GameActivity`, which may require users to update how they +package their application. -MSRV updates are not considered to be inherently semver breaking (unless a new feature is exposed in the public API) and so a `rust-version` change may happen in patch releases. +This is similar to how MSRV updates work, where new toolchain requirements can +affect how you build your application but that change is orthogonal to the +public API of the crate.