accesskit_android/
adapter.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 the Flutter engine.
7// Copyright 2013 The Flutter Authors. All rights reserved.
8// Use of this source code is governed by a BSD-style license that can be
9// found in the LICENSE.chromium file.
10
11use accesskit::{
12    Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, NodeId,
13    Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, TreeUpdate,
14};
15use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler};
16use jni::{
17    objects::JObject,
18    sys::{jfloat, jint},
19    JNIEnv,
20};
21
22use crate::{
23    action::{PlatformAction, PlatformActionInner},
24    event::{QueuedEvent, QueuedEvents, ScrollDimension},
25    filters::filter,
26    node::{add_action, NodeWrapper},
27    util::*,
28};
29
30fn enqueue_window_content_changed(events: &mut Vec<QueuedEvent>) {
31    events.push(QueuedEvent::WindowContentChanged {
32        virtual_view_id: HOST_VIEW_ID,
33    });
34}
35
36fn enqueue_focus_event_if_applicable(
37    events: &mut Vec<QueuedEvent>,
38    node_id_map: &mut NodeIdMap,
39    node: &Node,
40) {
41    if node.is_root() && node.role() == Role::Window {
42        return;
43    }
44    let id = node_id_map.get_or_create_java_id(node);
45    events.push(QueuedEvent::Simple {
46        virtual_view_id: id,
47        event_type: EVENT_VIEW_FOCUSED,
48    });
49}
50
51struct AdapterChangeHandler<'a> {
52    events: &'a mut Vec<QueuedEvent>,
53    node_id_map: &'a mut NodeIdMap,
54    enqueued_window_content_changed: bool,
55}
56
57impl<'a> AdapterChangeHandler<'a> {
58    fn new(events: &'a mut Vec<QueuedEvent>, node_id_map: &'a mut NodeIdMap) -> Self {
59        Self {
60            events,
61            node_id_map,
62            enqueued_window_content_changed: false,
63        }
64    }
65}
66
67impl AdapterChangeHandler<'_> {
68    fn enqueue_window_content_changed_if_needed(&mut self) {
69        if self.enqueued_window_content_changed {
70            return;
71        }
72        enqueue_window_content_changed(self.events);
73        self.enqueued_window_content_changed = true;
74    }
75}
76
77impl TreeChangeHandler for AdapterChangeHandler<'_> {
78    fn node_added(&mut self, _node: &Node) {
79        self.enqueue_window_content_changed_if_needed();
80        // TODO: live regions?
81    }
82
83    fn node_updated(&mut self, old_node: &Node, new_node: &Node) {
84        self.enqueue_window_content_changed_if_needed();
85        if filter(new_node) != FilterResult::Include {
86            return;
87        }
88        let old_wrapper = NodeWrapper(old_node);
89        let new_wrapper = NodeWrapper(new_node);
90        let old_text = old_wrapper.text();
91        let new_text = new_wrapper.text();
92        if old_text != new_text {
93            let id = self.node_id_map.get_or_create_java_id(new_node);
94            self.events.push(QueuedEvent::TextChanged {
95                virtual_view_id: id,
96                old: old_text.unwrap_or_else(String::new),
97                new: new_text.clone().unwrap_or_else(String::new),
98            });
99        }
100        if old_node.raw_text_selection() != new_node.raw_text_selection()
101            || (new_node.raw_text_selection().is_some()
102                && old_node.is_focused() != new_node.is_focused())
103        {
104            if let Some((start, end)) = new_wrapper.text_selection() {
105                if let Some(text) = new_text {
106                    let id = self.node_id_map.get_or_create_java_id(new_node);
107                    self.events.push(QueuedEvent::TextSelectionChanged {
108                        virtual_view_id: id,
109                        text,
110                        start: start as jint,
111                        end: end as jint,
112                    });
113                }
114            }
115        }
116        let scroll_x = old_wrapper
117            .scroll_x()
118            .zip(new_wrapper.scroll_x())
119            .and_then(|(old, new)| {
120                (new != old).then(|| ScrollDimension {
121                    current: new,
122                    delta: new - old,
123                    max: new_wrapper.max_scroll_x(),
124                })
125            });
126        let scroll_y = old_wrapper
127            .scroll_y()
128            .zip(new_wrapper.scroll_y())
129            .and_then(|(old, new)| {
130                (new != old).then(|| ScrollDimension {
131                    current: new,
132                    delta: new - old,
133                    max: new_wrapper.max_scroll_y(),
134                })
135            });
136        if scroll_x.is_some() || scroll_y.is_some() {
137            let id = self.node_id_map.get_or_create_java_id(new_node);
138            self.events.push(QueuedEvent::Scrolled {
139                virtual_view_id: id,
140                x: scroll_x,
141                y: scroll_y,
142            });
143        }
144        // TODO: other events
145    }
146
147    fn focus_moved(&mut self, _old_node: Option<&Node>, new_node: Option<&Node>) {
148        if let Some(new_node) = new_node {
149            enqueue_focus_event_if_applicable(self.events, self.node_id_map, new_node);
150        }
151    }
152
153    fn node_removed(&mut self, _node: &Node) {
154        self.enqueue_window_content_changed_if_needed();
155        // TODO: other events?
156    }
157}
158
159const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0);
160
161#[derive(Debug, Default)]
162enum State {
163    #[default]
164    Inactive,
165    Placeholder(Tree),
166    Active(Tree),
167}
168
169impl State {
170    fn get_or_init_tree<H: ActivationHandler + ?Sized>(
171        &mut self,
172        activation_handler: &mut H,
173    ) -> &Tree {
174        match self {
175            Self::Inactive => {
176                *self = match activation_handler.request_initial_tree() {
177                    Some(initial_state) => Self::Active(Tree::new(initial_state, true)),
178                    None => {
179                        let placeholder_update = TreeUpdate {
180                            nodes: vec![(PLACEHOLDER_ROOT_ID, NodeData::new(Role::Window))],
181                            tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)),
182                            focus: PLACEHOLDER_ROOT_ID,
183                        };
184                        Self::Placeholder(Tree::new(placeholder_update, true))
185                    }
186                };
187                self.get_or_init_tree(activation_handler)
188            }
189            Self::Placeholder(tree) => tree,
190            Self::Active(tree) => tree,
191        }
192    }
193
194    fn get_full_tree(&mut self) -> Option<&mut Tree> {
195        match self {
196            Self::Inactive => None,
197            Self::Placeholder(_) => None,
198            Self::Active(tree) => Some(tree),
199        }
200    }
201}
202
203fn update_tree(
204    events: &mut Vec<QueuedEvent>,
205    node_id_map: &mut NodeIdMap,
206    tree: &mut Tree,
207    update: TreeUpdate,
208) {
209    let mut handler = AdapterChangeHandler::new(events, node_id_map);
210    tree.update_and_process_changes(update, &mut handler);
211}
212
213/// Low-level AccessKit adapter for Android.
214///
215/// This layer provides maximum flexibility in the application threading
216/// model, the interface between Java and native code, and the implementation
217/// of action callbacks, at the expense of requiring its caller to provide
218/// glue code. For a higher-level implementation built on this type, see
219/// [`InjectingAdapter`].
220///
221/// [`InjectingAdapter`]: crate::InjectingAdapter
222#[derive(Debug, Default)]
223pub struct Adapter {
224    node_id_map: NodeIdMap,
225    state: State,
226    accessibility_focus: Option<jint>,
227    hover_target: Option<jint>,
228}
229
230impl Adapter {
231    /// If and only if the tree has been initialized, call the provided function
232    /// and apply the resulting update. Note: If the caller's implementation of
233    /// [`ActivationHandler::request_initial_tree`] initially returned `None`,
234    /// the [`TreeUpdate`] returned by the provided function must contain
235    /// a full tree.
236    ///
237    /// If a [`QueuedEvents`] instance is returned, the caller must call
238    /// [`QueuedEvents::raise`] on it.
239    ///
240    /// This method may be safely called on any thread, but refer to
241    /// [`QueuedEvents::raise`] for restrictions on the context in which
242    /// it should be called.
243    pub fn update_if_active(
244        &mut self,
245        update_factory: impl FnOnce() -> TreeUpdate,
246    ) -> Option<QueuedEvents> {
247        match &mut self.state {
248            State::Inactive => None,
249            State::Placeholder(_) => {
250                let tree = Tree::new(update_factory(), true);
251                let mut events = Vec::new();
252                enqueue_window_content_changed(&mut events);
253                let state = tree.state();
254                if let Some(focus) = state.focus() {
255                    enqueue_focus_event_if_applicable(&mut events, &mut self.node_id_map, &focus);
256                }
257                self.state = State::Active(tree);
258                Some(QueuedEvents(events))
259            }
260            State::Active(tree) => {
261                let mut events = Vec::new();
262                update_tree(&mut events, &mut self.node_id_map, tree, update_factory());
263                Some(QueuedEvents(events))
264            }
265        }
266    }
267
268    /// Create an `AccessibilityNodeInfo` for the AccessKit node
269    /// corresponding to the given virtual view ID. Returns null if
270    /// there is no such node.
271    ///
272    /// The `host` parameter is the Android view for this adapter.
273    /// It must be an instance of `android.view.View` or a subclass.
274    pub fn create_accessibility_node_info<'local, H: ActivationHandler + ?Sized>(
275        &mut self,
276        activation_handler: &mut H,
277        env: &mut JNIEnv<'local>,
278        host: &JObject,
279        virtual_view_id: jint,
280    ) -> JObject<'local> {
281        let tree = self.state.get_or_init_tree(activation_handler);
282        let tree_state = tree.state();
283        let node = if virtual_view_id == HOST_VIEW_ID {
284            tree_state.root()
285        } else {
286            let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else {
287                return JObject::null();
288            };
289            let Some(node) = tree_state.node_by_id(accesskit_id) else {
290                return JObject::null();
291            };
292            node
293        };
294
295        let node_info_class = env
296            .find_class("android/view/accessibility/AccessibilityNodeInfo")
297            .unwrap();
298        let node_info = env
299            .call_static_method(
300                &node_info_class,
301                "obtain",
302                "(Landroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;",
303                &[host.into(), virtual_view_id.into()],
304            )
305            .unwrap()
306            .l()
307            .unwrap();
308
309        let package_name = get_package_name(env, host);
310        env.call_method(
311            &node_info,
312            "setPackageName",
313            "(Ljava/lang/CharSequence;)V",
314            &[(&package_name).into()],
315        )
316        .unwrap();
317
318        let wrapper = NodeWrapper(&node);
319        wrapper.populate_node_info(env, host, &mut self.node_id_map, &node_info);
320
321        let is_accessibility_focus = self.accessibility_focus == Some(virtual_view_id);
322        env.call_method(
323            &node_info,
324            "setAccessibilityFocused",
325            "(Z)V",
326            &[is_accessibility_focus.into()],
327        )
328        .unwrap();
329        add_action(
330            env,
331            &node_info,
332            if is_accessibility_focus {
333                ACTION_CLEAR_ACCESSIBILITY_FOCUS
334            } else {
335                ACTION_ACCESSIBILITY_FOCUS
336            },
337        );
338
339        node_info
340    }
341
342    /// Create an `AccessibilityNodeInfo` for the AccessKit node
343    /// with the given focus type. Returns null if there is no such node.
344    ///
345    /// The `host` parameter is the Android view for this adapter.
346    /// It must be an instance of `android.view.View` or a subclass.
347    pub fn find_focus<'local, H: ActivationHandler + ?Sized>(
348        &mut self,
349        activation_handler: &mut H,
350        env: &mut JNIEnv<'local>,
351        host: &JObject,
352        focus_type: jint,
353    ) -> JObject<'local> {
354        let virtual_view_id = match focus_type {
355            FOCUS_INPUT => {
356                let tree = self.state.get_or_init_tree(activation_handler);
357                let tree_state = tree.state();
358                let node = tree_state.focus_in_tree();
359                self.node_id_map.get_or_create_java_id(&node)
360            }
361            FOCUS_ACCESSIBILITY => {
362                let Some(id) = self.accessibility_focus else {
363                    return JObject::null();
364                };
365                id
366            }
367            _ => return JObject::null(),
368        };
369        self.create_accessibility_node_info(activation_handler, env, host, virtual_view_id)
370    }
371
372    fn perform_simple_action<H: ActionHandler + ?Sized>(
373        &mut self,
374        action_handler: &mut H,
375        virtual_view_id: jint,
376        action: jint,
377    ) -> Option<QueuedEvents> {
378        let tree = self.state.get_full_tree()?;
379        let tree_state = tree.state();
380        let target = if virtual_view_id == HOST_VIEW_ID {
381            tree_state.root_id()
382        } else {
383            self.node_id_map.get_accesskit_id(virtual_view_id)?
384        };
385        let mut events = Vec::new();
386        let request = match action {
387            ACTION_CLICK => ActionRequest {
388                action: {
389                    let node = tree_state.node_by_id(target).unwrap();
390                    if node.is_focusable(&filter)
391                        && !node.is_focused()
392                        && !node.is_clickable(&filter)
393                    {
394                        Action::Focus
395                    } else {
396                        Action::Click
397                    }
398                },
399                target,
400                data: None,
401            },
402            ACTION_FOCUS => ActionRequest {
403                action: Action::Focus,
404                target,
405                data: None,
406            },
407            ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest {
408                action: {
409                    let node = tree_state.node_by_id(target).unwrap();
410                    if let Some(orientation) = node.orientation() {
411                        match orientation {
412                            Orientation::Horizontal => {
413                                if action == ACTION_SCROLL_BACKWARD {
414                                    Action::ScrollLeft
415                                } else {
416                                    Action::ScrollRight
417                                }
418                            }
419                            Orientation::Vertical => {
420                                if action == ACTION_SCROLL_BACKWARD {
421                                    Action::ScrollUp
422                                } else {
423                                    Action::ScrollDown
424                                }
425                            }
426                        }
427                    } else if action == ACTION_SCROLL_BACKWARD {
428                        if node.supports_action(Action::ScrollUp, &filter) {
429                            Action::ScrollUp
430                        } else {
431                            Action::ScrollLeft
432                        }
433                    } else if node.supports_action(Action::ScrollDown, &filter) {
434                        Action::ScrollDown
435                    } else {
436                        Action::ScrollRight
437                    }
438                },
439                target,
440                data: Some(ActionData::ScrollUnit(ScrollUnit::Page)),
441            },
442            ACTION_ACCESSIBILITY_FOCUS => {
443                self.accessibility_focus = Some(virtual_view_id);
444                events.push(QueuedEvent::InvalidateHost);
445                events.push(QueuedEvent::Simple {
446                    virtual_view_id,
447                    event_type: EVENT_VIEW_ACCESSIBILITY_FOCUSED,
448                });
449                return Some(QueuedEvents(events));
450            }
451            ACTION_CLEAR_ACCESSIBILITY_FOCUS => {
452                if self.accessibility_focus == Some(virtual_view_id) {
453                    self.accessibility_focus = None;
454                }
455                events.push(QueuedEvent::InvalidateHost);
456                events.push(QueuedEvent::Simple {
457                    virtual_view_id,
458                    event_type: EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
459                });
460                return Some(QueuedEvents(events));
461            }
462            _ => {
463                return None;
464            }
465        };
466        action_handler.do_action(request);
467        if action == ACTION_CLICK {
468            events.push(QueuedEvent::Simple {
469                virtual_view_id,
470                event_type: EVENT_VIEW_CLICKED,
471            });
472        }
473        Some(QueuedEvents(events))
474    }
475
476    fn set_text_selection_common<H: ActionHandler + ?Sized, F, Extra>(
477        &mut self,
478        action_handler: &mut H,
479        events: &mut Vec<QueuedEvent>,
480        virtual_view_id: jint,
481        selection_factory: F,
482    ) -> Option<Extra>
483    where
484        for<'a> F: FnOnce(&'a Node<'a>) -> Option<(TextPosition<'a>, TextPosition<'a>, Extra)>,
485    {
486        let tree = self.state.get_full_tree()?;
487        let tree_state = tree.state();
488        let node = if virtual_view_id == HOST_VIEW_ID {
489            tree_state.root()
490        } else {
491            let id = self.node_id_map.get_accesskit_id(virtual_view_id)?;
492            tree_state.node_by_id(id).unwrap()
493        };
494        let target = node.id();
495        // TalkBack expects the text selection change to take effect
496        // immediately, so we optimistically update the node.
497        // But don't be *too* optimistic.
498        if !node.supports_action(Action::SetTextSelection, &filter) {
499            return None;
500        }
501        let (anchor, focus, extra) = selection_factory(&node)?;
502        let selection = TextSelection {
503            anchor: anchor.to_raw(),
504            focus: focus.to_raw(),
505        };
506        let mut new_node = node.data().clone();
507        new_node.set_text_selection(selection);
508        let update = TreeUpdate {
509            nodes: vec![(node.id(), new_node)],
510            tree: None,
511            focus: tree_state.focus_id_in_tree(),
512        };
513        update_tree(events, &mut self.node_id_map, tree, update);
514        let request = ActionRequest {
515            target,
516            action: Action::SetTextSelection,
517            data: Some(ActionData::SetTextSelection(selection)),
518        };
519        action_handler.do_action(request);
520        Some(extra)
521    }
522
523    fn set_text_selection<H: ActionHandler + ?Sized>(
524        &mut self,
525        action_handler: &mut H,
526        virtual_view_id: jint,
527        anchor: jint,
528        focus: jint,
529    ) -> Option<QueuedEvents> {
530        let mut events = Vec::new();
531        self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
532            let anchor = usize::try_from(anchor).ok()?;
533            let anchor = node.text_position_from_global_utf16_index(anchor)?;
534            let focus = usize::try_from(focus).ok()?;
535            let focus = node.text_position_from_global_utf16_index(focus)?;
536            Some((anchor, focus, ()))
537        })?;
538        Some(QueuedEvents(events))
539    }
540
541    fn collapse_text_selection<H: ActionHandler + ?Sized>(
542        &mut self,
543        action_handler: &mut H,
544        virtual_view_id: jint,
545    ) -> Option<QueuedEvents> {
546        let mut events = Vec::new();
547        self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
548            node.text_selection_focus().map(|pos| (pos, pos, ()))
549        })?;
550        Some(QueuedEvents(events))
551    }
552
553    fn traverse_text<H: ActionHandler + ?Sized>(
554        &mut self,
555        action_handler: &mut H,
556        virtual_view_id: jint,
557        granularity: jint,
558        forward: bool,
559        extend_selection: bool,
560    ) -> Option<QueuedEvents> {
561        let mut events = Vec::new();
562        let (segment_start, segment_end) =
563            self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
564                let current = node.text_selection_focus().unwrap_or_else(|| {
565                    let range = node.document_range();
566                    if forward {
567                        range.start()
568                    } else {
569                        range.end()
570                    }
571                });
572                if (forward && current.is_document_end())
573                    || (!forward && current.is_document_start())
574                {
575                    return None;
576                }
577                let current = if forward {
578                    current.biased_to_start()
579                } else {
580                    current.biased_to_end()
581                };
582                let (segment_start, segment_end) = match granularity {
583                    MOVEMENT_GRANULARITY_CHARACTER => {
584                        if forward {
585                            (current, current.forward_to_character_end())
586                        } else {
587                            (current.backward_to_character_start(), current)
588                        }
589                    }
590                    MOVEMENT_GRANULARITY_WORD => {
591                        if forward {
592                            let start = if current.is_word_start() {
593                                current
594                            } else {
595                                let start = current.forward_to_word_start();
596                                if start.is_document_end() {
597                                    return None;
598                                }
599                                start
600                            };
601                            (start, start.forward_to_word_end())
602                        } else {
603                            let end = if current.is_line_end() || current.is_word_start() {
604                                current
605                            } else {
606                                let end = current.backward_to_word_start().biased_to_end();
607                                if end.is_document_start() {
608                                    return None;
609                                }
610                                end
611                            };
612                            (end.backward_to_word_start(), end)
613                        }
614                    }
615                    MOVEMENT_GRANULARITY_LINE => {
616                        if forward {
617                            let start = if current.is_line_start() {
618                                current
619                            } else {
620                                let start = current.forward_to_line_start();
621                                if start.is_document_end() {
622                                    return None;
623                                }
624                                start
625                            };
626                            (start, start.forward_to_line_end())
627                        } else {
628                            let end = if current.is_line_end() {
629                                current
630                            } else {
631                                let end = current.backward_to_line_start().biased_to_end();
632                                if end.is_document_start() {
633                                    return None;
634                                }
635                                end
636                            };
637                            (end.backward_to_line_start(), end)
638                        }
639                    }
640                    MOVEMENT_GRANULARITY_PARAGRAPH => {
641                        if forward {
642                            let mut start = current;
643                            while start.is_paragraph_separator() {
644                                start = start.forward_to_paragraph_start();
645                            }
646                            if start.is_document_end() {
647                                return None;
648                            }
649                            let mut end = start.forward_to_paragraph_end();
650                            let prev = end.backward_to_character_start();
651                            if prev.is_paragraph_separator() {
652                                end = prev;
653                            }
654                            (start, end)
655                        } else {
656                            let mut end = current;
657                            while !end.is_document_start()
658                                && end.backward_to_character_start().is_paragraph_separator()
659                            {
660                                end = end.backward_to_character_start();
661                            }
662                            if end.is_document_start() {
663                                return None;
664                            }
665                            (end.backward_to_paragraph_start(), end)
666                        }
667                    }
668                    _ => {
669                        return None;
670                    }
671                };
672                if segment_start == segment_end {
673                    return None;
674                }
675                let focus = if forward { segment_end } else { segment_start };
676                let anchor = if extend_selection {
677                    node.text_selection_anchor().unwrap_or({
678                        if forward {
679                            segment_start
680                        } else {
681                            segment_end
682                        }
683                    })
684                } else {
685                    focus
686                };
687                Some((
688                    anchor,
689                    focus,
690                    (
691                        segment_start.to_global_utf16_index(),
692                        segment_end.to_global_utf16_index(),
693                    ),
694                ))
695            })?;
696        events.push(QueuedEvent::TextTraversed {
697            virtual_view_id,
698            granularity,
699            forward,
700            segment_start: segment_start as jint,
701            segment_end: segment_end as jint,
702        });
703        Some(QueuedEvents(events))
704    }
705
706    /// Perform the specified accessibility action.
707    ///
708    /// If a [`QueuedEvents`] instance is returned, the caller must call
709    /// [`QueuedEvents::raise`] on it, and the Java `performAction` method
710    /// must return `true`. Otherwise, the Java `performAction` method
711    /// must either handle the action some other way or return `false`.
712    ///
713    /// This method may be safely called on any thread, but refer to
714    /// [`QueuedEvents::raise`] for restrictions on the context in which
715    /// it should be called.
716    pub fn perform_action<H: ActionHandler + ?Sized>(
717        &mut self,
718        action_handler: &mut H,
719        virtual_view_id: jint,
720        action: &PlatformAction,
721    ) -> Option<QueuedEvents> {
722        match action.0 {
723            PlatformActionInner::Simple { action } => {
724                self.perform_simple_action(action_handler, virtual_view_id, action)
725            }
726            PlatformActionInner::SetTextSelection { anchor, focus } => {
727                self.set_text_selection(action_handler, virtual_view_id, anchor, focus)
728            }
729            PlatformActionInner::CollapseTextSelection => {
730                self.collapse_text_selection(action_handler, virtual_view_id)
731            }
732            PlatformActionInner::TraverseText {
733                granularity,
734                forward,
735                extend_selection,
736            } => self.traverse_text(
737                action_handler,
738                virtual_view_id,
739                granularity,
740                forward,
741                extend_selection,
742            ),
743        }
744    }
745
746    fn virtual_view_at_point<H: ActivationHandler + ?Sized>(
747        &mut self,
748        activation_handler: &mut H,
749        x: jfloat,
750        y: jfloat,
751    ) -> Option<jint> {
752        let tree = self.state.get_or_init_tree(activation_handler);
753        let tree_state = tree.state();
754        let root = tree_state.root();
755        let point = Point::new(x.into(), y.into());
756        let point = root.transform().inverse() * point;
757        let node = root.node_at_point(point, &filter)?;
758        Some(self.node_id_map.get_or_create_java_id(&node))
759    }
760
761    /// Handle the provided hover event.
762    ///
763    /// The `action`, `x`, and `y` parameters must be retrieved from
764    /// the corresponding properties on an Android motion event. These
765    /// parameters are passed individually so you can use either a Java
766    /// or NDK event.
767    ///
768    /// If a [`QueuedEvents`] instance is returned, the caller must call
769    /// [`QueuedEvents::raise`] on it, and if using Java, the event handler
770    /// must return `true`. Otherwise, if using Java, the event handler
771    /// must either handle the event some other way or return `false`.
772    ///
773    /// This method may be safely called on any thread, but refer to
774    /// [`QueuedEvents::raise`] for restrictions on the context in which
775    /// it should be called.
776    pub fn on_hover_event<H: ActivationHandler + ?Sized>(
777        &mut self,
778        activation_handler: &mut H,
779        action: jint,
780        x: jfloat,
781        y: jfloat,
782    ) -> Option<QueuedEvents> {
783        let mut events = Vec::new();
784        match action {
785            MOTION_ACTION_HOVER_ENTER | MOTION_ACTION_HOVER_MOVE => {
786                let new_id = self.virtual_view_at_point(activation_handler, x, y);
787                if self.hover_target != new_id {
788                    if let Some(virtual_view_id) = new_id {
789                        events.push(QueuedEvent::Simple {
790                            virtual_view_id,
791                            event_type: EVENT_VIEW_HOVER_ENTER,
792                        });
793                    }
794                    if let Some(virtual_view_id) = self.hover_target {
795                        events.push(QueuedEvent::Simple {
796                            virtual_view_id,
797                            event_type: EVENT_VIEW_HOVER_EXIT,
798                        });
799                    }
800                    self.hover_target = new_id;
801                }
802            }
803            MOTION_ACTION_HOVER_EXIT => {
804                if let Some(virtual_view_id) = self.hover_target.take() {
805                    events.push(QueuedEvent::Simple {
806                        virtual_view_id,
807                        event_type: EVENT_VIEW_HOVER_EXIT,
808                    });
809                }
810            }
811            _ => return None,
812        }
813        Some(QueuedEvents(events))
814    }
815}