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
8 changes: 6 additions & 2 deletions android-activity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- input: TextInputAction enum representing action button types on soft keyboards.
- input: InputEvent::TextAction event for handling action button presses from soft keyboards.
- input: `TextInputAction` enum representing action button types on soft keyboards. ([#216](https://github.com/rust-mobile/android-activity/pull/216))
- input: `InputEvent::TextAction` event for handling action button presses from soft keyboards. ([#216](https://github.com/rust-mobile/android-activity/pull/216))
- 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))

### Fixed

- *Safety* `AndroidApp::asset_manager()` returns an `AssetManager` that has a safe `'static` lifetime that's not invalidated when `android_main()` returns [#233](https://github.com/rust-mobile/android-activity/pull/233)

### Changed
- rust-version bumped to 1.85.0 ([#193](https://github.com/rust-mobile/android-activity/pull/193), [#219](https://github.com/rust-mobile/android-activity/pull/219))
- GameActivity updated to 4.0.0 (requires the corresponding 4.0.0 `.aar` release from Google) ([#191](https://github.com/rust-mobile/android-activity/pull/191))
Expand Down
40 changes: 26 additions & 14 deletions android-activity/src/game_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ impl AndroidApp {
pub(crate) fn new(
ptr: NonNull<ffi::android_app>,
jvm: jni::JavaVM,
app_asset_manager: AssetManager,
main_looper: ndk::looper::ForeignLooper,
main_callbacks: MainCallbacks,
) -> Self {
Expand All @@ -137,6 +138,7 @@ impl AndroidApp {
main_callbacks,
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
app_asset_manager,
})),
})
})
Expand Down Expand Up @@ -303,6 +305,14 @@ pub struct AndroidAppInner {
/// InputReceiver reference which we track to ensure
/// we don't hand out more than one receiver at a time
input_receiver: Mutex<Option<Weak<InputReceiver>>>,

/// An `AAssetManager` wrapper for the `Application` `AssetManager`
/// Note: `AAssetManager_fromJava` specifies that the pointer is only valid
/// while we hold a global reference to the `AssetManager` Java object
/// to ensure it is not garbage collected. This AssetManager comes from
/// a OnceLock initialization that leaks a single global JNI reference
/// to guarantee that it remains valid for the lifetime of the process.
app_asset_manager: AssetManager,
}

impl AndroidAppInner {
Expand Down Expand Up @@ -652,11 +662,11 @@ impl AndroidAppInner {
}

pub fn asset_manager(&self) -> AssetManager {
unsafe {
let app_ptr = self.native_app.as_ptr();
let am_ptr = NonNull::new_unchecked((*(*app_ptr).activity).assetManager);
AssetManager::from_ptr(am_ptr)
}
// Safety: While constructing the AndroidApp we do a OnceLock initialization
// where we get the Application AssetManager and leak a single global JNI
// reference that guarantees it will not be garbage collected, so we can
// safely return the corresponding AAssetManager here.
unsafe { AssetManager::from_ptr(self.app_asset_manager.ptr()) }
}

pub(crate) fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
Expand Down Expand Up @@ -1023,20 +1033,22 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
// SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance
let jni_activity = unsafe { env.as_cast_raw::<Global<JObject>>(&jni_activity)? };

let main_callbacks = match init_android_main_thread(&jvm, &jni_activity, &main_looper) {
Ok(main_callbacks) => main_callbacks,
Err(err) => {
eprintln!(
"Failed to name Java thread and set thread context class loader: {err}"
);
return Err(err);
}
};
let (app_asset_manager, main_callbacks) =
match init_android_main_thread(&jvm, &jni_activity, &main_looper) {
Ok((asset_manager, main_callbacks)) => (asset_manager, main_callbacks),
Err(err) => {
eprintln!(
"Failed to name Java thread and set thread context class loader: {err}"
);
return Err(err);
}
};

unsafe {
let app = AndroidApp::new(
NonNull::new(native_app).unwrap(),
jvm.clone(),
app_asset_manager,
main_looper,
main_callbacks,
);
Expand Down
23 changes: 21 additions & 2 deletions android-activity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,28 @@ impl AndroidApp {
self.inner.read().unwrap().content_rect()
}

/// Queries the Asset Manager instance for the application.
/// Returns the `AssetManager` for the application's `Application` context.
///
/// Use this to access binary assets bundled inside your application's .apk file.
/// Use this to access raw files bundled in the application's .apk file.
///
/// This is an `Application`-scoped asset manager, not an `Activity`-scoped
/// one. In normal usage those behave the same for packaged assets, so this
/// is usually the correct API to use.
///
/// In uncommon cases, an `Activity` may have a context-specific
/// asset/resource view that differs from the `Application` context. If you
/// specifically need the current `Activity`'s `AssetManager`, obtain the
/// `Activity` via [`AndroidApp::activity_as_ptr`] and call `getAssets()`
/// through JNI.
///
/// The returned `AssetManager` has a `'static` lifetime and remains valid
/// across `Activity` recreation, including when `android_main()` is
/// re-entered.
///
/// **Beware**: If you consider accessing the `Activity` context's
/// `AssetManager` through JNI you must keep the `AssetManager` alive via a
/// global reference before accessing the ndk `AAssetManager` and
/// `ndk::asset::AssetManager` does not currently handle this for you.
pub fn asset_manager(&self) -> AssetManager {
self.inner.read().unwrap().asset_manager()
}
Expand Down
13 changes: 9 additions & 4 deletions android-activity/src/native_activity/glue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -898,9 +898,9 @@ fn rust_glue_entry(
// SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance
let jni_activity = unsafe { env.as_cast_raw::<Global<JObject>>(&jni_activity)? };

let main_callbacks =
let (app_asset_manager, main_callbacks) =
match init_android_main_thread(&jvm, &jni_activity, &main_looper) {
Ok(callbacks) => callbacks,
Ok((asset_manager, callbacks)) => (asset_manager, callbacks),
Err(err) => {
eprintln!(
"Failed to name Java thread and set thread context class loader: {err}"
Expand All @@ -909,8 +909,13 @@ fn rust_glue_entry(
}
};

let app =
AndroidApp::new(rust_glue.clone(), jvm.clone(), main_looper, main_callbacks);
let app = AndroidApp::new(
rust_glue.clone(),
jvm.clone(),
app_asset_manager,
main_looper,
main_callbacks,
);

rust_glue.notify_main_thread_running();

Expand Down
21 changes: 15 additions & 6 deletions android-activity/src/native_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::marker::PhantomData;
use std::panic::AssertUnwindSafe;
use std::ptr;
use std::ptr::NonNull;
use std::sync::{Arc, Mutex, RwLock, Weak};
use std::time::Duration;

Expand Down Expand Up @@ -66,6 +65,7 @@ impl AndroidApp {
pub(crate) fn new(
native_activity: NativeActivityGlue,
jvm: JavaVM,
app_asset_manager: AssetManager,
main_looper: ndk::looper::ForeignLooper,
main_callbacks: MainCallbacks,
) -> Self {
Expand All @@ -89,6 +89,7 @@ impl AndroidApp {
main_callbacks,
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
app_asset_manager,
})),
};

Expand Down Expand Up @@ -139,6 +140,14 @@ pub(crate) struct AndroidAppInner {
/// InputReceiver reference which we track to ensure
/// we don't hand out more than one receiver at a time
input_receiver: Mutex<Option<Weak<InputReceiver>>>,

/// An `AAssetManager` wrapper for the `Application` `AssetManager`
/// Note: `AAssetManager_fromJava` specifies that the pointer is only valid
/// while we hold a global reference to the `AssetManager` Java object
/// to ensure it is not garbage collected. This AssetManager comes from
/// a OnceLock initialization that leaks a single global JNI reference
/// to guarantee that it remains valid for the lifetime of the process.
app_asset_manager: AssetManager,
}

impl AndroidAppInner {
Expand Down Expand Up @@ -315,11 +324,11 @@ impl AndroidAppInner {
}

pub fn asset_manager(&self) -> AssetManager {
unsafe {
let activity_ptr = self.native_activity.activity;
let am_ptr = NonNull::new_unchecked((*activity_ptr).assetManager);
AssetManager::from_ptr(am_ptr)
}
// Safety: While constructing the AndroidApp we do a OnceLock initialization
// where we get the Application AssetManager and leak a single global JNI
// reference that guarantees it will not be garbage collected, so we can
// safely return the corresponding AAssetManager here.
unsafe { AssetManager::from_ptr(self.app_asset_manager.ptr()) }
}

pub fn set_window_flags(
Expand Down
46 changes: 43 additions & 3 deletions android-activity/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use jni::{
vm::JavaVM,
};
use log::{error, Level};
use ndk::asset::AssetManager;
use std::{
ffi::{CStr, CString},
fs::File,
Expand Down Expand Up @@ -122,6 +123,7 @@ pub(crate) fn abort_on_panic<R>(f: impl FnOnce() -> R) -> R {

struct AppState {
main_callbacks: MainCallbacks,
app_asset_manager: AssetManager,
}

static APP_ONCE: OnceLock<AppState> = OnceLock::new();
Expand All @@ -142,6 +144,21 @@ pub(crate) fn get_application<'local, 'any>(
Ok(app)
}

pub(crate) fn get_assets<'local, 'any>(
env: &mut jni::Env<'local>,
application: &JObject<'any>,
) -> jni::errors::Result<JObject<'local>> {
let assets_manager = env
.call_method(
application,
jni_str!("getAssets"),
jni_sig!(() -> android.content.res.AssetManager),
&[],
)?
.l()?;
Ok(assets_manager)
}

fn try_init_current_thread(env: &mut jni::Env, activity: &JObject) -> jni::errors::Result<()> {
let activity_class = env.get_object_class(activity)?;
let class_loader = activity_class.get_class_loader(env)?;
Expand All @@ -166,29 +183,52 @@ pub(crate) fn init_android_main_thread(
vm: &JavaVM,
jni_activity: &JObject,
java_main_looper: &ndk::looper::ForeignLooper,
) -> jni::errors::Result<MainCallbacks> {
) -> jni::errors::Result<(AssetManager, MainCallbacks)> {
vm.with_local_frame(10, |env| -> jni::errors::Result<_> {
let app_state = APP_ONCE.get_or_init(|| unsafe {
let application =
get_application(env, jni_activity).expect("Failed to get Application instance");
let app_asset_manager =
get_assets(env, &application).expect("Failed to get AssetManager");
let app_global = env
.new_global_ref(application)
.expect("Failed to create global ref for Application");
// Make sure we don't delete the global reference via Drop
let app_global = app_global.into_raw();
ndk_context::initialize_android_context(vm.get_raw().cast(), app_global.cast());

let asset_manager_global = env
.new_global_ref(app_asset_manager)
.expect("Failed to create global ref for AssetManager");
// Make sure we don't delete the global reference via Drop because
// the AAssetManager pointer will only be valid while we can
// guarantee that the Java AssetManager is not garbage collected
let asset_manager_global = asset_manager_global.into_raw();
let asset_manager_ptr =
ndk_sys::AAssetManager_fromJava(env.get_raw() as _, asset_manager_global as _);
assert_ne!(
asset_manager_ptr,
std::ptr::null_mut(),
"Failed to get Application AAssetManager"
);
let app_asset_manager =
AssetManager::from_ptr(std::ptr::NonNull::new(asset_manager_ptr).unwrap());

let main_callbacks = MainCallbacks::new(java_main_looper);

AppState { main_callbacks }
AppState {
main_callbacks,
app_asset_manager,
}
});

if let Err(err) = try_init_current_thread(env, jni_activity) {
eprintln!("Failed to initialize Java thread state: {:?}", err);
}

let asset_manager = unsafe { AssetManager::from_ptr(app_state.app_asset_manager.ptr()) };
let main_callbacks = app_state.main_callbacks.clone();

Ok(main_callbacks)
Ok((asset_manager, main_callbacks))
})
}
Loading