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
5 changes: 4 additions & 1 deletion src/android/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 25 additions & 1 deletion src/android/jni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}
Expand Down Expand Up @@ -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}"),
}
Expand Down Expand Up @@ -576,6 +588,18 @@ struct HostAssets {
}

fn host_assets(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result<HostAssets, String> {
// 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<HostAssets, String> {
let context = env
.call_method(
activity,
Expand Down
60 changes: 51 additions & 9 deletions src/android/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Arc<CosmicTextSystem>>> =
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)]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add deferred fonts through GPUI's text system

When host::start wins the race far enough for a surface to render text before set_host_activity runs, this adds the font directly to the platform CosmicTextSystem. That bypasses GPUI's TextSystem::add_fonts, which invalidates cached font resolution and line layouts, and this UI-thread path also requests no redraw. Consequently, already rendered emoji can remain cached as missing glyphs even though the log reports that the deferred font loaded; route the late installation through the app text system on the render thread and invalidate/redraw the windows.

Useful? React with 👍 / 👎.

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 {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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");
}

Expand Down