diff --git a/src/android/host.rs b/src/android/host.rs index 081b038..4192dbf 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 98501a3..17c84a5 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -498,8 +498,19 @@ fn is_gone(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> bool { /// with several alive, the most recently registered one is used. A no-op when /// `android-activity` owns the process. /// Call it on the UI thread (as `onCreate` is), and before `host::start`: the -/// platform reads bundled assets (the emoji font) when it starts. +/// 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(()); } @@ -533,6 +544,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) => log::warn!("set_host_activity: no AssetManager: {err}"), } @@ -576,6 +588,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 c1febcb..6f29f28 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -262,13 +262,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 { @@ -375,10 +407,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) => { @@ -394,11 +431,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"); }