#![allow(unsafe_code)]
#![allow(non_snake_case)]
#![allow(dead_code)]
use std::{
ffi::c_void,
sync::{Arc, OnceLock},
time::Duration,
};
use android_activity::{AndroidApp, MainEvent, PollEvent};
use super::platform::{AndroidPlatform, SharedPlatform};
type JNIEnvPtr = *mut c_void;
type JavaVMPtr = *mut c_void;
static ANDROID_APP: OnceLock<AndroidApp> = OnceLock::new();
static PLATFORM: OnceLock<Arc<AndroidPlatform>> = OnceLock::new();
pub mod jni_fns {
use std::ffi::c_void;
pub unsafe fn get_env_from_vm(vm: *mut c_void) -> *mut c_void {
#[repr(C)]
struct JNIInvokeInterface {
reserved0: *mut c_void,
reserved1: *mut c_void,
reserved2: *mut c_void,
destroy_java_vm: *mut c_void,
attach_current_thread:
unsafe extern "C" fn(*mut c_void, *mut *mut c_void, *mut c_void) -> i32,
detach_current_thread: *mut c_void,
get_env: unsafe extern "C" fn(*mut c_void, *mut *mut c_void, i32) -> i32,
}
let vm_table = *(vm as *const *const JNIInvokeInterface);
let mut env: *mut c_void = std::ptr::null_mut();
let result = unsafe { ((*vm_table).get_env)(vm, &mut env, 0x00010006) };
if result == 0 && !env.is_null() {
return env;
}
let result =
unsafe { ((*vm_table).attach_current_thread)(vm, &mut env, std::ptr::null_mut()) };
if result == 0 && !env.is_null() {
env
} else {
std::ptr::null_mut()
}
}
pub unsafe fn get_unicode_char(
env: *mut c_void,
key_code: i32,
action: i32,
meta_state: i32,
) -> u32 {
let fn_table = *(env as *const *const *const c_void);
macro_rules! jni_fn {
($idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*fn_table.add($idx))
};
}
type FindClassFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type GetMethodIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type NewObjectAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64) -> *mut c_void;
type CallIntMethodAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64) -> i32;
type DeleteLocalRefFn = unsafe extern "C" fn(*mut c_void, *mut c_void);
type ExceptionCheckFn = unsafe extern "C" fn(*mut c_void) -> u8;
type ExceptionClearFn = unsafe extern "C" fn(*mut c_void);
let find_class: FindClassFn = jni_fn!(6, FindClassFn);
let get_method_id: GetMethodIDFn = jni_fn!(33, GetMethodIDFn);
let new_object_a: NewObjectAFn = jni_fn!(30, NewObjectAFn);
let call_int_method_a: CallIntMethodAFn = jni_fn!(49, CallIntMethodAFn);
let delete_local_ref: DeleteLocalRefFn = jni_fn!(23, DeleteLocalRefFn);
let exception_check: ExceptionCheckFn = jni_fn!(228, ExceptionCheckFn);
let exception_clear: ExceptionClearFn = jni_fn!(17, ExceptionClearFn);
let class_name = b"android/view/KeyEvent\0";
let cls = find_class(env, class_name.as_ptr() as *const i8);
if cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return 0;
}
let init_name = b"<init>\0";
let init_sig = b"(II)V\0";
let ctor = get_method_id(
env,
cls,
init_name.as_ptr() as *const i8,
init_sig.as_ptr() as *const i8,
);
if ctor.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, cls);
return 0;
}
let ctor_args: [i64; 2] = [action as i64, key_code as i64];
let key_event_obj = new_object_a(env, cls, ctor, ctor_args.as_ptr());
if key_event_obj.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, cls);
return 0;
}
let method_name = b"getUnicodeChar\0";
let method_sig = b"(I)I\0";
let get_unicode_method = get_method_id(
env,
cls,
method_name.as_ptr() as *const i8,
method_sig.as_ptr() as *const i8,
);
if get_unicode_method.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, key_event_obj);
delete_local_ref(env, cls);
return 0;
}
let call_args: [i64; 1] = [meta_state as i64];
let unicode = call_int_method_a(env, key_event_obj, get_unicode_method, call_args.as_ptr());
if exception_check(env) != 0 {
exception_clear(env);
delete_local_ref(env, key_event_obj);
delete_local_ref(env, cls);
return 0;
}
delete_local_ref(env, key_event_obj);
delete_local_ref(env, cls);
if unicode > 0 {
unicode as u32
} else {
0
}
}
}
fn get_jni_env() -> JNIEnvPtr {
let vm = java_vm();
if vm.is_null() {
return std::ptr::null_mut();
}
unsafe { jni_fns::get_env_from_vm(vm) }
}
pub fn unicode_char_for_key_event(key_code: i32, action: i32, meta_state: i32) -> u32 {
let env = get_jni_env();
if env.is_null() {
return 0;
}
unsafe { jni_fns::get_unicode_char(env, key_code, action, meta_state) }
}
pub fn java_vm() -> *mut c_void {
ANDROID_APP
.get()
.map(|app| app.vm_as_ptr())
.unwrap_or(std::ptr::null_mut())
}
pub fn activity_as_ptr() -> *mut c_void {
ANDROID_APP
.get()
.map(|app| app.activity_as_ptr())
.unwrap_or(std::ptr::null_mut())
}
pub fn android_app() -> Option<AndroidApp> {
ANDROID_APP.get().cloned()
}
pub fn platform() -> Option<&'static Arc<AndroidPlatform>> {
PLATFORM.get()
}
pub fn shared_platform() -> Option<SharedPlatform> {
PLATFORM
.get()
.map(|arc| SharedPlatform::new(Arc::clone(arc)))
}
#[repr(C)]
pub struct ANativeWindow {
_priv: [u8; 0],
}
unsafe extern "C" {
fn ANativeWindow_acquire(window: *mut ANativeWindow);
fn ANativeWindow_release(window: *mut ANativeWindow);
fn ANativeWindow_getWidth(window: *mut ANativeWindow) -> i32;
fn ANativeWindow_getHeight(window: *mut ANativeWindow) -> i32;
}
const AMOTION_EVENT_ACTION_DOWN: u32 = 0;
const AMOTION_EVENT_ACTION_UP: u32 = 1;
const AMOTION_EVENT_ACTION_MOVE: u32 = 2;
pub fn query_night_mode_via_jni() -> bool {
let vm = java_vm();
if vm.is_null() {
return false;
}
let env = unsafe { jni_fns::get_env_from_vm(vm) };
if env.is_null() {
return false;
}
let activity_obj = activity_as_ptr();
if activity_obj.is_null() {
return false;
}
unsafe { query_night_mode_impl(env, activity_obj) }
}
unsafe fn query_night_mode_impl(env: *mut c_void, activity_obj: *mut c_void) -> bool {
let fn_table = *(env as *const *const *const c_void);
macro_rules! jni_fn {
($idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*fn_table.add($idx))
};
}
type FindClassFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type GetMethodIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type CallObjectMethodAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64) -> *mut c_void;
type GetFieldIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type GetIntFieldFn = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void) -> i32;
type DeleteLocalRefFn = unsafe extern "C" fn(*mut c_void, *mut c_void);
type ExceptionCheckFn = unsafe extern "C" fn(*mut c_void) -> u8;
type ExceptionClearFn = unsafe extern "C" fn(*mut c_void);
let find_class: FindClassFn = jni_fn!(6, FindClassFn);
let get_method_id: GetMethodIDFn = jni_fn!(33, GetMethodIDFn);
let call_object_method_a: CallObjectMethodAFn = jni_fn!(36, CallObjectMethodAFn);
let get_field_id: GetFieldIDFn = jni_fn!(94, GetFieldIDFn);
let get_int_field: GetIntFieldFn = jni_fn!(100, GetIntFieldFn);
let delete_local_ref: DeleteLocalRefFn = jni_fn!(23, DeleteLocalRefFn);
let exception_check: ExceptionCheckFn = jni_fn!(228, ExceptionCheckFn);
let exception_clear: ExceptionClearFn = jni_fn!(17, ExceptionClearFn);
let no_args: [i64; 0] = [];
let activity_cls = find_class(env, b"android/app/Activity\0".as_ptr() as *const i8);
if activity_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return false;
}
let get_resources = get_method_id(
env,
activity_cls,
b"getResources\0".as_ptr() as *const i8,
b"()Landroid/content/res/Resources;\0".as_ptr() as *const i8,
);
if get_resources.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, activity_cls);
return false;
}
let resources = call_object_method_a(env, activity_obj, get_resources, no_args.as_ptr());
delete_local_ref(env, activity_cls);
if resources.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return false;
}
let resources_cls = find_class(
env,
b"android/content/res/Resources\0".as_ptr() as *const i8,
);
if resources_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, resources);
return false;
}
let get_config = get_method_id(
env,
resources_cls,
b"getConfiguration\0".as_ptr() as *const i8,
b"()Landroid/content/res/Configuration;\0".as_ptr() as *const i8,
);
if get_config.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, resources_cls);
delete_local_ref(env, resources);
return false;
}
let config = call_object_method_a(env, resources, get_config, no_args.as_ptr());
delete_local_ref(env, resources_cls);
delete_local_ref(env, resources);
if config.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return false;
}
let config_cls = find_class(
env,
b"android/content/res/Configuration\0".as_ptr() as *const i8,
);
if config_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, config);
return false;
}
let ui_mode_field = get_field_id(
env,
config_cls,
b"uiMode\0".as_ptr() as *const i8,
b"I\0".as_ptr() as *const i8,
);
if ui_mode_field.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, config_cls);
delete_local_ref(env, config);
return false;
}
let ui_mode = get_int_field(env, config, ui_mode_field);
delete_local_ref(env, config_cls);
delete_local_ref(env, config);
if exception_check(env) != 0 {
exception_clear(env);
return false;
}
const UI_MODE_NIGHT_MASK: i32 = 0x30;
const UI_MODE_NIGHT_YES: i32 = 0x20;
let is_dark = (ui_mode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES;
log::debug!(
"query_night_mode_via_jni: uiMode={:#x} is_dark={}",
ui_mode,
is_dark
);
is_dark
}
fn process_input_events(app: &AndroidApp) {
let platform = match PLATFORM.get() {
Some(p) => p,
None => {
log::trace!("process_input_events: no platform yet");
return;
}
};
let win = match platform.primary_window() {
Some(w) => w,
None => {
log::trace!("process_input_events: no primary window yet");
return;
}
};
match app.input_events_iter() {
Ok(mut iter) => {
loop {
let read_input = iter.next(|event| {
use android_activity::input::{InputEvent, MotionAction};
match event {
InputEvent::MotionEvent(motion_event) => {
let action = motion_event.action();
let pointer_count = motion_event.pointer_count();
log::debug!(
"process_input_events: MotionEvent action={:?} pointers={}",
action,
pointer_count,
);
for i in 0..pointer_count {
let pointer = motion_event.pointer_at_index(i);
let touch_action = match action {
MotionAction::Down => AMOTION_EVENT_ACTION_DOWN,
MotionAction::PointerDown => {
if i != motion_event.pointer_index() {
continue;
}
AMOTION_EVENT_ACTION_DOWN
}
MotionAction::Up => AMOTION_EVENT_ACTION_UP,
MotionAction::PointerUp => {
if i != motion_event.pointer_index() {
continue;
}
AMOTION_EVENT_ACTION_UP
}
MotionAction::Move => AMOTION_EVENT_ACTION_MOVE,
MotionAction::Cancel => AMOTION_EVENT_ACTION_UP,
_ => continue,
};
let touch = crate::android::TouchPoint {
id: pointer.pointer_id(),
x: pointer.x(),
y: pointer.y(),
action: touch_action,
};
log::debug!(
"process_input_events: dispatching touch id={} x={:.0} y={:.0} action={}",
touch.id, touch.x, touch.y, touch.action,
);
win.handle_touch(touch);
}
android_activity::InputStatus::Handled
}
InputEvent::KeyEvent(key_event) => {
use android_activity::input::KeyAction;
let action = match key_event.action() {
KeyAction::Down => 0,
KeyAction::Up => 1,
_ => return android_activity::InputStatus::Unhandled,
};
let key_code: u32 = key_event.key_code().into();
let meta_state: u32 = key_event.meta_state().0;
let unicode_char = unicode_char_for_key_event(
key_code as i32,
action,
meta_state as i32,
);
if unicode_char != 0 {
log::trace!(
"dispatch_key_event: code={} action={} meta={:#x} → unicode=U+{:04X}",
key_code,
action,
meta_state,
unicode_char
);
}
let key_event = crate::android::AndroidKeyEvent {
key_code: key_code as i32,
action,
meta_state: meta_state as i32,
unicode_char,
};
win.handle_key_event(key_event);
android_activity::InputStatus::Handled
}
_ => android_activity::InputStatus::Unhandled,
}
});
if !read_input {
break;
}
}
}
Err(err) => {
log::error!("Failed to get input events iterator: {err:?}");
}
}
}
pub fn run_event_loop(app: &AndroidApp) {
log::info!("run_event_loop: entering main loop");
let mut init_window_done = false;
let mut iteration: u64 = 0;
let mut last_heartbeat = std::time::Instant::now();
let mut app_is_active = false;
loop {
iteration += 1;
let now = std::time::Instant::now();
if now.duration_since(last_heartbeat) >= Duration::from_secs(5) {
log::info!(
"run_event_loop: heartbeat — iteration={}, init_done={}",
iteration,
init_window_done,
);
last_heartbeat = now;
}
if let Some(platform) = PLATFORM.get() {
if platform.should_quit() {
log::info!("run_event_loop: platform requested quit");
break;
}
platform.tick();
}
let poll_start = std::time::Instant::now();
app.poll_events(Some(Duration::from_millis(16)), |event| {
match event {
PollEvent::Main(main_event) => {
handle_main_event(app, main_event);
}
PollEvent::Wake => {
}
_ => {}
}
});
let poll_elapsed = poll_start.elapsed();
if poll_elapsed > Duration::from_secs(1) {
log::warn!(
"run_event_loop: poll_events blocked for {:.1}s (iteration={}, active={})",
poll_elapsed.as_secs_f64(),
iteration,
app_is_active,
);
}
if let Some(platform) = PLATFORM.get() {
if let Some(win) = platform.primary_window() {
let is_active = win.is_active();
if is_active != app_is_active {
log::info!(
"run_event_loop: active state changed {} -> {} (iteration={})",
app_is_active,
is_active,
iteration,
);
app_is_active = is_active;
}
}
}
process_input_events(app);
if !init_window_done {
if let Some(platform) = PLATFORM.get() {
if platform.primary_window().is_some() {
if let Some(finish_cb) = platform.take_finish_launching_callback() {
log::info!(
"run_event_loop: invoking finish_launching callback (iteration={})",
iteration,
);
finish_cb();
log::info!(
"run_event_loop: finish_launching callback completed (iteration={})",
iteration,
);
}
if let Some(init_cb) = platform.take_on_init_window_callback() {
let win = platform.primary_window().unwrap();
log::info!(
"run_event_loop: invoking on_init_window callback (iteration={})",
iteration,
);
init_cb(win);
log::info!(
"run_event_loop: on_init_window callback completed (iteration={})",
iteration,
);
}
init_window_done = true;
}
}
}
if let Some(platform) = PLATFORM.get() {
platform.flush_main_thread_tasks();
if app_is_active {
if let Some(win) = platform.primary_window() {
let frame_start = std::time::Instant::now();
win.request_frame();
let frame_elapsed = frame_start.elapsed();
if frame_elapsed > Duration::from_millis(100) {
log::warn!(
"run_event_loop: request_frame took {:.1}ms (iteration={})",
frame_elapsed.as_secs_f64() * 1000.0,
iteration,
);
}
}
process_input_events(app);
}
}
}
log::info!("run_event_loop: exiting main loop");
}
fn handle_main_event(app: &AndroidApp, event: MainEvent<'_>) {
match event {
MainEvent::InitWindow { .. } => {
log::info!("MainEvent::InitWindow");
if let Some(platform) = PLATFORM.get() {
if let Some(native_window) = app.native_window() {
let raw_ptr = native_window.ptr().as_ptr() as *mut ANativeWindow;
let width = unsafe { ANativeWindow_getWidth(raw_ptr) };
let height = unsafe { ANativeWindow_getHeight(raw_ptr) };
log::info!("InitWindow — {}×{}", width, height);
let native_win = raw_ptr as *mut crate::android::display::ANativeWindow;
let asset_manager = app.asset_manager().ptr().as_ptr() as *mut std::ffi::c_void;
if let Err(e) =
unsafe { platform.update_primary_display(native_win, asset_manager) }
{
log::warn!("failed to update primary display: {e:#}");
}
let scale_factor = platform
.primary_display()
.map(|d| d.scale_factor())
.unwrap_or(1.0);
let win_ptr = raw_ptr as *mut crate::android::window::ANativeWindow;
match unsafe { platform.open_window(win_ptr, scale_factor, false) } {
Ok(win) => {
log::info!(
"window opened — id={:#x} scale={:.1}",
win.id(),
scale_factor
);
let cr = app.content_rect();
log::info!(
"content_rect: left={} top={} right={} bottom={} (window={}×{})",
cr.left,
cr.top,
cr.right,
cr.bottom,
width,
height,
);
win.update_safe_area_from_content_rect(
cr.left, cr.top, cr.right, cr.bottom,
);
log::info!("InitWindow: window ready, callback deferred to event loop");
}
Err(e) => {
log::error!("failed to open window: {e:#}");
}
}
}
}
}
MainEvent::TerminateWindow { .. } => {
log::info!("MainEvent::TerminateWindow");
if let Some(platform) = PLATFORM.get() {
if let Some(win) = platform.primary_window() {
win.term_window();
platform.close_window(win.id());
}
}
}
MainEvent::WindowResized { .. } => {
log::debug!("MainEvent::WindowResized");
if let Some(platform) = PLATFORM.get() {
if let Some(win) = platform.primary_window() {
win.handle_resize();
let cr = app.content_rect();
log::debug!(
"WindowResized content_rect: left={} top={} right={} bottom={}",
cr.left,
cr.top,
cr.right,
cr.bottom,
);
win.update_safe_area_from_content_rect(cr.left, cr.top, cr.right, cr.bottom);
}
}
}
MainEvent::GainedFocus => {
log::debug!("MainEvent::GainedFocus");
if let Some(platform) = PLATFORM.get() {
platform.did_become_active();
if let Some(win) = platform.primary_window() {
win.set_active(true);
}
}
}
MainEvent::LostFocus => {
log::debug!("MainEvent::LostFocus");
if let Some(platform) = PLATFORM.get() {
platform.did_enter_background();
if let Some(win) = platform.primary_window() {
win.set_active(false);
}
}
}
MainEvent::Resume { .. } => {
log::debug!("MainEvent::Resume");
if let Some(platform) = PLATFORM.get() {
platform.did_become_active();
}
}
MainEvent::Pause => {
log::debug!("MainEvent::Pause");
if let Some(platform) = PLATFORM.get() {
platform.did_enter_background();
}
}
MainEvent::ConfigChanged { .. } => {
log::debug!("MainEvent::ConfigChanged");
if let Some(platform) = PLATFORM.get() {
platform.notify_keyboard_layout_change();
let is_dark = query_night_mode_via_jni();
if let Some(win) = platform.primary_window() {
let appearance = if is_dark {
crate::android::window::WindowAppearance::Dark
} else {
crate::android::window::WindowAppearance::Light
};
win.set_appearance(appearance);
}
}
}
MainEvent::LowMemory => {
log::warn!("MainEvent::LowMemory — consider releasing cached resources");
}
MainEvent::Destroy => {
log::info!("MainEvent::Destroy");
if let Some(platform) = PLATFORM.get() {
platform.quit();
}
}
_ => {
}
}
}
pub fn poll_events(timeout_ms: i32) -> bool {
if let Some(platform) = PLATFORM.get() {
if platform.should_quit() {
return true;
}
platform.tick();
}
let app = match ANDROID_APP.get() {
Some(app) => app,
None => return false,
};
let timeout = if timeout_ms < 0 {
None
} else {
Some(Duration::from_millis(timeout_ms as u64))
};
app.poll_events(timeout, |event| match event {
PollEvent::Main(main_event) => {
handle_main_event(app, main_event);
}
PollEvent::Wake => {}
_ => {}
});
process_input_events(app);
if let Some(platform) = PLATFORM.get() {
platform.flush_main_thread_tasks();
if let Some(win) = platform.primary_window() {
win.request_frame();
}
}
false
}
pub fn install_panic_hook() {
std::panic::set_hook(Box::new(|info| {
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Box<dyn Any>".to_string()
};
if let Some(loc) = info.location() {
log::error!(
"PANIC at {}:{}:{}: {}",
loc.file(),
loc.line(),
loc.column(),
payload
);
} else {
log::error!("PANIC: {}", payload);
}
}));
}
pub fn init_platform(app: &AndroidApp) -> &'static Arc<AndroidPlatform> {
let _ = ANDROID_APP.set(app.clone());
log::info!("init_platform: stored AndroidApp");
let platform = Arc::new(AndroidPlatform::new(false));
log::info!("init_platform: AndroidPlatform created");
PLATFORM
.set(Arc::clone(&platform))
.unwrap_or_else(|_| log::warn!("PLATFORM already set — duplicate init_platform?"));
PLATFORM.get().unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn poll_events_returns_false_when_no_platform() {
let result = poll_events(0);
let _ = result;
}
#[test]
fn java_vm_returns_null_before_init() {
let vm = java_vm();
assert!(vm.is_null());
}
#[test]
fn activity_as_ptr_returns_null_before_init() {
let ptr = activity_as_ptr();
assert!(ptr.is_null());
}
#[test]
fn android_app_returns_none_before_init() {
assert!(android_app().is_none());
}
#[test]
fn platform_returns_none_before_init() {
assert!(platform().is_none());
}
}