Expand description
A glue layer for building standalone, Rust applications on Android
This crate 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.
Currently the crate supports two Activity base classes:
NativeActivity- Built in to Android, this doesn’t require compiling any Java or Kotlin code.GameActivity- From the Android Game Development Kit, it has more sophisticated input handling support thanNativeActivity.GameActivityis also based on theAndroidAppCompatclass which can help with supporting a wider range of devices.
Standalone applications based on this crate need to be built as cdylib
libraries, like:
[lib]
crate-type=["cdylib"]§Lifecycle of an Activity
Keep in mind that Android’s application programming model is based around
the
lifecycle
of Activity and Service components, and not the lifecycle of the
application process.
An Android application may have multiple Activity and Service
instances created and destroyed over its lifetime, and each of these
Activity and Service instances will have their own lifecycles that
are independent from the lifecycle of the application process.
See the Android SDK activity lifecycle
documentation
for more details on the Activity lifecycle.
Although native applications will typically only have a single instance of
NativeActivity or GameActivity, it’s possible for these activities
to be created and destroyed multiple times within the lifetime of your
application process.
Although NativeActivity and GameActivity were historically designed
for full-screen games and based on the assumption that there would only be a
single instance of these activities, it is good to keep in mind that Android
itself makes no such assumption. It’s very common for non-native Android
applications to be tracking multiple Activity instances at the same time.
The android-activity crate is designed to be robust to multiple Activity
instances being created and destroyed over the lifetime of the application
process.
§Entrypoints
There are currently two supported entrypoints for an android-activity
application:
android_on_create(optional) - This runs early, on the Java main / UI thread, duringActivity.onCreate(). It can be a good place to initialize logging and JNI bindings.android_main(required) - This run a dedicated main loop thread for handling lifecycle and input events for yourActivity.
Important: Your android-activity entrypoints are tied to the lifecycle
of your native Activity (i.e. NativeActivity or GameActivity)
and not the lifecycle of your application process! This means that if your
Activity is destroyed and re-created (e.g. depending on how your
application handles configuration changes) then these entrypoints may be
called multiple times, for each Activity instance.
§Your AndroidManifest configureChanges state affects Activity re-creation
Beware that, by default, certain configuration changes (e.g. device
rotation) will cause the Android system to destroy and re-create your
Activity, which will lead to a MainEvent::Destroy event being sent to
your android_main() thread and then android_main() will be called again
as a new native Activity instance is created.
Since this can be awkward to handle, it is common practice to set the
android:configChanges property to indicate that your application can
handle these changes at runtime via events instead.
Example:
Here’s how you can set android:configChanges for your Activity in your
AndroidManifest.xml:
<activity
android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:label="NativeActivity Example"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:exported="true">
<!-- ... -->
</activity>§onCreate entrypoint: android_on_create (optional)
The android_on_create entry point will be called from the Java main
thread, within the Activity’s onCreate method, before the android_main
entry point is called.
This must be an exported, unmangled, "Rust" ABI function with the
signature fn android_on_create(state: &OnCreateState).
The easiest way to achieve this is with #[unsafe(no_mangle)] like this:
#[unsafe(no_mangle)]
fn android_on_create(state: &android_activity::OnCreateState) {
// Initialization code here
}(Note extern "Rust" is the default ABI)
I/O redirection: Before android_on_create() is called an I/O thread is
spawned that will handle redirecting standard input and output to the
Android log, visible via logcat.
OnCreateState provides access to the Java VM and a JNI reference to the
Activity instance, as well as any saved state from a previous instance of
the Activity.
Due to the way JNI class loading works, this can be a convenient place to
initialize JNI bindings because it’s called while the Activity’s
onCreate callback is on the stack, so the default class loader will be
able to find the application’s Java classes. See the Android
JNI tips
guide for more details on this.
This can also be a good place to initialize logging, since it’s called first.
Important: This entrypoint must not block for a long time or do heavy
work, since it’s running on the Java main thread and will block the
Activity from being created until it returns.
Blocking the Java main thread for too long may cause an “Application Not Responding” (ANR) dialog to be shown to the user, and cause users to force close your application.
Panic behavior: If android_on_create panics, the application will
abort. This is because the callback runs within a native JNI callback where
unwinding is not permitted. Ensure your initialization code either cannot
panic or uses catch_unwind internally if you want to allow partial
initialization failures.
§Example:
#[unsafe(no_mangle)]
fn android_on_create(state: &OnCreateState) {
static APP_ONCE: OnceLock<()> = OnceLock::new();
APP_ONCE.get_or_init(|| {
// Initialize logging...
//
// Remember, `android_on_create` may be called multiple times but, depending on
// the crate, logger initialization may panic if attempted multiple times.
});
let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) };
let activity = state.activity_as_ptr() as jni::sys::jobject;
// Although the thread is implicitly already attached (we are inside an onCreate native method)
// using `vm.attach_current_thread` here will use the existing attachment, give us an `&Env`
// reference and also catch Java exceptions.
if let Err(err) = vm.attach_current_thread(|env| -> jni::errors::Result<()> {
// SAFETY:
// - The `Activity` reference / pointer is at least valid until we return
// - By creating a `Cast` we ensure we can't accidentally delete the reference
let activity = unsafe { env.as_cast_raw::<JObject>(&activity)? };
// Do something with the activity on the Java main thread...
Ok(())
}) {
eprintln!("Failed to interact with Android SDK on Java main thread: {err:?}");
}
}§Main loop thread entrypoint: android_main (required)
Your application must always define an android_main function as an entry
point for running a main loop thread for your Activity.
This must be an exported, unmangled, "Rust" ABI function with the
signature fn android_main(app: AndroidApp).
The easiest way to achieve this is with #[unsafe(no_mangle)] like this:
#[unsafe(no_mangle)]
fn android_main(app: android_activity::AndroidApp) {
// Main loop code here
}(Note extern "Rust" is the default ABI)
Once your application’s Activity class has loaded and it calls onCreate
then android-activity will spawn a dedicated thread to run your
android_main function, separate from the Java thread that created the
corresponding Activity.
Before android_main() is called:
- A
JavaVMandandroid.content.Contextinstance will be associated with thendk_contextcrate so that other, independent, Rust crates are able to find a JavaVM for making JNI calls. - The
JavaVMwill be attached to the native thread (for JNI) - A Looper is attached to the Rust native thread.
Important: This thread must call AndroidApp::poll_events()
regularly in order to receive lifecycle and input events for the Activity.
Some Activity lifecycle callbacks on the Java main thread will block until
the next time poll_events() is called, so if you don’t call
poll_events() regularly you may trigger an ANR dialog and cause users to
force close your application.
Important: You should return from android_main() as soon as possible
if you receive a MainEvent::Destroy event from poll_events(). Most
AndroidApp methods will become a no-op after MainEvent::Destroy is
received, since it no longer has an associated Activity.
Important: Do not call std::process::exit() from your
android_main() function since that will subvert the normal lifecycle of
the Activity and other components. Keep in mind that code running in
android_main() does not logically own the entire process since there may
be other Android components (e.g. Services) running within the process.
§AndroidApp: State and Event Loop
AndroidApp provides an interface to query state for the application as
well as monitor events, such as lifecycle and input events for the
associated native Activity instance.
§Cheaply Cloneable AndroidApp
AndroidApp is intended to be something that can be cheaply passed around
within an application. It is reference-counted and can be cheaply cloned.
§Send and Sync AndroidApp (but…)
Although an AndroidApp implements Send and Sync you do need to take
into consideration that some APIs, such as AndroidApp::poll_events() are
explicitly documented to only be usable from your android_main() thread.
§No associated Activity after MainEvent::Destroy
After you receive a MainEvent::Destroy event from poll_events() then
the AndroidApp will no longer have an associated Activity and most of
its methods will become no-ops. You should return from android_main() as
soon as possible after receiving a Destroy event since your native
Activity no longer exists.
If a new Activity instance is created after that then a new
AndroidApp will be created for that new Activity instance and sent
to a new call to android_main().
Important: It’s not recommended to store an AndroidApp as global
static state and it should instead be passed around by reference within your
application so it can be reliably dropped when the Activity is destroyed
and you return from android_main().
§Android Extensible Enums
There are numerous enums in the android-activity API which are effectively
bindings to enums declared in the Android SDK which need to be considered
runtime extensible.
Any enum variants that come from the Android SDK may be extended in future versions of Android and your code could be exposed to new variants if you build an application that might be installed on new versions of Android.
This crate follows a convention of adding a hidden __Unknown(u32) variant
to these enums to ensure we can always do lossless conversions between the
integers from the SDK and our corresponding Rust enums. This can be
important in case you need to pass certain variants back to the SDK
regardless of whether you knew about that variants specific semantics at
compile time.
You should never include this __Unknown(u32) variant within any exhaustive
pattern match and should instead treat the enums like #[non_exhaustive]
enums that require you to add a catch-all for any unknown => {} values.
Any code that would exhaustively include the __Unknown(u32) variant when
pattern matching can not be guaranteed to be forwards compatible with new
releases of android-activity which may add new Rust variants to these
enums without requiring a breaking semver bump.
You can (infallibly) convert these enums to and from primitive u32 values
using .into():
For example, here is how you could ensure forwards compatibility with both
compile-time and runtime extensions of a SomeEnum enum:
match some_enum {
SomeEnum::Foo => {},
SomeEnum::Bar => {},
unhandled => {
let sdk_val: u32 = unhandled.into();
println!("Unhandled enum variant {some_enum:?} has SDK value: {sdk_val}");
}
}Re-exports§
Modules§
Structs§
- Android
App - The top-level state and interface for a native Rust application
- Android
AppWaker - A means to wake up the main thread while it is blocked waiting for I/O
- Configuration
Ref - A runtime-replacable reference to
ndk::configuration::Configuration. - OnCreate
State - The state passed to the optional
android_on_createentry point if available. - Rect
- A rectangle with integer edge coordinates. Used to represent window insets, for example.
- State
Loader - An interface for loading application state during MainEvent::Resume events
- State
Saver - An interface for saving application state during MainEvent::SaveState events
- Window
Manager Flags - Flags for
AndroidApp::set_window_flagsas per the android.view.WindowManager.LayoutParams Java API
Enums§
- Input
Status - Indicates whether an application has handled or ignored an event
- Main
Event - An application event delivered during
AndroidApp::poll_events - Poll
Event - An event delivered during
AndroidApp::poll_events