Skip to main content

Crate android_activity

Crate android_activity 

Source
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:

  1. NativeActivity - Built in to Android, this doesn’t require compiling any Java or Kotlin code.
  2. GameActivity - From the Android Game Development Kit, it has more sophisticated input handling support than NativeActivity. GameActivity is also based on the AndroidAppCompat class 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:

  1. android_on_create (optional) - This runs early, on the Java main / UI thread, during Activity.onCreate(). It can be a good place to initialize logging and JNI bindings.
  2. android_main (required) - This run a dedicated main loop thread for handling lifecycle and input events for your Activity.

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 JavaVM and android.content.Context instance will be associated with the ndk_context crate so that other, independent, Rust crates are able to find a JavaVM for making JNI calls.
  • The JavaVM will 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§

pub use ndk;
pub use ndk_sys;

Modules§

error
input

Structs§

AndroidApp
The top-level state and interface for a native Rust application
AndroidAppWaker
A means to wake up the main thread while it is blocked waiting for I/O
ConfigurationRef
A runtime-replacable reference to ndk::configuration::Configuration.
OnCreateState
The state passed to the optional android_on_create entry point if available.
Rect
A rectangle with integer edge coordinates. Used to represent window insets, for example.
StateLoader
An interface for loading application state during MainEvent::Resume events
StateSaver
An interface for saving application state during MainEvent::SaveState events
WindowManagerFlags
Flags for AndroidApp::set_window_flags as per the android.view.WindowManager.LayoutParams Java API

Enums§

InputStatus
Indicates whether an application has handled or ignored an event
MainEvent
An application event delivered during AndroidApp::poll_events
PollEvent
An event delivered during AndroidApp::poll_events