#![allow(unsafe_code)]
use std::sync::{
Mutex, OnceLock,
atomic::{AtomicBool, Ordering},
};
use cranpose_app_shell::AppShell;
use cranpose_render_wgpu::WgpuRenderer;
use jni::{
EnvUnowned, jni_sig, jni_str,
objects::{JClass, JObject, JValue},
sys::{jboolean, jfloat, jint},
};
use crate::{
accessibility::{self, AccessibilityElement},
accessibility_publish_policy::AccessibilityPublishPolicy,
android_accessibility_wire::encode_elements,
android_jni::{clear_pending_android_jni_exception, with_android_activity_env},
};
static ACTIVATIONS: OnceLock<Mutex<Vec<(f32, f32)>>> = OnceLock::new();
static CUSTOM_ACTIONS: OnceLock<Mutex<Vec<(i32, usize)>>> = OnceLock::new();
static LOOP_WAKER: Mutex<Option<android_activity::AndroidAppWaker>> = Mutex::new(None);
static PLATFORM_ACCESSIBILITY_ENABLED: AtomicBool = AtomicBool::new(false);
fn accessibility_sync_override() -> Option<bool> {
static OVERRIDE: OnceLock<Option<bool>> = OnceLock::new();
*OVERRIDE.get_or_init(|| match std::env::var("CRANPOSE_A11Y_SYNC").as_deref() {
Ok("0") | Ok("false") | Ok("off") => Some(false),
Ok("1") | Ok("true") | Ok("on") => Some(true),
_ => None,
})
}
fn accessibility_bridge_enabled() -> bool {
accessibility_sync_override()
.unwrap_or_else(|| PLATFORM_ACCESSIBILITY_ENABLED.load(Ordering::Relaxed))
}
pub(crate) fn set_waker(waker: android_activity::AndroidAppWaker) {
*LOOP_WAKER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(waker);
}
fn wake_loop() {
let waker = LOOP_WAKER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
if let Some(waker) = waker {
waker.wake();
}
}
fn activations() -> &'static Mutex<Vec<(f32, f32)>> {
ACTIVATIONS.get_or_init(|| Mutex::new(Vec::new()))
}
fn custom_actions() -> &'static Mutex<Vec<(i32, usize)>> {
CUSTOM_ACTIONS.get_or_init(|| Mutex::new(Vec::new()))
}
pub(crate) fn drain_activations() -> Vec<(f32, f32)> {
std::mem::take(
&mut *activations()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
)
}
pub(crate) fn drain_custom_actions() -> Vec<(i32, usize)> {
std::mem::take(
&mut *custom_actions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
)
}
pub(crate) fn sync(
app: &android_activity::AndroidApp,
shell: &mut AppShell<WgpuRenderer>,
density: f32,
previous: &mut Vec<AccessibilityElement>,
seen_revision: &mut Option<u64>,
policy: &mut AccessibilityPublishPolicy,
) -> Result<(), String> {
if policy.update_enabled(accessibility_bridge_enabled()) {
*seen_revision = None;
}
let now = std::time::Instant::now();
if !policy.try_begin_publish(now) {
return Ok(());
}
let Some(elements) = accessibility::snapshot_if_changed(shell, seen_revision) else {
return Ok(());
};
if elements == *previous {
return Ok(());
}
*previous = elements;
let payload = encode_elements(previous, density);
with_android_activity_env(app, |env, activity| {
let payload = env.new_string(payload).map_err(|error| {
clear_pending_android_jni_exception(env);
format!("failed to encode Android accessibility tree: {error}")
})?;
let payload = JObject::from(payload);
env.call_method(
&activity,
jni_str!("cranposeSetAccessibilityElements"),
jni_sig!("(Ljava/lang/String;)V"),
&[JValue::Object(&payload)],
)
.map_err(|error| {
clear_pending_android_jni_exception(env);
format!("failed to publish Android accessibility tree: {error}")
})?;
Ok(())
})
}
#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnAccessibilityActivate(
_env: EnvUnowned<'_>,
_class: JClass<'_>,
x: jfloat,
y: jfloat,
) {
activations()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push((x, y));
wake_loop();
}
#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnAccessibilityStateChanged(
_env: EnvUnowned<'_>,
_class: JClass<'_>,
enabled: jboolean,
) {
let previous = PLATFORM_ACCESSIBILITY_ENABLED.swap(enabled, Ordering::Relaxed);
if previous != enabled {
wake_loop();
}
}
#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_cranpose_android_CranposeActivity_nativeOnAccessibilityCustomAction(
_env: EnvUnowned<'_>,
_class: JClass<'_>,
virtual_id: jint,
action_index: jint,
) {
if action_index < 0 {
return;
}
custom_actions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push((virtual_id, action_index as usize));
wake_loop();
}