Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,33 @@ Cargo.toml
[dependencies]
log = "0.4"
android_logger = "0.13"
android-activity = { version = "0.5", features = [ "native-activity" ] }
android-activity = { version = "0.6", features = [ "native-activity" ] }

[lib]
crate_type = ["cdylib"]
crate-type = ["cdylib"]
```

_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_
_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

```rust
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};

#[no_mangle]
#[unsafe(no_mangle)]
fn android_main(app: AndroidApp) {
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));

// `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.
//
// Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state
// multiple times.
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)) /* timeout */, |event| {
Expand All @@ -62,6 +73,11 @@ fn android_main(app: AndroidApp) {
PollEvent::Main(main_event) => {
log::info!("Main event: {:?}", main_event);
match main_event {
// Once you receive a `Destroy` event, your `AndroidApp` will no longer
// 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`.
MainEvent::Destroy => { return; }
_ => {}
}
Expand All @@ -85,6 +101,36 @@ cargo apk run
adb logcat example:V *:S
```

## 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.

For example:

```rust
use std::sync::OnceLock;
use jni::{JavaVM, objects::JObject};

#[unsafe(no_mangle)]
fn android_on_create(state: &android_activity::OnCreateState) {

// `android_on_create` 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.
//
// Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state
// multiple times.
static APP_ONCE: OnceLock<()> = OnceLock::new();
APP_ONCE.get_or_init(|| {
// Initialize logging...
});
let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) };
let activity = state.activity_as_ptr() as jni::sys::jobject;
// Do some other setup work on the Java main thread before `android_main` starts running
}
```

## Full Examples

See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`).
Expand All @@ -111,7 +157,7 @@ Even if you start out using `NativeActivity` for the convenience, it's likely th

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).

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 `#[no_mangle]fn android_main(app: AndroidApp)` entry point.
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.

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).

Expand All @@ -126,7 +172,7 @@ Middleware libraries can instead look at using the [ndk-context](https://crates.
The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be:

1. Remove `ndk-glue` from your Cargo.toml
2. Add a dependency on `android-activity`, like `android-activity = { version="0.5", features = [ "native-activity" ] }`
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:

Expand Down
36 changes: 36 additions & 0 deletions android-activity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `ndk` and `ndk-sys` crates are now re-exported under `android_activity::ndk` and `android_activity::ndk_sys` ([#194](https://github.com/rust-mobile/android-activity/pull/194))
- `AndroidApp::java_main_looper()` gives access to the `ALooper` for the Java main / UI thread ([#198](https://github.com/rust-mobile/android-activity/pull/198))
- `AndroidApp::run_on_java_main_thread()` can be used to run boxed closures on the Java main / UI thread ([#232](https://github.com/rust-mobile/android-activity/pull/232))
- Support for 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 / UI thread before the `android_main` Rust code starts running.

For example:

```rust
use std::sync::OnceLock;
use android_activity::OnCreateState;
use jni::{JavaVM, refs::Global, objects::JObject};

#[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 some
// logger crates will panic if initialized 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:?}");
}
}
```

### Changed

Expand Down
19 changes: 13 additions & 6 deletions android-activity/src/game_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;

use crate::error::InternalResult;
use crate::init::{init_android_main_thread, init_java_main_thread_on_create};
use crate::main_callbacks::MainCallbacks;
use crate::util::{
abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic,
try_get_path_from_ptr,
};
use crate::util::{abort_on_panic, log_panic, try_get_path_from_ptr};
use crate::{
AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect,
WindowManagerFlags,
Expand Down Expand Up @@ -1149,6 +1147,17 @@ pub unsafe extern "C" fn GameActivity_onCreate(
saved_state: *mut ::std::os::raw::c_void,
saved_state_size: libc::size_t,
) {
abort_on_panic(|| unsafe {
let vm = jni::JavaVM::from_raw((*activity).vm as *mut _);
let java_activity = (*activity).javaGameActivity;
let saved_state = if !saved_state.is_null() && saved_state_size > 0 {
std::slice::from_raw_parts(saved_state.cast(), saved_state_size)
} else {
&[]
};
init_java_main_thread_on_create(vm, java_activity as *mut c_void, saved_state);
});

GameActivity_onCreate_C(activity, saved_state, saved_state_size);
}

Expand All @@ -1162,8 +1171,6 @@ extern "Rust" {
#[no_mangle]
pub unsafe extern "C" fn _rust_glue_entry(game_activity_glue: *mut ffi::android_app) {
abort_on_panic(|| {
let _join_log_forwarder = forward_stdio_to_logcat();

let (jvm, jni_activity) = unsafe {
let jvm = (*(*game_activity_glue).activity).vm;
let activity: jobject = (*(*game_activity_glue).activity).javaGameActivity;
Expand Down
Loading
Loading