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
94 changes: 94 additions & 0 deletions src/android/jni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}
Expand All @@ -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<HostAssets> = OnceLock::new();

struct HostAssets {
/// Keeps the Java `AssetManager`, and so the native one, alive.
_java: jni::refs::Global<JObject<'static>>,
native: ndk::asset::AssetManager,
}

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,
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<T>(f: impl FnOnce(&ndk::asset::AssetManager) -> T) -> Option<T> {
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`.
Expand Down
70 changes: 58 additions & 12 deletions src/android/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,24 +250,58 @@ 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<Vec<u8>> {
fn load_asset_bytes(
asset_manager: &ndk::asset::AssetManager,
path: &str,
) -> anyhow::Result<Vec<u8>> {
use std::ffi::CString;

let asset_manager = app.asset_manager();
let c_path = CString::new(path)?;

// Open the asset. `AssetManager::open` returns `Option<Asset>`.
let mut asset = asset_manager
.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)]) {
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 @@ -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)",
Expand All @@ -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");
}

Expand Down
Loading