cranpose 0.1.149

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

use std::sync::Arc;

use cranpose_services::{
    Camera, CameraError, CameraFrame, CameraLens, CameraLenses, CameraState, CameraStill,
    FlashMode, FrameFormat, LensFacing, publish_camera_frame, publish_camera_lenses,
    publish_camera_state, publish_camera_still, record_dropped_camera_frame, set_platform_camera,
};
use jni::{
    EnvUnowned, Outcome, jni_sig, jni_str,
    objects::{JByteArray, JClass, JObject, JString, JValue},
    sys::{jint, jlong},
};

use crate::android_jni::{clear_pending_android_jni_exception, with_android_activity_env};

pub(crate) fn register(app: android_activity::AndroidApp) {
    set_platform_camera(Arc::new(AndroidCamera { app }));
}

struct AndroidCamera {
    app: android_activity::AndroidApp,
}

impl AndroidCamera {
    fn call_void(&self, name: &'static jni::strings::JNIStr) -> Result<(), String> {
        with_android_activity_env(&self.app, |env, activity| {
            env.call_method(&activity, name, jni_sig!("()V"), &[])
                .map(|_| ())
                .map_err(|error| {
                    clear_pending_android_jni_exception(env);
                    error.to_string()
                })
        })
    }

    fn call_bool(&self, name: &'static jni::strings::JNIStr) -> bool {
        with_android_activity_env(&self.app, |env, activity| {
            env.call_method(&activity, name, jni_sig!("()Z"), &[])
                .and_then(|value| value.z())
                .map_err(|error| {
                    clear_pending_android_jni_exception(env);
                    error.to_string()
                })
        })
        .unwrap_or(false)
    }

    fn call_string(&self, name: &'static jni::strings::JNIStr) -> Option<String> {
        with_android_activity_env(&self.app, |env, activity| {
            let value = env
                .call_method(&activity, name, jni_sig!("()Ljava/lang/String;"), &[])
                .and_then(|value| value.l())
                .map_err(|error| {
                    clear_pending_android_jni_exception(env);
                    error.to_string()
                })?;
            JString::cast_local(env, value)
                .map_err(|error| error.to_string())?
                .try_to_string(env)
                .map_err(|error| error.to_string())
        })
        .ok()
        .filter(|value| !value.is_empty())
    }
}

impl Camera for AndroidCamera {
    fn start(&self) -> Result<(), CameraError> {
        self.call_void(jni_str!("cranposeCameraStart"))
            .map_err(CameraError::Failed)
    }

    fn stop(&self) {
        let _ = self.call_void(jni_str!("cranposeCameraStop"));
    }

    fn request_still(&self) -> Result<(), CameraError> {
        self.call_void(jni_str!("cranposeCameraRequestStill"))
            .map_err(CameraError::Failed)
    }

    fn lenses(&self) -> Vec<CameraLens> {
        parse_lenses(
            &self
                .call_string(jni_str!("cranposeCameraLenses"))
                .unwrap_or_default(),
        )
    }

    fn lens(&self) -> Option<String> {
        self.call_string(jni_str!("cranposeCameraLens"))
    }

    fn use_lens(&self, id: &str) -> bool {
        with_android_activity_env(&self.app, |env, activity| {
            let id = env.new_string(id).map_err(|error| error.to_string())?;
            let id: &JObject = id.as_ref();
            env.call_method(
                &activity,
                jni_str!("cranposeCameraUseLens"),
                jni_sig!("(Ljava/lang/String;)Z"),
                &[JValue::Object(id)],
            )
            .and_then(|value| value.z())
            .map_err(|error| error.to_string())
        })
        .unwrap_or(false)
    }

    fn has_flash(&self) -> bool {
        self.call_bool(jni_str!("cranposeCameraHasFlash"))
    }

    fn set_flash(&self, mode: FlashMode) -> bool {
        let mode = match mode {
            FlashMode::Off => 0,
            FlashMode::Auto => 1,
            FlashMode::On => 2,
        };
        with_android_activity_env(&self.app, |env, activity| {
            env.call_method(
                &activity,
                jni_str!("cranposeCameraSetFlash"),
                jni_sig!("(I)Z"),
                &[JValue::Int(mode)],
            )
            .and_then(|value| value.z())
            .map_err(|error| error.to_string())
        })
        .unwrap_or(false)
    }
}

fn parse_lenses(text: &str) -> Vec<CameraLens> {
    text.lines()
        .filter_map(|line| {
            let mut parts = line.splitn(3, '|');
            let id = parts.next()?;
            let facing = parts.next()?;
            let name = parts.next()?;
            Some(CameraLens {
                id: id.to_string(),
                name: name.to_string(),
                facing: match facing {
                    "front" => LensFacing::Front,
                    "external" => LensFacing::External,
                    _ => LensFacing::Back,
                },
            })
        })
        .collect()
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnCameraFrame<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    nv12: JByteArray<'local>,
    width: jint,
    height: jint,
    rotation_degrees: jint,
    sequence: jlong,
) {
    let decoded = env.with_env(|env| env.convert_byte_array(&nv12));
    let Outcome::Ok(bytes) = decoded.into_outcome() else {
        record_dropped_camera_frame();
        return;
    };
    let frame = CameraFrame::new(
        width.max(0) as u32,
        height.max(0) as u32,
        FrameFormat::Nv12,
        rotation_degrees.rem_euclid(360) as u16,
        sequence.max(0) as u64,
        bytes,
    );
    match frame {
        Some(frame) => publish_camera_frame(frame),
        None => record_dropped_camera_frame(),
    }
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnCameraFrameDropped(
    _env: EnvUnowned<'_>,
    _class: JClass<'_>,
) {
    record_dropped_camera_frame();
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnCameraState<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    kind: jint,
    detail: JString<'local>,
) {
    let decoded = env.with_env(|env| detail.try_to_string(env));
    let Outcome::Ok(detail) = decoded.into_outcome() else {
        return;
    };
    let state = match kind {
        1 => CameraState::Running {
            device: if detail.is_empty() {
                "Back camera".to_string()
            } else {
                detail
            },
        },
        2 => CameraState::Stopped,
        3 => CameraState::Failed(CameraError::Failed(if detail.is_empty() {
            "the camera session failed".to_string()
        } else {
            detail
        })),
        _ => CameraState::Idle,
    };
    publish_camera_state(state);
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnCameraStill<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    jpeg: JByteArray<'local>,
    error: JString<'local>,
) {
    let decoded = env.with_env(|env| -> jni::errors::Result<(Option<Vec<u8>>, String)> {
        let bytes = if jpeg.is_null() {
            None
        } else {
            Some(env.convert_byte_array(&jpeg)?)
        };
        Ok((bytes, error.try_to_string(env)?))
    });
    let Outcome::Ok((bytes, error)) = decoded.into_outcome() else {
        return;
    };
    publish_camera_still(match bytes {
        Some(jpeg) if error.is_empty() => Ok(CameraStill { jpeg }),
        _ => Err(CameraError::Failed(if error.is_empty() {
            "the still capture produced no image".to_string()
        } else {
            error
        })),
    });
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnCameraLenses<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    list: JString<'local>,
    active: JString<'local>,
) {
    let decoded = env.with_env(|env| -> jni::errors::Result<(String, String)> {
        Ok((list.try_to_string(env)?, active.try_to_string(env)?))
    });
    let Outcome::Ok((list, active)) = decoded.into_outcome() else {
        return;
    };
    publish_camera_lenses(CameraLenses {
        lenses: parse_lenses(&list),
        active: (!active.is_empty()).then_some(active),
    });
}