cranpose 0.1.174

Cranpose runtime and UI facade
Documentation
#![expect(unsafe_code)]

use std::sync::Arc;

use cranpose_services::{
    AppUpdateCapabilities, AppUpdateError, AppUpdateStatus, AppUpdater, GitHubReleaseUpdate,
    PackageDigest, UpdatePackage, set_platform_app_updater,
};
use jni::{
    Env, EnvUnowned, Outcome, jni_sig, jni_str,
    objects::{JClass, JObject, JString, JValue},
    sys::{jint, jlong},
};

use crate::{
    android_jni::{
        clear_pending_android_jni_exception, load_cranpose_java_class, with_android_activity_env,
    },
    android_services::wake_native_loop,
};

const UPDATE_CLASS: &str = "dev/cranpose/android/CranposeAppUpdate";

pub(crate) fn register(app: android_activity::AndroidApp) {
    set_platform_app_updater(Arc::new(AndroidAppUpdater { app }));
}

struct AndroidAppUpdater {
    app: android_activity::AndroidApp,
}

impl AndroidAppUpdater {
    fn call(
        &self,
        run: impl for<'local> FnOnce(
            &mut Env<'local>,
            &JObject<'local>,
            JClass<'local>,
        ) -> jni::errors::Result<()>,
    ) -> Result<(), AppUpdateError> {
        with_android_activity_env(&self.app, |env, activity| {
            let class = load_cranpose_java_class(env, &activity, UPDATE_CLASS)?;
            run(env, &activity, class).map_err(|error| {
                clear_pending_android_jni_exception(env);
                error.to_string()
            })
        })
        .map_err(AppUpdateError::Request)
    }
}

impl AppUpdater for AndroidAppUpdater {
    fn capabilities(&self) -> AppUpdateCapabilities {
        AppUpdateCapabilities {
            check: true,
            install: true,
        }
    }

    fn check(&self, source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
        self.call(|env, activity, class| {
            let repository = env.new_string(&source.repository)?;
            let current_version = env.new_string(&source.current_version)?;
            let asset_suffix = env.new_string(&source.asset_suffix)?;
            env.call_static_method(
                class,
                jni_str!("cranposeCheckGitHubUpdate"),
                jni_sig!(
                    "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"
                ),
                &[
                    JValue::Object(activity),
                    JValue::Object(repository.as_ref()),
                    JValue::Object(current_version.as_ref()),
                    JValue::Object(asset_suffix.as_ref()),
                ],
            )
            .map(|_| ())
        })
    }

    fn install(&self, package: &UpdatePackage) -> Result<(), AppUpdateError> {
        let digest = package
            .digest
            .as_ref()
            .map(PackageDigest::to_feed_string)
            .unwrap_or_default();
        self.call(|env, activity, class| {
            let download_url = env.new_string(&package.download_url)?;
            let digest = env.new_string(&digest)?;
            env.call_static_method(
                class,
                jni_str!("cranposeInstallUpdate"),
                jni_sig!("(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;J)V"),
                &[
                    JValue::Object(activity),
                    JValue::Object(download_url.as_ref()),
                    JValue::Object(digest.as_ref()),
                    JValue::Long(package.size.unwrap_or(0) as i64),
                ],
            )
            .map(|_| ())
        })
    }
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeAppUpdate_nativeOnAppUpdateStatus<
    'local,
>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    kind: jint,
    version: JString<'local>,
    download_url: JString<'local>,
    downloaded: jlong,
    total: jlong,
    message: JString<'local>,
    digest: JString<'local>,
) {
    let decoded = env.with_env(
        |env| -> jni::errors::Result<(String, String, String, String)> {
            Ok((
                version.try_to_string(env)?,
                download_url.try_to_string(env)?,
                message.try_to_string(env)?,
                digest.try_to_string(env)?,
            ))
        },
    );
    let Outcome::Ok((version, download_url, message, digest)) = decoded.into_outcome() else {
        return;
    };
    let status = match kind {
        1 => AppUpdateStatus::Checking,
        2 => AppUpdateStatus::UpToDate,
        3 => {
            let mut package = UpdatePackage::new(version, download_url);
            if total > 0 {
                package = package.with_size(total as u64);
            }
            if let Some(digest) = PackageDigest::parse(&digest) {
                package = package.with_digest(digest);
            }
            AppUpdateStatus::Available { package }
        }
        4 => AppUpdateStatus::Downloading {
            downloaded: downloaded.max(0) as u64,
            total: (total > 0).then_some(total as u64),
        },
        5 => AppUpdateStatus::AwaitingConfirmation,
        6 => AppUpdateStatus::Installing,
        7 => AppUpdateStatus::Error(if message.is_empty() {
            "application update failed".to_string()
        } else {
            message
        }),
        8 => AppUpdateStatus::Verifying,
        _ => AppUpdateStatus::Idle,
    };
    cranpose_services::set_app_update_status(status);
    wake_native_loop();
}