#[cfg(target_os = "android")]
use std::sync::OnceLock;
#[cfg(target_os = "android")]
use std::sync::Mutex;
#[cfg(target_os = "android")]
use android_activity::AndroidApp;
#[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<crate::SceneFactory> = OnceLock::new();
#[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")]
fn find_class<'a>(env: &mut jni::JNIEnv<'a>, class_name: &str) -> jni::errors::Result<jni::objects::JClass<'a>> {
if class_name.starts_with("android/") || class_name.starts_with("java/") {
return env.find_class(class_name);
}
let activity = ACTIVITY.get().expect("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 vm = jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _).unwrap();
let activity = jni::objects::JObject::from_raw(app.activity_as_ptr() as *mut _);
let _ = JVM.set(vm);
let mut env = JVM.get().unwrap().attach_current_thread().unwrap();
let _ = ACTIVITY.set(env.new_global_ref(activity).unwrap());
if let Some(class_name) = floating_window_service_class() {
if let Ok(class) = find_class(&mut 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).expect("Failed to register native methods for floating window service");
}
}
}
}
#[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 crate::SceneFactory> {
FLOATING_SCENE_FACTORY.get()
}
#[cfg(target_os = "android")]
pub(crate) fn take_floating_surface() -> Option<jni::objects::GlobalRef> {
FLOATING_SURFACE.lock().unwrap().take()
}
#[cfg(target_os = "android")]
pub fn on_surface_created(env: &jni::JNIEnv, surface: jni::objects::JObject) {
eprintln!("[spot][android] on_surface_created called");
let global_ref = env.new_global_ref(surface).unwrap();
*FLOATING_SURFACE.lock().unwrap() = Some(global_ref);
}
#[cfg(target_os = "android")]
pub fn on_surface_destroyed() {
*FLOATING_SURFACE.lock().unwrap() = None;
}
#[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 mut env = jvm.attach_current_thread().unwrap();
if let Ok(class) = find_class(&mut 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).expect("Failed to register native methods for floating window service");
}
}
}
}
#[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 fn start_service(class_name: &str) {
let Some(jvm) = JVM.get() else { return; };
let Some(activity_ref) = ACTIVITY.get() else { return; };
let mut env = jvm.attach_current_thread().unwrap();
let activity = activity_ref.as_obj();
let intent_class = find_class(&mut env, "android/content/Intent").unwrap();
let service_class = find_class(&mut env, class_name).unwrap();
let intent = env.new_object(
intent_class,
"(Landroid/content/Context;Ljava/lang/Class;)V",
&[(&activity).into(), (&service_class).into()],
).unwrap();
let version_class = find_class(&mut env, "android/os/Build$VERSION").unwrap();
let sdk_int = env.get_static_field(version_class, "SDK_INT", "I").unwrap().i().unwrap();
if sdk_int >= 26 {
env.call_method(&activity, "startForegroundService", "(Landroid/content/Intent;)Landroid/content/ComponentName;", &[(&intent).into()]).unwrap();
} else {
env.call_method(&activity, "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;", &[(&intent).into()]).unwrap();
}
}
#[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 mut env = jvm.attach_current_thread().unwrap();
let activity = activity_ref.as_obj();
let intent_class = find_class(&mut env, "android/content/Intent").unwrap();
let service_class = find_class(&mut env, class_name).unwrap();
let intent = env.new_object(
intent_class,
"(Landroid/content/Context;Ljava/lang/Class;)V",
&[(&activity).into(), (&service_class).into()],
).unwrap();
env.call_method(&activity, "stopService", "(Landroid/content/Intent;)Z", &[(&intent).into()]).unwrap();
}
#[cfg(target_os = "android")]
pub fn set_floating_window_enabled(_enabled: bool) {
}