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 = 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() } }