diff --git a/src/android/host.rs b/src/android/host.rs index 6dae644..f6b9c53 100644 --- a/src/android/host.rs +++ b/src/android/host.rs @@ -25,7 +25,10 @@ //! //! 1. In `Activity.onCreate` — every time, recreation included — call //! [`super::jni::set_host_activity`] from a JNI entry point, then [`start`] (a no-op -//! after the first call). +//! after the first call). `set_host_activity` must come first: it keeps the +//! application's `AssetManager`, which the platform reads the bundled emoji font +//! from when it starts. (If `start` runs first, the font is loaded late, when +//! `set_host_activity` provides the `AssetManager`.) //! 2. From `SurfaceHolder.Callback`: [`surface_created`] in both `surfaceCreated` and //! `surfaceChanged`, [`surface_destroyed`] in `surfaceDestroyed`, and //! [`host_destroyed`] from `onDestroy` when the Activity is finishing. diff --git a/src/android/jni.rs b/src/android/jni.rs index c190e49..03ff1df 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -304,7 +304,19 @@ struct HostActivity { /// the JVM, so `java_vm()` / `activity_as_ptr()` and everything built on them (IME, /// safe areas, file pickers, `rustls-platform-verifier`) work exactly as on the /// `android-activity` path. A no-op when `android-activity` owns the process. +/// Call it before `host::start`: the platform reads bundled assets (the emoji font) +/// when it starts. If `start` came first, the emoji font is loaded here instead. pub fn set_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result<(), String> { + let result = register_host_activity(env, activity); + // The caller may make more JNI calls before returning to Java, which a pending + // exception would make undefined. + if result.is_err() && env.exception_check() { + env.exception_clear(); + } + result +} + +fn register_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result<(), String> { if ANDROID_APP.get().is_some() { return Ok(()); } @@ -317,9 +329,91 @@ pub fn set_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Resu let mut slot = HOST_ACTIVITY.lock().expect("poisoned"); slot.previous = slot.current.take(); slot.current = Some(global); + drop(slot); + if HOST_ASSETS.get().is_none() { + match host_assets(env, activity) { + Ok(assets) => { + let _ = HOST_ASSETS.set(assets); + super::platform::load_deferred_emoji_font(); + } + Err(err) => { + if env.exception_check() { + env.exception_clear(); + } + log::warn!("set_host_activity: no AssetManager: {err}"); + } + } + } Ok(()) } +/// The application's `AssetManager`, which `android-activity` provides on the +/// other path. Taken once, from the first host Activity's application context, and +/// kept for the life of the process. +static HOST_ASSETS: OnceLock = OnceLock::new(); + +struct HostAssets { + /// Keeps the Java `AssetManager`, and so the native one, alive. + _java: jni::refs::Global>, + native: ndk::asset::AssetManager, +} + +fn host_assets(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result { + // A local frame frees `context` and `assets` even when the caller stays in + // native code, e.g. on a native-attached thread. + env.with_local_frame(2, |env| { + Ok::<_, jni::errors::Error>(host_assets_in_frame(env, activity)) + }) + .e()? +} + +fn host_assets_in_frame( + env: &mut jni::Env<'_>, + activity: &JObject<'_>, +) -> Result { + let context = env + .call_method( + activity, + jni::jni_str!("getApplicationContext"), + jni::jni_sig!("()Landroid/content/Context;"), + &[], + ) + .and_then(|value| value.l()) + .e()?; + let assets = env + .call_method( + &context, + jni::jni_str!("getAssets"), + jni::jni_sig!("()Landroid/content/res/AssetManager;"), + &[], + ) + .and_then(|value| value.l()) + .e()?; + if assets.is_null() { + return Err("getAssets returned null".into()); + } + let java = env.new_global_ref(&assets).e()?; + // SAFETY: `env` is the current thread's JNI env and `assets` a live AssetManager. + let native = + unsafe { ndk_sys::AAssetManager_fromJava(env.get_raw() as _, assets.as_raw() as _) }; + let native = std::ptr::NonNull::new(native).ok_or("AAssetManager_fromJava returned null")?; + Ok(HostAssets { + _java: java, + // SAFETY: `native` belongs to the Java AssetManager that `_java` keeps alive + // for as long as this value exists, which is the life of the process. + native: unsafe { ndk::asset::AssetManager::from_ptr(native) }, + }) +} + +/// Run `f` with the app's `AssetManager`: from `android-activity`, or from the +/// Activity passed to [`set_host_activity`] on the host-driven path. +pub(crate) fn with_asset_manager(f: impl FnOnce(&ndk::asset::AssetManager) -> T) -> Option { + if let Some(app) = android_app() { + return Some(f(&app.asset_manager())); + } + HOST_ASSETS.get().map(|assets| f(&assets.native)) +} + /// Public accessor for the JavaVM pointer. /// /// Uses `AndroidApp::vm_as_ptr()` from the stored `AndroidApp`. diff --git a/src/android/platform.rs b/src/android/platform.rs index e9df00f..eaafa6e 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -250,10 +250,12 @@ fn font_has_cbdt_tables(data: &[u8]) -> bool { /// /// `path` is relative to the `assets/` directory in the APK (e.g. /// `"fonts/NotoColorEmoji.ttf"`). -fn load_asset_bytes(app: &android_activity::AndroidApp, path: &str) -> anyhow::Result> { +fn load_asset_bytes( + asset_manager: &ndk::asset::AssetManager, + path: &str, +) -> anyhow::Result> { use std::ffi::CString; - let asset_manager = app.asset_manager(); let c_path = CString::new(path)?; // Open the asset. `AssetManager::open` returns `Option`. @@ -261,13 +263,45 @@ fn load_asset_bytes(app: &android_activity::AndroidApp, path: &str) -> anyhow::R .open(&c_path) .ok_or_else(|| anyhow::anyhow!("asset not found: {path}"))?; - // `Asset::buffer` returns `io::Result<&[u8]>` — the entire file mapped - // into memory. - let bytes = asset - .buffer() - .map_err(|e| anyhow::anyhow!("failed to read asset buffer for {path}: {e}"))?; + // Read straight into one buffer of the asset's size: the emoji font is + // ~10 MB, and mapping it first and copying would hold it twice. + let mut bytes = Vec::with_capacity(asset.length()); + std::io::Read::read_to_end(&mut asset, &mut bytes) + .map_err(|e| anyhow::anyhow!("failed to read asset {path}: {e}"))?; - Ok(bytes.to_vec()) + Ok(bytes) +} + +/// The bundled CBDT emoji font, relative to the APK's `assets/` directory. +const BUNDLED_EMOJI_FONT: &str = "fonts/NotoColorEmoji.ttf"; + +/// The text system still waiting for the bundled emoji font. On the host-driven +/// path the platform can start before `set_host_activity` has provided an +/// AssetManager; the font is then loaded when it does. +static EMOJI_FONT_PENDING: std::sync::Mutex>> = + std::sync::Mutex::new(None); + +/// Load the bundled emoji font into a text system that started without an +/// AssetManager. Called by `set_host_activity` once it has one. +pub(crate) fn load_deferred_emoji_font() { + let Some(text_system) = EMOJI_FONT_PENDING.lock().expect("poisoned").take() else { + return; + }; + match crate::android::jni::with_asset_manager(|assets| { + load_asset_bytes(assets, BUNDLED_EMOJI_FONT) + }) { + Some(Ok(bytes)) => { + log::info!( + "loaded bundled CBDT emoji font from assets ({} bytes)", + bytes.len() + ); + if let Err(e) = text_system.add_fonts(vec![std::borrow::Cow::Owned(bytes)]) { + log::warn!("failed to add bundled emoji font: {e:#}"); + } + } + Some(Err(e)) => log::warn!("failed to load bundled emoji font from assets: {e:#}"), + None => log::warn!("no AssetManager available — cannot load bundled emoji font"), + } } impl AndroidPlatform { @@ -374,10 +408,17 @@ impl AndroidPlatform { } } + let mut emoji_deferred = false; if !emoji_loaded { // Try loading the bundled CBDT NotoColorEmoji from APK assets. - if let Some(app) = crate::android::jni::android_app() { - match load_asset_bytes(&app, "fonts/NotoColorEmoji.ttf") { + // The lock is held across the check, so a concurrent + // `set_host_activity` either provides the AssetManager first or + // finds the text system pending afterwards. + let mut pending = EMOJI_FONT_PENDING.lock().expect("poisoned"); + if let Some(result) = crate::android::jni::with_asset_manager(|assets| { + load_asset_bytes(assets, BUNDLED_EMOJI_FONT) + }) { + match result { Ok(bytes) => { log::info!( "loaded bundled CBDT emoji font from assets ({} bytes)", @@ -391,11 +432,16 @@ impl AndroidPlatform { } } } else { - log::debug!("no AndroidApp available — cannot load bundled emoji font"); + log::warn!( + "no AssetManager yet — the bundled emoji font loads once \ + set_host_activity provides one" + ); + *pending = Some(Arc::clone(&text_system)); + emoji_deferred = true; } } - if !emoji_loaded { + if !emoji_loaded && !emoji_deferred { log::warn!("no compatible emoji font loaded — emoji glyphs may not render"); }