use crate::scenes::SceneFactory;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
#[cfg(target_os = "android")]
use std::ffi::{CStr, CString};
#[cfg(target_os = "android")]
use std::sync::Mutex;
#[cfg(target_os = "android")]
use std::sync::OnceLock;
#[cfg(target_os = "android")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "android")]
static ANDROID_APP: OnceLock<AndroidApp> = OnceLock::new();
#[cfg(target_os = "android")]
static JVM: OnceLock<jni::JavaVM> = OnceLock::new();
static ACTIVITY: OnceLock<jni::objects::GlobalRef> = OnceLock::new();
static FLOATING_SERVICE_CLASS: OnceLock<String> = OnceLock::new();
static FLOATING_SURFACE: Mutex<Option<jni::objects::GlobalRef>> = Mutex::new(None);
static FLOATING_SCENE_FACTORY: OnceLock<SceneFactory> = OnceLock::new();
static FLOATING_WINDOW_ENABLED: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "android")]
const SPOT_NATIVE_TAG: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"SpotNative\0") };
#[cfg(target_os = "android")]
pub fn get_jvm() -> Option<&'static jni::JavaVM> {
JVM.get()
}
#[cfg(target_os = "android")]
pub fn get_activity() -> Option<&'static jni::objects::GlobalRef> {
ACTIVITY.get()
}
#[cfg(target_os = "android")]
pub fn get_app() -> Option<AndroidApp> {
ANDROID_APP.get().cloned()
}
#[cfg(target_os = "android")]
pub(crate) fn logcat_info(message: &str) {
logcat(ndk_sys::android_LogPriority::ANDROID_LOG_INFO, message);
}
#[cfg(target_os = "android")]
pub(crate) fn logcat_warn(message: &str) {
logcat(ndk_sys::android_LogPriority::ANDROID_LOG_WARN, message);
}
#[cfg(target_os = "android")]
fn logcat(priority: ndk_sys::android_LogPriority, message: &str) {
let Ok(message) = CString::new(message.replace('\0', " ")) else {
return;
};
unsafe {
ndk_sys::__android_log_write(
priority.0 as i32,
SPOT_NATIVE_TAG.as_ptr(),
message.as_ptr(),
);
}
}
#[cfg(target_os = "android")]
fn find_class<'a>(
env: &mut jni::JNIEnv<'a>,
class_name: &str,
) -> anyhow::Result<jni::objects::JClass<'a>> {
if class_name.starts_with("android/") || class_name.starts_with("java/") {
return Ok(env.find_class(class_name)?);
}
let activity = ACTIVITY
.get()
.ok_or_else(|| anyhow::anyhow!("ACTIVITY not initialized"))?
.as_obj();
let class_loader = env
.call_method(activity, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])?
.l()?;
let class_name_java = env.new_string(class_name.replace('/', "."))?;
let class_obj = env
.call_method(
class_loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[(&class_name_java).into()],
)?
.l()?;
Ok(class_obj.into())
}
#[cfg(target_os = "android")]
pub fn init(app: AndroidApp) {
let _ = ANDROID_APP.set(app.clone());
unsafe {
let Ok(vm) = jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) else {
eprintln!("[spot][android] failed to create JavaVM from raw pointer");
return;
};
let activity = jni::objects::JObject::from_raw(app.activity_as_ptr() as *mut _);
let _ = JVM.set(vm);
let Some(jvm) = JVM.get() else {
eprintln!("[spot][android] JVM was not stored during init");
return;
};
let Ok(mut env) = jvm.attach_current_thread() else {
eprintln!("[spot][android] failed to attach current thread during init");
return;
};
let Ok(activity_ref) = env.new_global_ref(activity) else {
eprintln!("[spot][android] failed to create global activity ref");
return;
};
let _ = ACTIVITY.set(activity_ref);
if let Some(class_name) = floating_window_service_class() {
if let Err(err) = register_floating_window_methods(&mut env, class_name) {
eprintln!(
"[spot][android] failed to register floating window methods for {}: {:?}",
class_name, err
);
}
}
}
}
#[cfg(target_os = "android")]
pub fn set_floating_window_scene<T: crate::Spot + 'static>() {
let _ = FLOATING_SCENE_FACTORY.set(Box::new(|ctx| Box::new(T::initialize(ctx))));
}
#[cfg(target_os = "android")]
pub(crate) fn get_floating_scene_factory() -> Option<&'static SceneFactory> {
FLOATING_SCENE_FACTORY.get()
}
#[cfg(target_os = "android")]
pub(crate) fn take_floating_surface() -> Option<jni::objects::GlobalRef> {
match FLOATING_SURFACE.lock() {
Ok(mut guard) => guard.take(),
Err(err) => {
eprintln!("[spot][android] failed to lock floating surface: {}", err);
None
}
}
}
#[cfg(target_os = "android")]
pub fn on_surface_created(env: &jni::JNIEnv, surface: jni::objects::JObject) {
eprintln!("[spot][android] on_surface_created called");
let Ok(global_ref) = env.new_global_ref(surface) else {
eprintln!("[spot][android] failed to create global ref for floating surface");
return;
};
match FLOATING_SURFACE.lock() {
Ok(mut guard) => *guard = Some(global_ref),
Err(err) => eprintln!("[spot][android] failed to lock floating surface: {}", err),
}
}
#[cfg(target_os = "android")]
pub fn on_surface_destroyed() {
match FLOATING_SURFACE.lock() {
Ok(mut guard) => *guard = None,
Err(err) => eprintln!("[spot][android] failed to lock floating surface: {}", err),
}
}
#[cfg(target_os = "android")]
extern "system" fn native_on_floating_surface_created(
env: jni::JNIEnv,
_class: jni::objects::JClass,
surface: jni::objects::JObject,
) {
on_surface_created(&env, surface);
}
#[cfg(target_os = "android")]
extern "system" fn native_on_floating_surface_destroyed(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
on_surface_destroyed();
}
#[cfg(target_os = "android")]
pub fn set_floating_window_service(class_name: &str) {
let _ = FLOATING_SERVICE_CLASS.set(class_name.to_string());
if let Some(jvm) = JVM.get() {
if ACTIVITY.get().is_some() {
let Ok(mut env) = jvm.attach_current_thread() else {
eprintln!(
"[spot][android] failed to attach thread for floating service registration"
);
return;
};
if let Err(err) = register_floating_window_methods(&mut env, class_name) {
eprintln!(
"[spot][android] failed to register floating window methods for {}: {:?}",
class_name, err
);
}
}
}
}
#[cfg(target_os = "android")]
pub(crate) fn floating_window_service_class() -> Option<&'static str> {
FLOATING_SERVICE_CLASS.get().map(|s| s.as_str())
}
#[cfg(target_os = "android")]
pub(crate) fn floating_window_enabled() -> bool {
FLOATING_WINDOW_ENABLED.load(Ordering::Relaxed)
}
#[cfg(target_os = "android")]
pub fn start_service(class_name: &str) {
let Some(jvm) = JVM.get() else {
return;
};
let Some(activity_ref) = ACTIVITY.get() else {
return;
};
let Ok(mut env) = jvm.attach_current_thread() else {
eprintln!("[spot][android] failed to attach thread for start_service");
return;
};
let activity = activity_ref.as_obj();
let Ok(intent_class) = find_class(&mut env, "android/content/Intent") else {
eprintln!("[spot][android] failed to resolve Intent class");
return;
};
let Ok(service_class) = find_class(&mut env, class_name) else {
eprintln!(
"[spot][android] failed to resolve service class {}",
class_name
);
return;
};
let Ok(intent) = env.new_object(
intent_class,
"(Landroid/content/Context;Ljava/lang/Class;)V",
&[(&activity).into(), (&service_class).into()],
) else {
eprintln!(
"[spot][android] failed to create intent for service {}",
class_name
);
return;
};
let Ok(version_class) = find_class(&mut env, "android/os/Build$VERSION") else {
eprintln!("[spot][android] failed to resolve Build.VERSION");
return;
};
let Ok(sdk_int) = env.get_static_field(version_class, "SDK_INT", "I") else {
eprintln!("[spot][android] failed to read SDK_INT");
return;
};
let Ok(sdk_int) = sdk_int.i() else {
eprintln!("[spot][android] SDK_INT field had unexpected type");
return;
};
if sdk_int >= 26 {
if let Err(err) = env.call_method(
&activity,
"startForegroundService",
"(Landroid/content/Intent;)Landroid/content/ComponentName;",
&[(&intent).into()],
) {
eprintln!(
"[spot][android] startForegroundService failed for {}: {:?}",
class_name, err
);
}
} else {
if let Err(err) = env.call_method(
&activity,
"startService",
"(Landroid/content/Intent;)Landroid/content/ComponentName;",
&[(&intent).into()],
) {
eprintln!(
"[spot][android] startService failed for {}: {:?}",
class_name, err
);
}
}
}
#[cfg(target_os = "android")]
pub fn stop_service(class_name: &str) {
let Some(jvm) = JVM.get() else {
return;
};
let Some(activity_ref) = ACTIVITY.get() else {
return;
};
let Ok(mut env) = jvm.attach_current_thread() else {
eprintln!("[spot][android] failed to attach thread for stop_service");
return;
};
let activity = activity_ref.as_obj();
let Ok(intent_class) = find_class(&mut env, "android/content/Intent") else {
eprintln!("[spot][android] failed to resolve Intent class");
return;
};
let Ok(service_class) = find_class(&mut env, class_name) else {
eprintln!(
"[spot][android] failed to resolve service class {}",
class_name
);
return;
};
let Ok(intent) = env.new_object(
intent_class,
"(Landroid/content/Context;Ljava/lang/Class;)V",
&[(&activity).into(), (&service_class).into()],
) else {
eprintln!(
"[spot][android] failed to create stop-service intent for {}",
class_name
);
return;
};
if let Err(err) = env.call_method(
&activity,
"stopService",
"(Landroid/content/Intent;)Z",
&[(&intent).into()],
) {
eprintln!(
"[spot][android] stopService failed for {}: {:?}",
class_name, err
);
}
}
#[cfg(target_os = "android")]
pub fn current_local_epoch_day() -> Option<u64> {
let jvm = JVM.get()?;
let _activity_ref = ACTIVITY.get()?;
let mut env = jvm.attach_current_thread().ok()?;
let local_date_class = find_class(&mut env, "java/time/LocalDate").ok()?;
let today = env
.call_static_method(&local_date_class, "now", "()Ljava/time/LocalDate;", &[])
.and_then(|value| value.l())
.ok()?;
env.call_method(&today, "toEpochDay", "()J", &[])
.and_then(|value| value.j())
.ok()
.map(|day| day.max(0) as u64)
}
#[cfg(target_os = "android")]
pub fn set_floating_window_enabled(enabled: bool) {
FLOATING_WINDOW_ENABLED.store(enabled, Ordering::Relaxed);
}
#[cfg(target_os = "android")]
fn register_floating_window_methods(
env: &mut jni::JNIEnv<'_>,
class_name: &str,
) -> anyhow::Result<()> {
let class = find_class(env, class_name)?;
let methods = [
jni::NativeMethod {
name: "onFloatingSurfaceCreated".into(),
sig: "(Landroid/view/Surface;)V".into(),
fn_ptr: native_on_floating_surface_created as *mut std::ffi::c_void,
},
jni::NativeMethod {
name: "onFloatingSurfaceDestroyed".into(),
sig: "()V".into(),
fn_ptr: native_on_floating_surface_destroyed as *mut std::ffi::c_void,
},
];
env.register_native_methods(class, &methods)?;
Ok(())
}