Skip to main content

accesskit_android/
inject.rs

1// Copyright 2025 The AccessKit Authors. All rights reserved.
2// Licensed under the Apache License, Version 2.0 (found in
3// the LICENSE-APACHE file) or the MIT license (found in
4// the LICENSE-MIT file), at your option.
5
6// Derived from jni-rs
7// Copyright 2016 Prevoty, Inc. and jni-rs contributors
8// Licensed under the Apache License, Version 2.0 (found in
9// the LICENSE-APACHE file) or the MIT license (found in
10// the LICENSE-MIT file), at your option.
11
12use accesskit::{ActionHandler, ActivationHandler, TreeUpdate};
13use jni::{
14    errors::Result,
15    objects::{GlobalRef, JClass, JObject, WeakRef},
16    sys::{jboolean, jfloat, jint, jlong, JNI_FALSE, JNI_TRUE},
17    JNIEnv, JavaVM, NativeMethod,
18};
19use log::debug;
20use std::{
21    collections::BTreeMap,
22    ffi::c_void,
23    fmt::{Debug, Formatter},
24    sync::{
25        atomic::{AtomicI64, Ordering},
26        Arc, Mutex, OnceLock, Weak,
27    },
28};
29
30use crate::{action::PlatformAction, adapter::Adapter, event::QueuedEvents};
31
32struct InnerInjectingAdapter {
33    adapter: Adapter,
34    activation_handler: Box<dyn ActivationHandler + Send>,
35    action_handler: Box<dyn ActionHandler + Send>,
36}
37
38impl Debug for InnerInjectingAdapter {
39    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("InnerInjectingAdapter")
41            .field("adapter", &self.adapter)
42            .field("activation_handler", &"ActivationHandler")
43            .field("action_handler", &"ActionHandler")
44            .finish()
45    }
46}
47
48impl InnerInjectingAdapter {
49    fn create_accessibility_node_info<'local>(
50        &mut self,
51        env: &mut JNIEnv<'local>,
52        host: &JObject,
53        virtual_view_id: jint,
54    ) -> JObject<'local> {
55        self.adapter.create_accessibility_node_info(
56            &mut *self.activation_handler,
57            env,
58            host,
59            virtual_view_id,
60        )
61    }
62
63    fn find_focus<'local>(
64        &mut self,
65        env: &mut JNIEnv<'local>,
66        host: &JObject,
67        focus_type: jint,
68    ) -> JObject<'local> {
69        self.adapter
70            .find_focus(&mut *self.activation_handler, env, host, focus_type)
71    }
72
73    fn perform_action(
74        &mut self,
75        virtual_view_id: jint,
76        action: &PlatformAction,
77    ) -> Option<QueuedEvents> {
78        self.adapter
79            .perform_action(&mut *self.action_handler, virtual_view_id, action)
80    }
81
82    fn on_hover_event(&mut self, action: jint, x: jfloat, y: jfloat) -> Option<QueuedEvents> {
83        self.adapter
84            .on_hover_event(&mut *self.activation_handler, action, x, y)
85    }
86}
87
88static NEXT_HANDLE: AtomicI64 = AtomicI64::new(0);
89static HANDLE_MAP: Mutex<BTreeMap<jlong, Weak<Mutex<InnerInjectingAdapter>>>> =
90    Mutex::new(BTreeMap::new());
91
92fn inner_adapter_from_handle(handle: jlong) -> Option<Arc<Mutex<InnerInjectingAdapter>>> {
93    let handle_map_guard = HANDLE_MAP.lock().unwrap();
94    handle_map_guard.get(&handle).and_then(Weak::upgrade)
95}
96
97static NEXT_CALLBACK_HANDLE: AtomicI64 = AtomicI64::new(0);
98#[allow(clippy::type_complexity)]
99static CALLBACK_MAP: Mutex<
100    BTreeMap<jlong, Box<dyn FnOnce(&mut JNIEnv, &JClass, &JObject) + Send>>,
101> = Mutex::new(BTreeMap::new());
102
103fn post_to_ui_thread(
104    env: &mut JNIEnv,
105    delegate_class: &JClass,
106    host: &JObject,
107    callback: impl FnOnce(&mut JNIEnv, &JClass, &JObject) + Send + 'static,
108) {
109    let handle = NEXT_CALLBACK_HANDLE.fetch_add(1, Ordering::Relaxed);
110    CALLBACK_MAP
111        .lock()
112        .unwrap()
113        .insert(handle, Box::new(callback));
114    let runnable = env
115        .call_static_method(
116            delegate_class,
117            "newCallback",
118            "(Landroid/view/View;J)Ljava/lang/Runnable;",
119            &[host.into(), handle.into()],
120        )
121        .unwrap()
122        .l()
123        .unwrap();
124    env.call_method(
125        host,
126        "post",
127        "(Ljava/lang/Runnable;)Z",
128        &[(&runnable).into()],
129    )
130    .unwrap();
131}
132
133extern "system" fn run_callback<'local>(
134    mut env: JNIEnv<'local>,
135    class: JClass<'local>,
136    host: JObject<'local>,
137    handle: jlong,
138) {
139    let Some(callback) = CALLBACK_MAP.lock().unwrap().remove(&handle) else {
140        return;
141    };
142    callback(&mut env, &class, &host);
143}
144
145extern "system" fn create_accessibility_node_info<'local>(
146    mut env: JNIEnv<'local>,
147    _class: JClass<'local>,
148    adapter_handle: jlong,
149    host: JObject<'local>,
150    virtual_view_id: jint,
151) -> JObject<'local> {
152    let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else {
153        return JObject::null();
154    };
155    let mut inner_adapter = inner_adapter.lock().unwrap();
156    inner_adapter.create_accessibility_node_info(&mut env, &host, virtual_view_id)
157}
158
159extern "system" fn find_focus<'local>(
160    mut env: JNIEnv<'local>,
161    _class: JClass<'local>,
162    adapter_handle: jlong,
163    host: JObject<'local>,
164    focus_type: jint,
165) -> JObject<'local> {
166    let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else {
167        return JObject::null();
168    };
169    let mut inner_adapter = inner_adapter.lock().unwrap();
170    inner_adapter.find_focus(&mut env, &host, focus_type)
171}
172
173extern "system" fn perform_action<'local>(
174    mut env: JNIEnv<'local>,
175    _class: JClass<'local>,
176    adapter_handle: jlong,
177    host: JObject<'local>,
178    virtual_view_id: jint,
179    action: jint,
180    arguments: JObject<'local>,
181) -> jboolean {
182    let Some(action) = PlatformAction::from_java(&mut env, action, &arguments) else {
183        return JNI_FALSE;
184    };
185    let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else {
186        return JNI_FALSE;
187    };
188    let mut inner_adapter = inner_adapter.lock().unwrap();
189    let Some(events) = inner_adapter.perform_action(virtual_view_id, &action) else {
190        return JNI_FALSE;
191    };
192    drop(inner_adapter);
193    events.raise(&mut env, &host);
194    JNI_TRUE
195}
196
197extern "system" fn on_hover_event<'local>(
198    mut env: JNIEnv<'local>,
199    _class: JClass<'local>,
200    adapter_handle: jlong,
201    host: JObject<'local>,
202    action: jint,
203    x: jfloat,
204    y: jfloat,
205) -> jboolean {
206    let Some(inner_adapter) = inner_adapter_from_handle(adapter_handle) else {
207        return JNI_FALSE;
208    };
209    let mut inner_adapter = inner_adapter.lock().unwrap();
210    let Some(events) = inner_adapter.on_hover_event(action, x, y) else {
211        return JNI_FALSE;
212    };
213    drop(inner_adapter);
214    events.raise(&mut env, &host);
215    JNI_TRUE
216}
217
218fn delegate_class(env: &mut JNIEnv) -> &'static JClass<'static> {
219    static CLASS: OnceLock<GlobalRef> = OnceLock::new();
220    let global = CLASS.get_or_init(|| {
221        #[cfg(feature = "embedded-dex")]
222        let class = {
223            let dex_class_loader_class = env
224                .find_class("dalvik/system/InMemoryDexClassLoader")
225                .unwrap();
226            let dex_bytes = include_bytes!("../classes.dex");
227            let dex_buffer = unsafe {
228                env.new_direct_byte_buffer(dex_bytes.as_ptr() as *mut u8, dex_bytes.len())
229            }
230            .unwrap();
231            let dex_class_loader = env
232                .new_object(
233                    &dex_class_loader_class,
234                    "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
235                    &[(&dex_buffer).into(), (&JObject::null()).into()],
236                )
237                .unwrap();
238            let class_name = env.new_string("dev.accesskit.android.Delegate").unwrap();
239            let class_obj = env
240                .call_method(
241                    &dex_class_loader,
242                    "loadClass",
243                    "(Ljava/lang/String;)Ljava/lang/Class;",
244                    &[(&class_name).into()],
245                )
246                .unwrap()
247                .l()
248                .unwrap();
249            JClass::from(class_obj)
250        };
251        #[cfg(not(feature = "embedded-dex"))]
252        let class = env.find_class("dev/accesskit/android/Delegate").unwrap();
253        env.register_native_methods(
254            &class,
255            &[
256                NativeMethod {
257                    name: "runCallback".into(),
258                    sig: "(Landroid/view/View;J)V".into(),
259                    fn_ptr: run_callback as *mut c_void,
260                },
261                NativeMethod {
262                    name: "createAccessibilityNodeInfo".into(),
263                    sig:
264                        "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;"
265                            .into(),
266                    fn_ptr: create_accessibility_node_info as *mut c_void,
267                },
268                NativeMethod {
269                    name: "findFocus".into(),
270                    sig:
271                        "(JLandroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;"
272                            .into(),
273                    fn_ptr: find_focus as *mut c_void,
274                },
275                NativeMethod {
276                    name: "performAction".into(),
277                    sig: "(JLandroid/view/View;IILandroid/os/Bundle;)Z".into(),
278                    fn_ptr: perform_action as *mut c_void,
279                },
280                NativeMethod {
281                    name: "onHoverEvent".into(),
282                    sig: "(JLandroid/view/View;IFF)Z".into(),
283                    fn_ptr: on_hover_event as *mut c_void,
284                },
285            ],
286        )
287        .unwrap();
288        env.new_global_ref(class).unwrap()
289    });
290    global.as_obj().into()
291}
292
293/// High-level AccessKit Android adapter that injects itself into an Android
294/// view without requiring the view class to be modified for accessibility.
295///
296/// This depends on the Java `dev.accesskit.android.Delegate` class, the source
297/// code for which is in this crate's `java` directory. If the `embedded-dex`
298/// feature is enabled, then that class is loaded from a prebuilt `.dex` file
299/// that this crate embeds. Otherwise, it's simply assumed that the class
300/// is in the application package. None of this type's public functions
301/// make assumptions about whether they're called from the Android UI thread.
302/// As such, some requests are posted to the UI thread and handled
303/// asynchronously.
304pub struct InjectingAdapter {
305    vm: JavaVM,
306    delegate_class: &'static JClass<'static>,
307    host: WeakRef,
308    handle: jlong,
309    inner: Arc<Mutex<InnerInjectingAdapter>>,
310}
311
312impl Debug for InjectingAdapter {
313    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
314        f.debug_struct("InnerInjectingAdapter")
315            .field("vm", &self.vm)
316            .field("delegate_class", &self.delegate_class)
317            .field("host", &"WeakRef")
318            .field("handle", &self.handle)
319            .field("inner", &self.inner)
320            .finish()
321    }
322}
323
324impl InjectingAdapter {
325    pub fn new(
326        env: &mut JNIEnv,
327        host: &JObject,
328        activation_handler: impl 'static + ActivationHandler + Send,
329        action_handler: impl 'static + ActionHandler + Send,
330    ) -> Self {
331        let inner = Arc::new(Mutex::new(InnerInjectingAdapter {
332            adapter: Adapter::default(),
333            activation_handler: Box::new(activation_handler),
334            action_handler: Box::new(action_handler),
335        }));
336        let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
337        HANDLE_MAP
338            .lock()
339            .unwrap()
340            .insert(handle, Arc::downgrade(&inner));
341        let delegate_class = delegate_class(env);
342        post_to_ui_thread(
343            env,
344            delegate_class,
345            host,
346            move |env, delegate_class, host| {
347                let prev_delegate = env
348                    .call_method(
349                        host,
350                        "getAccessibilityDelegate",
351                        "()Landroid/view/View$AccessibilityDelegate;",
352                        &[],
353                    )
354                    .unwrap()
355                    .l()
356                    .unwrap();
357                if !prev_delegate.is_null() {
358                    panic!("host already has an accessibility delegate");
359                }
360                let delegate = env
361                    .new_object(delegate_class, "(J)V", &[handle.into()])
362                    .unwrap();
363                env.call_method(
364                    host,
365                    "setAccessibilityDelegate",
366                    "(Landroid/view/View$AccessibilityDelegate;)V",
367                    &[(&delegate).into()],
368                )
369                .unwrap();
370                env.call_method(
371                    host,
372                    "setOnHoverListener",
373                    "(Landroid/view/View$OnHoverListener;)V",
374                    &[(&delegate).into()],
375                )
376                .unwrap();
377            },
378        );
379        Self {
380            vm: env.get_java_vm().unwrap(),
381            delegate_class,
382            host: env.new_weak_ref(host).unwrap().unwrap(),
383            handle,
384            inner,
385        }
386    }
387
388    /// If and only if the tree has been initialized, call the provided function
389    /// and apply the resulting update. Note: If the caller's implementation of
390    /// [`ActivationHandler::request_initial_tree`] initially returned `None`,
391    /// the [`TreeUpdate`] returned by the provided function must contain
392    /// a full tree.
393    pub fn update_if_active(&mut self, update_factory: impl FnOnce() -> TreeUpdate) {
394        let mut env = self.vm.get_env().unwrap();
395        let Some(host) = self.host.upgrade_local(&env).unwrap() else {
396            return;
397        };
398        let mut inner = self.inner.lock().unwrap();
399        let Some(events) = inner.adapter.update_if_active(update_factory) else {
400            return;
401        };
402        drop(inner);
403        post_to_ui_thread(
404            &mut env,
405            self.delegate_class,
406            &host,
407            |env, _delegate_class, host| {
408                events.raise(env, host);
409            },
410        );
411    }
412}
413
414impl Drop for InjectingAdapter {
415    fn drop(&mut self) {
416        fn drop_impl(env: &mut JNIEnv, delegate_class: &JClass, host: &WeakRef) -> Result<()> {
417            let Some(host) = host.upgrade_local(env)? else {
418                return Ok(());
419            };
420            post_to_ui_thread(env, delegate_class, &host, |env, delegate_class, host| {
421                let prev_delegate = env
422                    .call_method(
423                        host,
424                        "getAccessibilityDelegate",
425                        "()Landroid/view/View$AccessibilityDelegate;",
426                        &[],
427                    )
428                    .unwrap()
429                    .l()
430                    .unwrap();
431                if prev_delegate.is_null()
432                    && !env.is_instance_of(&prev_delegate, delegate_class).unwrap()
433                {
434                    return;
435                }
436                let null = JObject::null();
437                env.call_method(
438                    host,
439                    "setAccessibilityDelegate",
440                    "(Landroid/view/View$AccessibilityDelegate;)V",
441                    &[(&null).into()],
442                )
443                .unwrap();
444                env.call_method(
445                    host,
446                    "setOnHoverListener",
447                    "(Landroid/view/View$OnHoverListener;)V",
448                    &[(&null).into()],
449                )
450                .unwrap();
451            });
452            Ok(())
453        }
454
455        let res = match self.vm.get_env() {
456            Ok(mut env) => drop_impl(&mut env, self.delegate_class, &self.host),
457            Err(_) => self
458                .vm
459                .attach_current_thread()
460                .and_then(|mut env| drop_impl(&mut env, self.delegate_class, &self.host)),
461        };
462
463        if let Err(err) = res {
464            debug!("error dropping InjectingAdapter: {:#?}", err);
465        }
466
467        HANDLE_MAP.lock().unwrap().remove(&self.handle);
468    }
469}