YrXtals/src/android.rs

390 lines
13 KiB
Rust

use jni::objects::{JByteArray, JClass, JIntArray, JObject, JObjectArray, JString};
use jni::sys::{jboolean, jbyte, jfloat, jint, jlong, jobject};
use jni::JNIEnv;
use ndk::native_window::NativeWindow;
use raw_window_handle::{
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
};
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::Once;
use crate::viewport::ViewportHandle;
macro_rules! dbg_log {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
log::info!($($arg)*);
};
}
/// owns the inner viewport plus the ANativeWindow whose lifetime backs the wgpu surface.
struct AndroidViewport {
inner: ViewportHandle,
_window: NativeWindow,
}
/// initializes android logger and the ndk_context singleton consumed by cpal's AAudio backend.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_initContext<'a>(
env: JNIEnv<'a>,
_class: JClass<'a>,
activity: JObject<'a>,
) {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
#[cfg(debug_assertions)]
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("yr_crystals"),
);
redirect_stdio_to_logcat();
let vm = match env.get_java_vm() {
Ok(vm) => vm,
Err(_) => return,
};
let activity_ref = env.new_global_ref(&activity).ok();
if let Some(activity_ref) = activity_ref {
unsafe {
ndk_context::initialize_android_context(
vm.get_java_vm_pointer() as *mut c_void,
activity_ref.as_raw() as *mut c_void,
);
}
std::mem::forget(activity_ref);
}
dbg_log!("ndk_context initialized");
});
}
/// builds the viewport from a Surface jobject, holding the ANativeWindow for the surface's life.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportCreate<'a>(
env: JNIEnv<'a>,
_class: JClass<'a>,
surface: JObject<'a>,
width: jfloat,
height: jfloat,
scale: jfloat,
) -> jlong {
let scale_f = (scale as f32).max(1.0);
let window = match unsafe { NativeWindow::from_surface(env.get_raw(), surface.as_raw()) } {
Some(w) => w,
None => {
dbg_log!("ANativeWindow_fromSurface returned null");
return 0;
}
};
let win_ptr = match NonNull::new(window.ptr().as_ptr() as *mut c_void) {
Some(p) => p,
None => return 0,
};
let raw_window = RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(win_ptr));
let raw_display = RawDisplayHandle::Android(AndroidDisplayHandle::new());
let inner = match ViewportHandle::new_from_raw(
raw_window,
raw_display,
width as f32,
height as f32,
scale_f,
) {
Some(v) => v,
None => {
dbg_log!("ViewportHandle::new_from_raw returned None");
return 0;
}
};
dbg_log!("viewportCreate ok: {}x{}@{}", width, height, scale);
let boxed = Box::new(AndroidViewport {
inner,
_window: window,
});
Box::into_raw(boxed) as jlong
}
/// drops the viewport and releases the ANativeWindow reference.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportDestroy<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
) {
if handle == 0 {
return;
}
unsafe { drop(Box::from_raw(handle as *mut AndroidViewport)) };
}
/// renders one frame from the choreographer callback.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportRender<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
av.inner.render_frame();
}
/// reconfigures the wgpu surface to the given bounds and scale.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportResize<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
width: jfloat,
height: jfloat,
scale: jfloat,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
av.inner.resize_px(width as f32, height as f32, (scale as f32).max(1.0));
}
/// forwards a single touch lifecycle event into the viewport gesture pipeline.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportTouchEvent<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
x: jfloat,
y: jfloat,
pressed: jboolean,
moved: jboolean,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
av.inner.push_touch(x as f32, y as f32, pressed != 0, moved != 0);
}
/// forwards an Android KeyEvent into the viewport with optional unicode text.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportKeyEvent<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
key: jint,
modifiers: jint,
pressed: jboolean,
text: JString<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let utf8 = if text.is_null() {
None
} else {
env.get_string(&text).ok().map(|s| String::from(s))
};
av.inner.push_key_event(key as u32, utf8, modifiers as u32, pressed != 0);
}
/// resolves a SAF picked folder path into a library scan request.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFolder<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
path: JString<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let Ok(path_str) = env.get_string(&path) else { return };
av.inner.apply_picked_folder(String::from(path_str).into());
}
/// loads a single picked audio file path as a one-track library.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFile<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
path: JString<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let Ok(path_str) = env.get_string(&path) else { return };
av.inner.apply_picked_file(String::from(path_str).into());
}
/// passes a batch of picked file paths to the library in a single call.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFiles<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
paths: JObjectArray<'a>,
) {
let Ok(count) = env.get_array_length(&paths) else { return };
dbg_log!("viewportApplyPickedFiles: enter count={count} handle={handle:#x}");
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else {
dbg_log!("viewportApplyPickedFiles: null handle");
return;
};
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let Ok(elem) = env.get_object_array_element(&paths, i) else { continue };
let jstr: JString = elem.into();
let Ok(s) = env.get_string(&jstr) else { continue };
let s = String::from(s);
dbg_log!("viewportApplyPickedFiles: path[{i}] = {s}");
out.push(std::path::PathBuf::from(s));
}
dbg_log!("viewportApplyPickedFiles: forwarding {} paths to App", out.len());
av.inner.apply_picked_files(out);
dbg_log!("viewportApplyPickedFiles: returned from App");
}
/// drives the import progress strip, clearing on a total of zero.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetLibraryProgress<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
current: jint,
total: jint,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
av.inner.set_library_progress(current.max(0) as u32, total.max(0) as u32);
}
/// shows or clears the modal coordinator-wait overlay; null/empty message hides the overlay.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetCoordinatingMessage<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
msg: JString<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let s = if msg.is_null() {
None
} else {
env.get_string(&msg).ok().map(String::from).filter(|s| !s.is_empty())
};
av.inner.set_coordinating_message(s);
}
/// seeds the sidebar with placeholder rows of titles plus track-number tags ahead of export completion.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetPendingTitles<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
titles: JObjectArray<'a>,
track_numbers: JIntArray<'a>,
) {
let Ok(count) = env.get_array_length(&titles) else { return };
dbg_log!("viewportSetPendingTitles: count={count}");
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else {
dbg_log!("viewportSetPendingTitles: null handle");
return;
};
let mut tns = vec![0i32; count as usize];
let _ = env.get_int_array_region(&track_numbers, 0, &mut tns);
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let title = env
.get_object_array_element(&titles, i)
.ok()
.map(|e| {
let js: JString = e.into();
env.get_string(&js).ok().map(String::from).unwrap_or_default()
})
.unwrap_or_default();
let title = if title.is_empty() {
format!("Track {}", i + 1)
} else {
title
};
let tn_raw = tns[i as usize];
let tn = if tn_raw <= 0 { None } else { Some(tn_raw as u32) };
out.push((title, tn));
}
av.inner.set_pending_titles(out);
}
/// fills in the file path for a placeholder track at completion of the export.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetTrackPath<'a>(
mut env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
idx: jint,
path: JString<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let Ok(s) = env.get_string(&path) else { return };
let s = String::from(s);
dbg_log!("viewportSetTrackPath: idx={idx} path={s}");
av.inner.set_track_path(idx.max(0) as usize, std::path::PathBuf::from(s));
}
/// hands raw JPEG/PNG artwork bytes to the art decode worker for a given track index.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetTrackArt<'a>(
env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
idx: jint,
bytes: JByteArray<'a>,
) {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
let Ok(len) = env.get_array_length(&bytes) else { return };
dbg_log!("viewportSetTrackArt: idx={idx} len={len}");
if len <= 0 { return; }
let mut buf = vec![0i8; len as usize];
if env.get_byte_array_region(&bytes, 0, &mut buf).is_err() { return; }
let owned: Vec<u8> = buf.into_iter().map(|b: jbyte| b as u8).collect();
av.inner.set_track_art_bytes(idx.max(0) as usize, owned);
}
/// drains the pending picker request flag: 0 idle, 1 folder, 2 file.
#[unsafe(no_mangle)]
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportTakePendingPick<'a>(
_env: JNIEnv<'a>,
_class: JClass<'a>,
handle: jlong,
) -> jint {
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return 0 };
av.inner.take_pending_pick() as jint
}
/// dup2s stdout and stderr onto a pipe and pumps each line into logcat under the `yr_crystals.io` tag.
fn redirect_stdio_to_logcat() {
use std::io::{BufRead, BufReader};
use std::os::fd::FromRawFd;
let mut fds: [libc::c_int; 2] = [0, 0];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
if rc != 0 {
return;
}
let (rfd, wfd) = (fds[0], fds[1]);
unsafe {
libc::dup2(wfd, libc::STDOUT_FILENO);
libc::dup2(wfd, libc::STDERR_FILENO);
libc::close(wfd);
}
std::thread::Builder::new()
.name("yr_crystals.stdio_to_logcat".into())
.spawn(move || {
let file = unsafe { std::fs::File::from_raw_fd(rfd) };
let reader = BufReader::new(file);
for line in reader.lines().flatten() {
log::info!(target: "yr_crystals.io", "{}", line);
}
})
.ok();
}
/// nullability check on a JObject through its raw pointer.
trait JObjectExt {
fn is_null(&self) -> bool;
}
impl<'a> JObjectExt for JObject<'a> {
fn is_null(&self) -> bool {
self.as_raw() as *const jobject == std::ptr::null()
}
}
impl<'a> JObjectExt for JString<'a> {
fn is_null(&self) -> bool {
let o: &JObject = self.as_ref();
o.is_null()
}
}