cranpose 0.9.3

Cranpose runtime and UI facade
Documentation
use std::{
    collections::BTreeMap,
    sync::{
        LazyLock, Mutex,
        atomic::{AtomicI64, Ordering},
    },
};

use android_activity::{AndroidApp, AndroidAppWaker};
use cranpose_ui::Rect;
use jni::{
    Env, jni_sig, jni_str,
    objects::{JClass, JString},
    sys::jlong,
};

use crate::{
    native_view::NativeViewHost,
    webview_host::{BrowserView, Events, WebViews},
};

struct Route {
    id: u64,
    events: Events,
    wake: AndroidAppWaker,
}
static ROUTES: LazyLock<Mutex<BTreeMap<i64, Route>>> = LazyLock::new(Mutex::default);
static NEXT_ROUTE: AtomicI64 = AtomicI64::new(1);

#[expect(
    deprecated,
    reason = "jni 0.22.4 generates AtomicBool::fetch_update for its ABI check"
)]
const _: jni::NativeMethod = jni::native_method! {
    java_type = "dev.cranpose.android.CranposeWebViews",
    static extern fn native_event(route: jlong, event: JString),
};

fn native_event<'local>(
    env: &mut Env<'local>,
    _class: JClass<'local>,
    route: jlong,
    event: JString<'local>,
) -> jni::errors::Result<()> {
    let event = event.try_to_string(env)?;
    if let Some(route) = ROUTES
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .get(&route)
    {
        let _ = route.events.send((route.id, event));
        route.wake.wake();
    }
    Ok(())
}

pub(crate) struct AndroidWebView {
    app: AndroidApp,
    route: i64,
}

impl AndroidWebView {
    fn update(&self, url: Option<&str>, bounds: Option<Rect>) -> Result<(), String> {
        crate::android_jni::with_android_activity_env(&self.app, |env, activity| {
            let url = url
                .map(|url| env.new_string(url))
                .transpose()
                .map_err(|error| error.to_string())?;
            let null = jni::objects::JObject::null();
            let text = url
                .as_ref()
                .map_or(jni::objects::JValue::Object(&null), Into::into);
            let rect = bounds.unwrap_or(Rect {
                x: 0.0,
                y: 0.0,
                width: 0.0,
                height: 0.0,
            });
            env.call_method(
                &activity,
                jni_str!("cranposeUpdateWebView"),
                jni_sig!("(JLjava/lang/String;ZFFFF)V"),
                &[
                    self.route.into(),
                    text,
                    bounds.is_some().into(),
                    rect.x.into(),
                    rect.y.into(),
                    rect.width.into(),
                    rect.height.into(),
                ],
            )
            .map_err(|error| {
                crate::android_jni::clear_pending_android_jni_exception(env);
                error.to_string()
            })?;
            Ok(())
        })
    }
}

impl BrowserView for AndroidWebView {
    fn navigate(&mut self, url: &str) -> Result<(), String> {
        self.update(Some(url), None)
    }
    fn place(&mut self, bounds: Rect) -> Result<(), String> {
        self.update(None, Some(bounds))
    }
}

impl Drop for AndroidWebView {
    fn drop(&mut self) {
        ROUTES
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .remove(&self.route);
        let result = crate::android_jni::with_android_activity_env(&self.app, |env, activity| {
            env.call_method(
                &activity,
                jni_str!("cranposeRemoveWebView"),
                jni_sig!("(J)V"),
                &[self.route.into()],
            )
            .map_err(|error| {
                crate::android_jni::clear_pending_android_jni_exception(env);
                error.to_string()
            })?;
            Ok(())
        });
        if let Err(error) = result {
            log::warn!("could not remove Android WebView: {error}");
        }
    }
}

pub(crate) fn sync<R: cranpose_render_common::Renderer>(
    views: &mut WebViews<AndroidWebView>,
    host: &NativeViewHost,
    shell: &mut cranpose_app_shell::AppShell<R>,
    app: &AndroidApp,
) where
    R::Error: std::fmt::Debug,
{
    if host.is_empty() && views.is_empty() {
        return;
    }
    views.sync(host, shell.layout_tree(), |id, url, bounds, events| {
        let route = NEXT_ROUTE.fetch_add(1, Ordering::Relaxed);
        ROUTES
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(
                route,
                Route {
                    id,
                    events: events.clone(),
                    wake: app.create_waker(),
                },
            );
        let view = AndroidWebView {
            app: app.clone(),
            route,
        };
        view.update(Some(url), Some(bounds))?;
        Ok(view)
    });
}