From 773e5ade2353399a3bac9f848c8dfd4b4c9a1e19 Mon Sep 17 00:00:00 2001 From: lorenzomicheli Date: Mon, 12 Oct 2026 07:54:56 +0000 Subject: [PATCH] android: Load the emoji font late if start comes first, free JNI refs Review follow-up for the host-path emoji font: - If host::start runs before set_host_activity, the platform finds no AssetManager. It now leaves its text system pending and logs a warning, and set_host_activity loads the font once it has the AssetManager. The host.rs docs say set_host_activity comes first. - host_assets runs in a JNI local frame, so the context and AssetManager local references are freed even when the caller stays in native code. - set_host_activity clears a pending exception on every error path. - load_asset_bytes reads the asset into one Vec of its size instead of copying the mapped buffer, so the ~10 MB font is not held twice. Co-Authored-By: Claude Opus 5.5 --- src/android/host.rs | 5 +++- src/android/jni.rs | 25 ++++++++++++++++- src/android/platform.rs | 60 ++++++++++++++++++++++++++++++++++------- 3 files changed, 79 insertions(+), 11 deletions(-) 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 60d3ed7..03ff1df 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -305,8 +305,18 @@ struct HostActivity { /// 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. +/// 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(()); } @@ -324,6 +334,7 @@ pub fn set_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Resu match host_assets(env, activity) { Ok(assets) => { let _ = HOST_ASSETS.set(assets); + super::platform::load_deferred_emoji_font(); } Err(err) => { if env.exception_check() { @@ -348,6 +359,18 @@ struct HostAssets { } 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, diff --git a/src/android/platform.rs b/src/android/platform.rs index 49daeab..eaafa6e 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -263,13 +263,45 @@ fn load_asset_bytes( .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 { @@ -376,10 +408,15 @@ impl AndroidPlatform { } } + let mut emoji_deferred = false; if !emoji_loaded { // Try loading the bundled CBDT NotoColorEmoji from APK assets. + // 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, "fonts/NotoColorEmoji.ttf") + load_asset_bytes(assets, BUNDLED_EMOJI_FONT) }) { match result { Ok(bytes) => { @@ -395,11 +432,16 @@ impl AndroidPlatform { } } } else { - log::debug!("no AssetManager 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"); }