android-activity

repository·main·Indexed 18 days ago

https://github.com/rust-mobile/android-activity

A Rust glue layer for building native Android applications using NativeActivity or GameActivity. It manages the lifecycle and event marshaling between the Android Java/Kotlin environment and a native Rust main loop, avoiding global static state to support lifecycle resilience and multi-activity configurations.

Tokens
18.1K
Snippets
42
Records
71
Agent score
63%

What's inside android-activity

  1. Understand the android-activity architecture and state management

    main

    Unlike older Android glue crates, android-activity is designed to avoid global static state. This provides several architectural benefits:

    1. Encapsulated State: Instead of using global getters to access application state, the entry point provides an explicit app: AndroidApp argument. This argument encapsulates the state connected to a single Activity.
    2. Lifecycle Resilience: Because it avoids global statics, applications can gracefully handle repeated create -> run -> destroy cycles of the Activity.
    3. Multi-Activity Support: The design theoretically allows for running multiple Activity instances simultaneously, as state is not tied to a global singleton.
    4. Thread Synchronization: The crate encapsulates the Inter-Process Communication (IPC) and synchronization required between the native thread and the JVM thread, reducing the risk of race conditions during state changes (like state saving).
  2. Compare NativeActivity and GameActivity

    main

    Choosing between NativeActivity and GameActivity depends on your application's requirements for input and complexity:

    NativeActivity

    • Best for: Simple apps, quick prototyping, or applications that require zero Java/Kotlin code.
    • Pros: Shipped with the Android OS; allows building purely in Rust without any Java/Kotlin.
    • Cons: Limited input method support (no built-in onscreen keyboard support; only physical key events).
    • Setup: Enable the native-activity feature in Cargo.toml.

    GameActivity

    • Best for: Modern games, apps requiring text input (IME), and apps using AndroidX features.
    • Pros: Built-in support for input methods (via GameTextInput), AppCompatActivity features, and better compatibility across Android versions.
    • Cons: Requires adding a Gradle dependency and compiling some Java/Kotlin code.
    • Setup:
      1. Add androidx.games:games-activity:4.4.0 to your Gradle dependencies.
      2. Enable the game-activity feature in Cargo.toml.
      3. Warning: Do NOT enable Android Prefab support in Gradle or CMake, as android-activity provides its own native glue layer.
  3. Choose between NativeActivity and GameActivity

    main

    When building Android applications with android-activity, you must decide which Android Activity class to target. This choice affects your ability to use certain Android features:

    • NativeActivity: The traditional approach for standalone native applications. It is more limited in terms of supporting modern Android features like advanced text input.
    • GameActivity: A more modern approach that supports the [GameTextInput] library, facilitating better onscreen keyboard support. It is the preferred choice for games and applications requiring robust text input.

    android-activity is designed to support both, moving away from the limitations of older glue crates that were strictly tied to NativeActivity.

  4. Run the agdk-mainloop example

    main

    The agdk-mainloop example is a minimal test application based on GameActivity. It demonstrates how to run a mainloop using android_activity::poll_events(), trace received events without rendering, and save/restore minimal application state.

    To run this example on an Android device or emulator, follow these steps:

    1. Set up environment variables: Ensure your NDK and SDK paths are exported.
    2. Prepare the Rust toolchain: Add the Android target and install cargo-ndk.
    3. Build the native library: Use cargo ndk to build for the arm64-v8a target and output the resulting JNI libraries to the Android project directory.
    4. Build and install the Android app: Use Gradle to build the APK and install it.
    5. Launch the activity: Use adb to start the MainActivity.
    # 1. Set environment variables
    export ANDROID_NDK_HOME="path/to/ndk"
    export ANDROID_HOME="path/to/sdk"
    
    # 2. Prepare toolchain
    rustup target add aarch64-linux-android
    cargo install cargo-ndk
    
    # 3. Build native library
    cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build
    
    # 4. Build and install app
    ./gradlew build
    ./gradlew installDebug
    
    # 5. Launch the app
    adb shell am start -n com.github.rust_mobile.agdkmainloop/.MainActivity
  5. Check MSRV and Game Activity Library compatibility

    main

    When integrating android-activity into your project, be aware of two non-API breaking constraints:

    Minimum Supported Rust Version (MSRV)

    android-activity aims to support the last three stable releases of Rust (approximately 6 months of releases). MSRV updates are considered orthogonal to the public API and may occur in patch releases.

    Game Activity Library Versioning

    Each release of android-activity supports a specific version of the Game Activity Jetpack / AndroidX library.

    Note: A patch release of android-activity might update the required version of the Game Activity library. If you update android-activity, you may need to update how you package your Android application to ensure the library versions match.

  6. Update to a new AGDK version

    main

    When updating android-activity to a new version of the Android Game Development Kit (AGDK), follow this checklist to rebase integration patches and regenerate bindings:

    1. Prepare the integration branch: Clone the android-games-sdk repository, add the Google remote, and rebase your integration branch onto the latest Google release branch using the appropriate <base> commit ID.
    2. Set Environment Variable: Set ANDROID_GAMES_SDK to point to your external games-sdk branch to allow building android-activity against it during the update process.
    3. Regenerate Bindings: Run ./generate-bindings.sh to recreate the GameActivity FFI bindings.
    4. Update Build Configuration: Modify build.rs to include any new header files or source files introduced by the new AGDK version.
    5. Update Backend: Adjust the src/game-activity backend implementation if required by the new SDK version.
    6. Import Source: Once the external branch is ready, run ./import-games-sdk.sh to copy the new AGDK code into the android-activity repository.
    7. Document the Update: Reference the specific branch name and commit hash from the android-games-sdk repository in your android-activity commit and update CHANGELOG.md.
    # Example rebase workflow
    git clone git@github.com:rust-mobile/android-games-sdk.git
    cd android-games-sdk
    git remote add google https://android.googlesource.com/platform/frameworks/opt/gamesdk
    git fetch google
    git checkout -b android-activity-5.0.0 origin/android-activity-4.0.0
    git rebase --onto google/android-games-sdk-game-activity-release <base>
    # (where <base> is the upstream commit ID below our stack of integration patches)
  7. Build and run the na-mainloop example using Gradle

    main

    If you are using a standard Android build workflow, you can build and install the na-mainloop example using Gradle. This requires setting up your NDK and SDK environment variables and using cargo-ndk to compile the Rust components into JNI libraries before running the Gradle tasks.

    Prerequisites:

    • ANDROID_NDK_HOME and ANDROID_HOME must be set.
    • The aarch64-linux-android target must be added via rustup.
    • cargo-ndk must be installed.

    Steps:

    1. Compile the Rust code into the app/src/main/jniLibs/ directory using cargo ndk.
    2. Build the Android project using ./gradlew build.
    3. Install the debug APK using ./gradlew installDebug.
    4. Launch the NativeActivity via adb shell.
    export ANDROID_NDK_HOME="path/to/ndk"
    export ANDROID_HOME="path/to/sdk"
    
    rustup target add aarch64-linux-android
    cargo install cargo-ndk
    
    cargo ndk -t arm64-v8a -o app/src/main/jniLibs/  build
    ./gradlew build
    ./gradlew installDebug
    
    # To run:
    adb shell am start -n com.github.realfit_mobile.namainloop/android.app.NativeActivity
  8. Build and run the na-mainloop example using cargo-apk

    main

    Because this example does not require a custom Activity subclass, you can use cargo-apk for a simplified build and run workflow. This is the fastest way to test the NativeActivity implementation.

    Prerequisites:

    • ANDROID_NDK_HOME and ANDROID_SDK_HOME must be set.
    • The aarch64-linux-android target must be added via rustup.
    • cargo-apk must be installed.

    Steps:

    1. Build the APK using cargo apk build.
    2. Run the application directly using cargo apk run.
    export ANDROID_NDK_HOME="path/to/ndk"
    export ANDROID_SDK_HOME="path/to/sdk"
    
    rustup target add aarch64-linux-android
    cargo install cargo-apk
    
    cargo apk build
    cargo apk run
  9. Quick Start with android-activity

    main

    To use android-activity, configure your Cargo.toml to output a cdylib and enable either the native-activity or game-activity feature. You must implement the android_main function, which runs on a dedicated thread and handles the application's event loop.

    Important Lifecycle Note: android_main is tied to the Activity lifecycle, not the application lifecycle. If the Activity is destroyed and recreated, android_main may be called multiple times. Use std::sync::OnceLock to ensure global state (like logging) is only initialized once.

    [dependencies]
    log = "0.4"
    android_logger = "0.13"
    android-activity = { version = "0.6", features = [ "native-activity" ] }
    
    [lib]
    crate-type = ["cdylib"]
    use std::sync::OnceLock;
    use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
    
    #[unsafe(no_mangle)]
    fn android_main(app: AndroidApp) {
        static APP_ONCE: OnceLock<()> = OnceLock::new();
        APP_ONCE.get_or_init(|| {
            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)), |event| {
                match event {
                    PollEvent::Wake => { log::info!("Early wake up"); },
                    PollEvent::Timeout => { log::info!("Hello, World!"); },
                    PollEvent::Main(main_event) => {
                        match main_event {
                            MainEvent::Destroy => { return; }
                            _ => {}
                        }
                    },
                    _ => {}
                }
    
                app.input_events(|event| {
                    log::info!("Input Event: {event:?}");
                    InputStatus::Unhandled
                });
            });
        }
    }
  10. How the NativeActivity glue layer handles panics and lifecycle

    main

    The android-activity glue layer acts as an IPC shim between the JVM main thread and the Rust application. When running via NativeActivity, the glue layer wraps the application's android_main function in a catch_unwind block. This ensures that if the Rust application panics, the error is caught and logged via log_panic rather than causing an unmanaged crash.

    After android_main returns (or after a panic is handled), the glue layer calls ndk_sys::ANativeActivity_finish(activity) to signal to the JVM that the Activity can be destroyed. This allows for a graceful shutdown of the Android Activity lifecycle from the native side.

  11. Handle input events with InputIteratorInner

    main

    Input events (Key, Motion, Text, and Text Actions) are processed using an iterator pattern. The InputIteratorInner::next method allows you to dispatch events to a callback function.

    When iterating, the system handles:

    1. Buffered Events: Key events and Motion events currently in the input buffer.
    2. Text Input State: Changes to the IME (Input Method Editor) text state.
    3. Editor Actions: Pending text actions (e.g., 'Enter' or 'Done' from a keyboard).

    The iterator ensures that input state changes are processed before checking for editor actions so that actions apply to the most recent state.

    /* 
    Conceptual usage of the input event loop:
    
    while input_iterator.next(|event| {
        match event {
            InputEvent::KeyEvent(key) => { /* handle key */ }
            InputEvent::MotionEvent(motion) => { /* handle motion */ }
            InputEvent::TextEvent(text) => { /* handle text */ }
            InputEvent::TextAction(action) => { /* handle action */ }
        }
        InputStatus::Consumed // or other status
    }) {
        // loop continues
    }
    */
  12. Identify input device sources with the `Source` enum

    main

    The Source enum represents the source of an MotionEvent or KeyEvent. It is an extensible enum that maps to Android SDK integer values. Because it is extensible, you should handle it using a catch-all pattern match to maintain forward compatibility with future Android versions.

    Common variants include:

    • BluetoothStylus
    • Dpad
    • Gamepad / Joystick
    • Keyboard
    • Mouse / MouseRelative
    • Stylus
    • Touchscreen
    • Touchpad

    You can also query the device class using helper methods:

    • is_button_class()
    • is_pointer_class()
    • is_trackball_class()
    • is_position_class()
    • is_joystick_class()
    match source {
        Source::Touchscreen => { /* handle touchscreen */ }
        Source::Gamepad => { /* handle gamepad */ }
        _ => { /* handle unknown or other sources */ }
    }