1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.rotary;
17 
18 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
19 import static android.view.View.FOCUS_DOWN;
20 import static android.view.View.FOCUS_LEFT;
21 import static android.view.View.FOCUS_RIGHT;
22 import static android.view.View.FOCUS_UP;
23 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD;
24 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
25 import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_INPUT;
26 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_APPLICATION;
27 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_INPUT_METHOD;
28 
29 import android.content.pm.PackageManager;
30 import android.graphics.Rect;
31 import android.view.Display;
32 import android.view.View;
33 import android.view.accessibility.AccessibilityNodeInfo;
34 import android.view.accessibility.AccessibilityWindowInfo;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.car.ui.FocusArea;
41 import com.android.car.ui.FocusParkingView;
42 import com.android.internal.util.dump.DualDumpOutputStream;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.function.Predicate;
47 
48 /**
49  * A helper class used for finding the next focusable node when the rotary controller is rotated or
50  * nudged.
51  */
52 class Navigator {
53 
54     @NonNull
55     private NodeCopier mNodeCopier = new NodeCopier();
56 
57     @NonNull
58     private final TreeTraverser mTreeTraverser = new TreeTraverser();
59 
60     @NonNull
61     @VisibleForTesting
62     final SurfaceViewHelper mSurfaceViewHelper = new SurfaceViewHelper();
63 
64     private final int mHunLeft;
65     private final int mHunRight;
66 
67     @View.FocusRealDirection
68     private int mHunNudgeDirection;
69 
70     @NonNull
71     private final Rect mAppWindowBounds;
72 
73     private int mAppWindowTaskId = INVALID_TASK_ID;
74 
Navigator(int displayWidth, int displayHeight, int hunLeft, int hunRight, boolean showHunOnBottom)75     Navigator(int displayWidth, int displayHeight, int hunLeft, int hunRight,
76             boolean showHunOnBottom) {
77         mHunLeft = hunLeft;
78         mHunRight = hunRight;
79         mHunNudgeDirection = showHunOnBottom ? FOCUS_DOWN : FOCUS_UP;
80         mAppWindowBounds = new Rect(0, 0, displayWidth, displayHeight);
81     }
82 
83     @VisibleForTesting
Navigator()84     Navigator() {
85         this(0, 0, 0, 0, false);
86     }
87 
88     /**
89      * Updates {@link #mAppWindowTaskId} if {@code window} is a full-screen app window on the
90      * default display.
91      */
updateAppWindowTaskId(@onNull AccessibilityWindowInfo window)92     void updateAppWindowTaskId(@NonNull AccessibilityWindowInfo window) {
93         if (window.getType() == TYPE_APPLICATION
94                 && window.getDisplayId() == Display.DEFAULT_DISPLAY) {
95             Rect windowBounds = new Rect();
96             window.getBoundsInScreen(windowBounds);
97             if (mAppWindowBounds.equals(windowBounds)) {
98                 mAppWindowTaskId = window.getTaskId();
99                 L.d("Task ID of app window: " + mAppWindowTaskId);
100             }
101         }
102     }
103 
104     /** Initializes the package name of the host app. */
initHostApp(@onNull PackageManager packageManager)105     void initHostApp(@NonNull PackageManager packageManager) {
106         mSurfaceViewHelper.initHostApp(packageManager);
107     }
108 
109     /** Clears the package name of the host app if the given {@code packageName} matches. */
clearHostApp(@onNull String packageName)110     void clearHostApp(@NonNull String packageName) {
111         mSurfaceViewHelper.clearHostApp(packageName);
112     }
113 
114     /** Returns whether it supports AAOS template apps. */
supportTemplateApp()115     boolean supportTemplateApp() {
116         return mSurfaceViewHelper.supportTemplateApp();
117     }
118 
119     /** Adds the package name of the client app. */
addClientApp(@onNull CharSequence clientAppPackageName)120     void addClientApp(@NonNull CharSequence clientAppPackageName) {
121         mSurfaceViewHelper.addClientApp(clientAppPackageName);
122     }
123 
124     /** Returns whether the given {@code node} represents a view of the host app. */
isHostNode(@onNull AccessibilityNodeInfo node)125     boolean isHostNode(@NonNull AccessibilityNodeInfo node) {
126         return mSurfaceViewHelper.isHostNode(node);
127     }
128 
129     /** Returns whether the given {@code node} represents a view of the client app. */
isClientNode(@onNull AccessibilityNodeInfo node)130     boolean isClientNode(@NonNull AccessibilityNodeInfo node) {
131         return mSurfaceViewHelper.isClientNode(node);
132     }
133 
134     @Nullable
findHunWindow(@onNull List<AccessibilityWindowInfo> windows)135     AccessibilityWindowInfo findHunWindow(@NonNull List<AccessibilityWindowInfo> windows) {
136         for (AccessibilityWindowInfo window : windows) {
137             if (isHunWindow(window)) {
138                 return window;
139             }
140         }
141         return null;
142     }
143 
144     /**
145      * Returns the target focusable for a rotate. The caller is responsible for recycling the node
146      * in the result.
147      *
148      * <p>Limits navigation to focusable views within a scrollable container's viewport, if any.
149      *
150      * @param sourceNode    the current focus
151      * @param direction     rotate direction, must be {@link View#FOCUS_FORWARD} or {@link
152      *                      View#FOCUS_BACKWARD}
153      * @param rotationCount the number of "ticks" to rotate. Only count nodes that can take focus
154      *                      (visible, focusable and enabled). If {@code skipNode} is encountered, it
155      *                      isn't counted.
156      * @return a FindRotateTargetResult containing a node and a count of the number of times the
157      *         search advanced to another node. The node represents a focusable view in the given
158      *         {@code direction} from the current focus within the same {@link FocusArea}. If the
159      *         first or last view is reached before counting up to {@code rotationCount}, the first
160      *         or last view is returned. However, if there are no views that can take focus in the
161      *         given {@code direction}, {@code null} is returned.
162      */
163     @Nullable
findRotateTarget( @onNull AccessibilityNodeInfo sourceNode, int direction, int rotationCount)164     FindRotateTargetResult findRotateTarget(
165             @NonNull AccessibilityNodeInfo sourceNode, int direction, int rotationCount) {
166         int advancedCount = 0;
167         AccessibilityNodeInfo currentFocusArea = getAncestorFocusArea(sourceNode);
168         AccessibilityNodeInfo candidate = copyNode(sourceNode);
169         AccessibilityNodeInfo target = null;
170         while (advancedCount < rotationCount) {
171             AccessibilityNodeInfo nextCandidate = null;
172             // Virtual View hierarchies like WebViews and ComposeViews do not support focusSearch().
173             AccessibilityNodeInfo virtualViewAncestor = findVirtualViewAncestor(candidate);
174             if (virtualViewAncestor != null) {
175                 nextCandidate =
176                     findNextFocusableInVirtualRoot(virtualViewAncestor, candidate, direction);
177             }
178             if (nextCandidate == null) {
179                 // If we aren't in a virtual node hierarchy, or there aren't any more focusable
180                 // nodes within the virtual node hierarchy, use focusSearch().
181                 nextCandidate = candidate.focusSearch(direction);
182             }
183             AccessibilityNodeInfo candidateFocusArea =
184                     nextCandidate == null ? null : getAncestorFocusArea(nextCandidate);
185 
186             // Only advance to nextCandidate if:
187             // 1. it's in the same focus area,
188             // 2. and it isn't a FocusParkingView (this is to prevent wrap-around when there is only
189             //    one focus area in the window, including when the root node is treated as a focus
190             //    area),
191             // 3. and nextCandidate is different from candidate (if sourceNode is the first
192             //    focusable node in the window, searching backward will return sourceNode itself).
193             if (nextCandidate != null && currentFocusArea != null
194                     && currentFocusArea.equals(candidateFocusArea)
195                     && !Utils.isFocusParkingView(nextCandidate)
196                     && !nextCandidate.equals(candidate)) {
197                 // We need to skip nextTargetNode if:
198                 // 1. it can't perform focus action (focusSearch() may return a node with zero
199                 //    width and height),
200                 // 2. or it is a scrollable container but it shouldn't be scrolled (i.e., it is not
201                 //    scrollable, or its descendants can take focus).
202                 //    When we want to focus on its element directly, we'll skip the container. When
203                 //    we want to focus on container and scroll it, we won't skip the container.
204                 if (!Utils.canPerformFocus(nextCandidate)
205                         || (Utils.isScrollableContainer(nextCandidate)
206                             && !Utils.canScrollableContainerTakeFocus(nextCandidate))) {
207                     Utils.recycleNode(candidate);
208                     Utils.recycleNode(candidateFocusArea);
209                     candidate = nextCandidate;
210                     continue;
211                 }
212 
213                 // If we're navigating in a scrollable container that can scroll in the specified
214                 // direction and the next candidate is off-screen or there are no more focusable
215                 // views within the scrollable container, stop navigating so that any remaining
216                 // detents are used for scrolling.
217                 AccessibilityNodeInfo scrollableContainer = findScrollableContainer(candidate);
218                 AccessibilityNodeInfo.AccessibilityAction scrollAction =
219                         direction == View.FOCUS_FORWARD
220                                 ? ACTION_SCROLL_FORWARD
221                                 : ACTION_SCROLL_BACKWARD;
222                 if (scrollableContainer != null
223                         && scrollableContainer.getActionList().contains(scrollAction)
224                         && (!Utils.isDescendant(scrollableContainer, nextCandidate)
225                                 || Utils.getBoundsInScreen(nextCandidate).isEmpty())) {
226                     Utils.recycleNode(nextCandidate);
227                     Utils.recycleNode(candidateFocusArea);
228                     break;
229                 }
230                 Utils.recycleNode(scrollableContainer);
231 
232                 Utils.recycleNode(candidate);
233                 Utils.recycleNode(candidateFocusArea);
234                 candidate = nextCandidate;
235                 Utils.recycleNode(target);
236                 target = copyNode(candidate);
237                 advancedCount++;
238             } else {
239                 Utils.recycleNode(nextCandidate);
240                 Utils.recycleNode(candidateFocusArea);
241                 break;
242             }
243         }
244         candidate.recycle();
245         if (sourceNode.equals(target)) {
246             L.e("Wrap-around on the same node");
247             target.recycle();
248             return null;
249         }
250         return target == null ? null : new FindRotateTargetResult(target, advancedCount);
251     }
252 
253     /** Sets a NodeCopier instance for testing. */
254     @VisibleForTesting
setNodeCopier(@onNull NodeCopier nodeCopier)255     void setNodeCopier(@NonNull NodeCopier nodeCopier) {
256         mNodeCopier = nodeCopier;
257         mTreeTraverser.setNodeCopier(nodeCopier);
258     }
259 
260     /**
261      * Returns the root node in the tree containing {@code node}. The caller is responsible for
262      * recycling the result.
263      */
264     @NonNull
getRoot(@onNull AccessibilityNodeInfo node)265     AccessibilityNodeInfo getRoot(@NonNull AccessibilityNodeInfo node) {
266         // If the node represents a view in the embedded view hierarchy hosted by a SurfaceView,
267         // return the root node of the hierarchy, which is the only child of the SurfaceView node.
268         if (isHostNode(node)) {
269             AccessibilityNodeInfo child = mNodeCopier.copy(node);
270             AccessibilityNodeInfo parent = node.getParent();
271             while (parent != null && !Utils.isSurfaceView(parent)) {
272                 child.recycle();
273                 child = parent;
274                 parent = child.getParent();
275             }
276             Utils.recycleNode(parent);
277             return child;
278         }
279 
280         // Get the root node directly via the window.
281         AccessibilityWindowInfo window = node.getWindow();
282         if (window != null) {
283             AccessibilityNodeInfo root = window.getRoot();
284             window.recycle();
285             if (root != null) {
286                 return root;
287             }
288         }
289 
290         // If the root node can't be accessed via the window, navigate up the node tree.
291         AccessibilityNodeInfo child = mNodeCopier.copy(node);
292         AccessibilityNodeInfo parent = node.getParent();
293         while (parent != null) {
294             child.recycle();
295             child = parent;
296             parent = child.getParent();
297         }
298         return child;
299     }
300 
301     /**
302      * Searches {@code root} and its descendants, and returns the currently focused node if it's
303      * not a {@link FocusParkingView}, or returns null in other cases. The caller is responsible
304      * for recycling the result.
305      */
306     @Nullable
findFocusedNodeInRoot(@onNull AccessibilityNodeInfo root)307     AccessibilityNodeInfo findFocusedNodeInRoot(@NonNull AccessibilityNodeInfo root) {
308         AccessibilityNodeInfo focusedNode = findFocusedNodeInRootInternal(root);
309         if (focusedNode != null && Utils.isFocusParkingView(focusedNode)) {
310             focusedNode.recycle();
311             return null;
312         }
313         return focusedNode;
314     }
315 
316     /**
317      * Searches {@code root} and its descendants, and returns the currently focused node, if any,
318      * or returns null if not found. The caller is responsible for recycling the result.
319      */
320     @Nullable
findFocusedNodeInRootInternal( @onNull AccessibilityNodeInfo root)321     private AccessibilityNodeInfo findFocusedNodeInRootInternal(
322             @NonNull AccessibilityNodeInfo root) {
323         AccessibilityNodeInfo surfaceView = null;
324         if (!isClientNode(root)) {
325             AccessibilityNodeInfo focusedNode = Utils.findFocusWithRetry(root);
326             if (focusedNode != null && Utils.isSurfaceView(focusedNode)) {
327                 // The focused node represents a SurfaceView. In this case the root node is actually
328                 // a client node but Navigator doesn't know that because SurfaceViewHelper doesn't
329                 // know the package name of the client app.
330                 // Although the package name of the client app will be stored in SurfaceViewHelper
331                 // when RotaryService handles TYPE_WINDOW_STATE_CHANGED event, RotaryService may not
332                 // receive the event. For example, RotaryService may have been killed and restarted.
333                 // In this case, Navigator should store the package name.
334                 surfaceView = focusedNode;
335                 addClientApp(surfaceView.getPackageName());
336             } else {
337                 return focusedNode;
338             }
339         }
340 
341         // The root node is in client app, which contains a SurfaceView to display the embedded
342         // view hierarchy. In this case only search inside the embedded view hierarchy.
343         if (surfaceView == null) {
344             surfaceView = findSurfaceViewInRoot(root);
345         }
346         if (surfaceView == null) {
347             L.w("Failed to find SurfaceView in client app " + root);
348             return null;
349         }
350         if (surfaceView.getChildCount() == 0) {
351             L.d("Host app is not loaded yet");
352             surfaceView.recycle();
353             return null;
354         }
355         AccessibilityNodeInfo embeddedRoot = surfaceView.getChild(0);
356         surfaceView.recycle();
357         if (embeddedRoot == null) {
358             L.w("Failed to get the root of host app");
359             return null;
360         }
361         AccessibilityNodeInfo focusedNode = embeddedRoot.findFocus(FOCUS_INPUT);
362         embeddedRoot.recycle();
363         return focusedNode;
364     }
365 
366     /**
367      * Searches the window containing {@code node}, and returns the node representing a {@link
368      * FocusParkingView}, if any, or returns null if not found. The caller is responsible for
369      * recycling the result.
370      */
371     @Nullable
findFocusParkingView(@onNull AccessibilityNodeInfo node)372     AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityNodeInfo node) {
373         AccessibilityNodeInfo root = getRoot(node);
374         AccessibilityNodeInfo fpv = findFocusParkingViewInRoot(root);
375         root.recycle();
376         return fpv;
377     }
378 
379     /**
380      * Searches {@code root} and its descendants, and returns the node representing a {@link
381      * FocusParkingView}, if any, or returns null if not found. The caller is responsible for
382      * recycling the result.
383      */
384     @Nullable
findFocusParkingViewInRoot(@onNull AccessibilityNodeInfo root)385     AccessibilityNodeInfo findFocusParkingViewInRoot(@NonNull AccessibilityNodeInfo root) {
386         return mTreeTraverser.depthFirstSearch(
387                 root,
388                 /* skipPredicate= */ Utils::isFocusArea,
389                 /* targetPredicate= */ Utils::isFocusParkingView
390         );
391     }
392 
393     /**
394      * Searches {@code root} and its descendants, and returns the node representing a {@link
395      * android.view.SurfaceView}, if any, or returns null if not found. The caller is responsible
396      * for recycling the result.
397      */
398     @Nullable
findSurfaceViewInRoot(@onNull AccessibilityNodeInfo root)399     AccessibilityNodeInfo findSurfaceViewInRoot(@NonNull AccessibilityNodeInfo root) {
400         return mTreeTraverser.depthFirstSearch(root, /* targetPredicate= */ Utils::isSurfaceView);
401     }
402 
403     /**
404      * Returns the best target focus area for a nudge in the given {@code direction}. The caller is
405      * responsible for recycling the result.
406      *
407      * @param windows          a list of windows to search from
408      * @param sourceNode       the current focus
409      * @param currentFocusArea the current focus area
410      * @param direction        nudge direction, must be {@link View#FOCUS_UP}, {@link
411      *                         View#FOCUS_DOWN}, {@link View#FOCUS_LEFT}, or {@link
412      *                         View#FOCUS_RIGHT}
413      */
findNudgeTargetFocusArea( @onNull List<AccessibilityWindowInfo> windows, @NonNull AccessibilityNodeInfo sourceNode, @NonNull AccessibilityNodeInfo currentFocusArea, int direction)414     AccessibilityNodeInfo findNudgeTargetFocusArea(
415             @NonNull List<AccessibilityWindowInfo> windows,
416             @NonNull AccessibilityNodeInfo sourceNode,
417             @NonNull AccessibilityNodeInfo currentFocusArea,
418             int direction) {
419         AccessibilityWindowInfo currentWindow = sourceNode.getWindow();
420         if (currentWindow == null) {
421             L.e("Currently focused window is null");
422             return null;
423         }
424 
425         // Build a list of candidate focus areas, starting with all the other explicit focus areas
426         // in the same window as the current focus area.
427         List<AccessibilityNodeInfo> candidateFocusAreas = findNonEmptyFocusAreas(currentWindow);
428         for (AccessibilityNodeInfo focusArea : candidateFocusAreas) {
429             if (focusArea.equals(currentFocusArea)) {
430                 candidateFocusAreas.remove(focusArea);
431                 focusArea.recycle();
432                 break;
433             }
434         }
435 
436         List<Rect> candidateFocusAreasBounds = new ArrayList<>();
437         for (AccessibilityNodeInfo focusArea : candidateFocusAreas) {
438             Rect bounds = Utils.getBoundsInScreen(focusArea);
439             candidateFocusAreasBounds.add(bounds);
440         }
441 
442         // There is up to one implicit focus area in a window. If the current focus area is an
443         // implicit focus area, we're done with the current window. Otherwise, we need to look for
444         // the potential implicit focus area.
445         if (Utils.isFocusArea(currentFocusArea)) {
446             maybeAddImplicitFocusArea(currentWindow, candidateFocusAreas,
447                     candidateFocusAreasBounds);
448         }
449 
450         // If the current focus area is an explicit focus area, use its focus area bounds to find
451         // nudge target as usual. Otherwise, use the tailored bounds, which was added as the last
452         // element of the list in maybeAddImplicitFocusArea().
453         Rect currentFocusAreaBounds;
454         if (Utils.isFocusArea(currentFocusArea)) {
455             currentFocusAreaBounds = Utils.getBoundsInScreen(currentFocusArea);
456         } else if (candidateFocusAreasBounds.size() > 0) {
457             currentFocusAreaBounds =
458                     candidateFocusAreasBounds.get(candidateFocusAreasBounds.size() - 1);
459         } else {
460             // TODO(b/323112198): this should never happen, but let's try to recover from this.
461             L.e("currentFocusArea is an implicit focus area but not added to"
462                     + " currentFocusAreaBounds");
463             L.d("sourceNode:" + sourceNode);
464             L.d("currentFocusArea:" + currentFocusArea);
465             AccessibilityNodeInfo root = getRoot(sourceNode);
466             Utils.printDescendants(root, Utils.LOG_INDENT);
467             Utils.recycleNode(root);
468 
469             currentFocusArea.recycle();
470             currentFocusArea = getAncestorFocusArea(sourceNode);
471             currentFocusAreaBounds = Utils.getBoundsInScreen(currentFocusArea);
472             L.d("updated currentFocusArea:" + currentFocusArea);
473         }
474 
475         if (currentWindow.getType() != TYPE_INPUT_METHOD
476                 || shouldNudgeOutOfIme(sourceNode, currentFocusArea, candidateFocusAreas,
477                            direction)) {
478             // Add candidate focus areas in other windows in the given direction.
479             List<AccessibilityWindowInfo> candidateWindows = new ArrayList<>();
480             boolean isSourceNodeEditable = sourceNode.isEditable();
481             addWindowsInDirection(windows, currentWindow, candidateWindows, direction,
482                     isSourceNodeEditable);
483             currentWindow.recycle();
484             for (AccessibilityWindowInfo window : candidateWindows) {
485                 List<AccessibilityNodeInfo> focusAreasInAnotherWindow =
486                         findNonEmptyFocusAreas(window);
487                 candidateFocusAreas.addAll(focusAreasInAnotherWindow);
488 
489                 for (AccessibilityNodeInfo focusArea : focusAreasInAnotherWindow) {
490                     Rect bounds = Utils.getBoundsInScreen(focusArea);
491                     candidateFocusAreasBounds.add(bounds);
492                 }
493 
494                 maybeAddImplicitFocusArea(window, candidateFocusAreas, candidateFocusAreasBounds);
495             }
496         }
497 
498         Rect sourceBounds = Utils.getBoundsInScreen(sourceNode);
499         // Choose the best candidate as our target focus area.
500         AccessibilityNodeInfo targetFocusArea = chooseBestNudgeCandidate(sourceBounds,
501                 currentFocusAreaBounds, candidateFocusAreas, candidateFocusAreasBounds, direction);
502         Utils.recycleNodes(candidateFocusAreas);
503         return targetFocusArea;
504     }
505 
506     /**
507      * If there are orphan nodes in {@code window}, treats the root node of the window as an
508      * implicit focus area, and add it to {@code candidateFocusAreas}. Besides, tailors its bounds
509      * so that it just wraps its orphan descendants, and adds the tailored bounds to
510      * {@code candidateFocusAreasBounds}.
511      * Orphan nodes are focusable nodes not wrapped inside any explicitly declared focus areas.
512      * It happens in two scenarios:
513      * <ul>
514      *     <li>The app developer wants to treat the entire window as a focus area but doesn't bother
515      *         declaring a focus area to wrap around them. This is allowed.
516      *     <li>The app developer intends to declare focus areas to wrap around focusable views, but
517      *         misses some focusable views, causing them to be unreachable via rotary controller.
518      *         This is not allowed, but RotaryService will try its best to make them reachable.
519      * </ul>
520      */
521     @VisibleForTesting
maybeAddImplicitFocusArea(@onNull AccessibilityWindowInfo window, @NonNull List<AccessibilityNodeInfo> candidateFocusAreas, @NonNull List<Rect> candidateFocusAreasBounds)522     void maybeAddImplicitFocusArea(@NonNull AccessibilityWindowInfo window,
523             @NonNull List<AccessibilityNodeInfo> candidateFocusAreas,
524             @NonNull List<Rect> candidateFocusAreasBounds) {
525         AccessibilityNodeInfo root = window.getRoot();
526         if (root == null) {
527             L.e("No root node for " + window);
528             return;
529         }
530         // If the root node is in the client app and therefore contains a SurfaceView, skip the view
531         // hierarchy of the client app, and scan the view hierarchy of the host app, which is
532         // embedded in the SurfaceView.
533         if (isClientNode(root)) {
534             L.v("Root node is client node " + root);
535             AccessibilityNodeInfo hostRoot = getDescendantHostRoot(root);
536             root.recycle();
537             if (hostRoot == null || !hasFocusableDescendants(hostRoot)) {
538                 L.w("No host node or host node has no focusable descendants " + hostRoot);
539                 Utils.recycleNode(hostRoot);
540                 return;
541             }
542             candidateFocusAreas.add(hostRoot);
543             Rect bounds = new Rect();
544             // To make things simple, just use the node's bounds. Don't tailor the bounds.
545             hostRoot.getBoundsInScreen(bounds);
546             candidateFocusAreasBounds.add(bounds);
547             return;
548         }
549 
550         Rect bounds = computeMinimumBoundsForOrphanDescendants(root);
551         if (bounds.isEmpty()) {
552             return;
553         }
554         L.w("The root node contains focusable nodes that are not inside any focus "
555                 + "areas: " + root);
556         candidateFocusAreas.add(root);
557         candidateFocusAreasBounds.add(bounds);
558     }
559 
560     /**
561      * Returns whether it should nudge out the IME window. If the current window is IME window and
562      * there are candidate FocusAreas in it for the given direction, it shouldn't nudge out of the
563      * IME window.
564      */
shouldNudgeOutOfIme(@onNull AccessibilityNodeInfo sourceNode, @NonNull AccessibilityNodeInfo currentFocusArea, @NonNull List<AccessibilityNodeInfo> focusAreasInCurrentWindow, int direction)565     private boolean shouldNudgeOutOfIme(@NonNull AccessibilityNodeInfo sourceNode,
566             @NonNull AccessibilityNodeInfo currentFocusArea,
567             @NonNull List<AccessibilityNodeInfo> focusAreasInCurrentWindow,
568             int direction) {
569         if (!focusAreasInCurrentWindow.isEmpty()) {
570             Rect sourceBounds = Utils.getBoundsInScreen(sourceNode);
571             Rect sourceFocusAreaBounds = Utils.getBoundsInScreen(currentFocusArea);
572             Rect candidateBounds = Utils.getBoundsInScreen(currentFocusArea);
573             for (AccessibilityNodeInfo candidate : focusAreasInCurrentWindow) {
574                 if (isCandidate(sourceBounds, sourceFocusAreaBounds, candidate, candidateBounds,
575                         direction)) {
576                     return false;
577                 }
578             }
579         }
580         return true;
581     }
582 
containsWebViewWithFocusableDescendants(@onNull AccessibilityNodeInfo node)583     private boolean containsWebViewWithFocusableDescendants(@NonNull AccessibilityNodeInfo node) {
584         List<AccessibilityNodeInfo> webViews = new ArrayList<>();
585         mTreeTraverser.depthFirstSelect(node, Utils::isWebView, webViews);
586         if (webViews.isEmpty()) {
587             return false;
588         }
589         boolean hasFocusableDescendant = false;
590         for (AccessibilityNodeInfo webView : webViews) {
591             if (webViewHasFocusableDescendants(webView)) {
592                 hasFocusableDescendant = true;
593                 break;
594             }
595         }
596         Utils.recycleNodes(webViews);
597         return hasFocusableDescendant;
598     }
599 
webViewHasFocusableDescendants(@onNull AccessibilityNodeInfo webView)600     private boolean webViewHasFocusableDescendants(@NonNull AccessibilityNodeInfo webView) {
601         AccessibilityNodeInfo focusableDescendant = mTreeTraverser.depthFirstSearch(webView,
602                 Utils::canPerformFocus);
603         if (focusableDescendant == null) {
604             return false;
605         }
606         focusableDescendant.recycle();
607         return true;
608     }
609 
isWebViewWithFocusableDescendants(@onNull AccessibilityNodeInfo node)610     private boolean isWebViewWithFocusableDescendants(@NonNull AccessibilityNodeInfo node) {
611         return Utils.isWebView(node) && webViewHasFocusableDescendants(node);
612     }
613 
614     /**
615      * Adds all the {@code windows} in the given {@code direction} of the given {@code source}
616      * window to the given list if the {@code source} window is not an overlay. If it's an overlay
617      * and the source node is editable, adds the IME window only. Otherwise does nothing.
618      */
addWindowsInDirection(@onNull List<AccessibilityWindowInfo> windows, @NonNull AccessibilityWindowInfo source, @NonNull List<AccessibilityWindowInfo> results, int direction, boolean isSourceNodeEditable)619     private void addWindowsInDirection(@NonNull List<AccessibilityWindowInfo> windows,
620             @NonNull AccessibilityWindowInfo source,
621             @NonNull List<AccessibilityWindowInfo> results,
622             int direction,
623             boolean isSourceNodeEditable) {
624         Rect sourceBounds = new Rect();
625         source.getBoundsInScreen(sourceBounds);
626         boolean isSourceWindowOverlayWindow = isOverlayWindow(source, sourceBounds);
627         Rect destBounds = new Rect();
628         for (AccessibilityWindowInfo window : windows) {
629             if (window.equals(source)) {
630                continue;
631             }
632             // Nudging out of the overlay window is not allowed unless the source node is editable
633             // and the target window is an IME window. E.g., nudging from the EditText in the Dialog
634             // to the IME is allowed, while nudging from the Button in the Dialog to the IME is not
635             // allowed.
636             if (isSourceWindowOverlayWindow
637                     && (!isSourceNodeEditable || window.getType() != TYPE_INPUT_METHOD)) {
638                 continue;
639             }
640 
641             window.getBoundsInScreen(destBounds);
642             // Even if only part of destBounds is in the given direction of sourceBounds, we
643             // still include it because that part may contain the target focus area.
644             if (FocusFinder.isPartiallyInDirection(sourceBounds, destBounds, direction)) {
645                 results.add(window);
646             }
647         }
648     }
649 
650     /**
651      * Returns whether the given {@code window} with the given {@code bounds} is an overlay window.
652      * <p>
653      * If the source window is an application window on the default display and it's smaller than
654        the display, then it's either a TaskView window or an overlay window (such as a Dialog
655        window). The ID of a TaskView task is different from the full screen application, while
656        the ID of an overlay task is the same with the full screen application, so task ID is used
657        to decide whether it's an overlay window.
658      */
isOverlayWindow(@onNull AccessibilityWindowInfo window, @NonNull Rect bounds)659     private boolean isOverlayWindow(@NonNull AccessibilityWindowInfo window, @NonNull Rect bounds) {
660         return window.getType() == TYPE_APPLICATION
661                 && window.getDisplayId() == Display.DEFAULT_DISPLAY
662                 && !mAppWindowBounds.equals(bounds)
663                 && window.getTaskId() == mAppWindowTaskId;
664     }
665 
666     /**
667      * Returns whether nudging to the given {@code direction} can dismiss the given {@code window}
668      * with the given {@code bounds}.
669      */
isDismissible(@onNull AccessibilityWindowInfo window, @NonNull Rect bounds, @View.FocusRealDirection int direction)670     boolean isDismissible(@NonNull AccessibilityWindowInfo window,
671             @NonNull Rect bounds,
672             @View.FocusRealDirection int direction) {
673         // Only overlay windows can be dismissed.
674         if (!isOverlayWindow(window, bounds)) {
675             return false;
676         }
677         // The window can be dismissed when part of the underlying window is not covered by it in
678         // the given direction.
679         switch (direction) {
680             case FOCUS_UP:
681                 return mAppWindowBounds.top < bounds.top;
682             case FOCUS_DOWN:
683                 return mAppWindowBounds.bottom > bounds.bottom;
684             case FOCUS_LEFT:
685                 return mAppWindowBounds.left < bounds.left;
686             case FOCUS_RIGHT:
687                 return mAppWindowBounds.right > bounds.right;
688         }
689         return false;
690     }
691 
692     /**
693      * Scans the view hierarchy of the given {@code window} looking for explicit focus areas with
694      * focusable descendants and returns the focus areas. The caller is responsible for recycling
695      * the result.
696      */
697     @NonNull
698     @VisibleForTesting
findNonEmptyFocusAreas(@onNull AccessibilityWindowInfo window)699     List<AccessibilityNodeInfo> findNonEmptyFocusAreas(@NonNull AccessibilityWindowInfo window) {
700         List<AccessibilityNodeInfo> results = new ArrayList<>();
701         AccessibilityNodeInfo rootNode = window.getRoot();
702         if (rootNode == null) {
703             L.e("No root node for " + window);
704         } else if (!isClientNode(rootNode)) {
705             addNonEmptyFocusAreas(rootNode, results);
706         }
707         // If the root node is in the client app, it won't contain any explicit focus areas, so
708         // skip it.
709 
710         Utils.recycleNode(rootNode);
711         return results;
712     }
713 
714     /**
715      * Searches from {@code clientNode}, and returns the root of the embedded view hierarchy if any,
716      * or returns null if not found. The caller is responsible for recycling the result.
717      */
718     @Nullable
getDescendantHostRoot(@onNull AccessibilityNodeInfo clientNode)719     AccessibilityNodeInfo getDescendantHostRoot(@NonNull AccessibilityNodeInfo clientNode) {
720         return mTreeTraverser.depthFirstSearch(clientNode, this::isHostNode);
721     }
722 
723     /**
724      * Returns whether the given window is the Heads-up Notification (HUN) window. The HUN window
725      * is identified by the left and right edges. The top and bottom vary depending on whether the
726      * HUN appears at the top or bottom of the screen and on the height of the notification being
727      * displayed so they aren't used.
728      */
isHunWindow(@ullable AccessibilityWindowInfo window)729     boolean isHunWindow(@Nullable AccessibilityWindowInfo window) {
730         if (window == null || window.getType() != AccessibilityWindowInfo.TYPE_SYSTEM) {
731             return false;
732         }
733         Rect bounds = new Rect();
734         window.getBoundsInScreen(bounds);
735         return bounds.left == mHunLeft && bounds.right == mHunRight;
736     }
737 
738     /**
739      * Searches from the given node up through its ancestors to the containing focus area, looking
740      * for a node that's marked as horizontally or vertically scrollable. Returns a copy of the
741      * first such node or null if none is found. The caller is responsible for recycling the result.
742      */
743     @Nullable
findScrollableContainer(@onNull AccessibilityNodeInfo node)744     AccessibilityNodeInfo findScrollableContainer(@NonNull AccessibilityNodeInfo node) {
745         return mTreeTraverser.findNodeOrAncestor(node, /* stopPredicate= */ Utils::isFocusArea,
746                 /* targetPredicate= */ Utils::isScrollableContainer);
747     }
748 
749     /**
750      * Returns the previous node  before {@code referenceNode} in Tab order that can take focus or
751      * the next node after {@code referenceNode} in Tab order that can take focus, depending on
752      * {@code direction}. The search is limited to descendants of {@code containerNode}. Returns
753      * null if there are no descendants that can take focus in the given direction. The caller is
754      * responsible for recycling the result.
755      *
756      * @param containerNode the node with descendants
757      * @param referenceNode a descendant of {@code containerNode} to start from
758      * @param direction     {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}
759      * @return the node before or after {@code referenceNode} or null if none
760      */
761     @Nullable
findFocusableDescendantInDirection( @onNull AccessibilityNodeInfo containerNode, @NonNull AccessibilityNodeInfo referenceNode, int direction)762     AccessibilityNodeInfo findFocusableDescendantInDirection(
763             @NonNull AccessibilityNodeInfo containerNode,
764             @NonNull AccessibilityNodeInfo referenceNode,
765             int direction) {
766         AccessibilityNodeInfo targetNode = copyNode(referenceNode);
767         do {
768             AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(direction);
769             if (nextTargetNode == null
770                     || nextTargetNode.equals(containerNode)
771                     || !Utils.isDescendant(containerNode, nextTargetNode)) {
772                 Utils.recycleNode(nextTargetNode);
773                 Utils.recycleNode(targetNode);
774                 return null;
775             }
776             if (nextTargetNode.equals(referenceNode) || nextTargetNode.equals(targetNode)) {
777                 L.w((direction == View.FOCUS_FORWARD ? "Next" : "Previous")
778                         + " node is the same node: " + referenceNode);
779                 Utils.recycleNode(nextTargetNode);
780                 Utils.recycleNode(targetNode);
781                 return null;
782             }
783             targetNode.recycle();
784             targetNode = nextTargetNode;
785         } while (!Utils.canTakeFocus(targetNode));
786         return targetNode;
787     }
788 
789     /**
790      * Returns the first descendant of {@code node} which can take focus. The nodes are searched in
791      * in depth-first order, not including {@code node} itself. If no descendant can take focus,
792      * null is returned. The caller is responsible for recycling the result.
793      */
794     @Nullable
findFirstFocusableDescendant(@onNull AccessibilityNodeInfo node)795     AccessibilityNodeInfo findFirstFocusableDescendant(@NonNull AccessibilityNodeInfo node) {
796         return mTreeTraverser.depthFirstSearch(node,
797                 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode));
798     }
799 
800     /**
801      * Returns the first orphan descendant (focusable descendant not inside any focus areas) of
802      * {@code node}. The nodes are searched in depth-first order, not including {@code node} itself.
803      * If not found, null is returned. The caller is responsible for recycling the result.
804      */
805     @Nullable
findFirstOrphan(@onNull AccessibilityNodeInfo node)806     AccessibilityNodeInfo findFirstOrphan(@NonNull AccessibilityNodeInfo node) {
807         return mTreeTraverser.depthFirstSearch(node,
808                 /* skipPredicate= */ Utils::isFocusArea,
809                 /* targetPredicate= */ candidateNode -> candidateNode != node
810                         && Utils.canTakeFocus(candidateNode));
811     }
812 
813     /**
814      * Returns the last descendant of {@code node} which can take focus. The nodes are searched in
815      * reverse depth-first order, not including {@code node} itself. If no descendant can take
816      * focus, null is returned. The caller is responsible for recycling the result.
817      */
818     @Nullable
findLastFocusableDescendant(@onNull AccessibilityNodeInfo node)819     AccessibilityNodeInfo findLastFocusableDescendant(@NonNull AccessibilityNodeInfo node) {
820         return mTreeTraverser.reverseDepthFirstSearch(node,
821                 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode));
822     }
823 
824     /**
825      * Scans descendants of the given {@code rootNode} looking for explicit focus areas with
826      * focusable descendants and adds the focus areas to the given list. It doesn't scan inside
827      * focus areas since nested focus areas aren't allowed. It ignores focus areas without
828      * focusable descendants, because once we found the best candidate focus area, we don't dig
829      * into other ones. If it has no descendants to take focus, the nudge will fail. The caller is
830      * responsible for recycling added nodes.
831      *
832      * @param rootNode the root to start scanning from
833      * @param results  a list of focus areas to add to
834      */
addNonEmptyFocusAreas(@onNull AccessibilityNodeInfo rootNode, @NonNull List<AccessibilityNodeInfo> results)835     private void addNonEmptyFocusAreas(@NonNull AccessibilityNodeInfo rootNode,
836             @NonNull List<AccessibilityNodeInfo> results) {
837         mTreeTraverser.depthFirstSelect(rootNode,
838                 (focusArea) -> Utils.isFocusArea(focusArea) && hasFocusableDescendants(focusArea),
839                 results);
840     }
841 
hasFocusableDescendants(@onNull AccessibilityNodeInfo focusArea)842     private boolean hasFocusableDescendants(@NonNull AccessibilityNodeInfo focusArea) {
843         return Utils.canHaveFocus(focusArea) || containsWebViewWithFocusableDescendants(focusArea);
844     }
845 
846     /**
847      * Returns the minimum rectangle wrapping the given {@code node}'s orphan descendants. If
848      * {@code node} has no orphan descendants, returns an empty {@link Rect}.
849      */
850     @NonNull
851     @VisibleForTesting
computeMinimumBoundsForOrphanDescendants( @onNull AccessibilityNodeInfo node)852     Rect computeMinimumBoundsForOrphanDescendants(
853             @NonNull AccessibilityNodeInfo node) {
854         Rect bounds = new Rect();
855         if (Utils.isFocusArea(node) || Utils.isFocusParkingView(node)) {
856             return bounds;
857         }
858         if (Utils.canTakeFocus(node) || isWebViewWithFocusableDescendants(node)) {
859             return Utils.getBoundsInScreen(node);
860         }
861         for (int i = 0; i < node.getChildCount(); i++) {
862             AccessibilityNodeInfo child = node.getChild(i);
863             if (child == null) {
864                 continue;
865             }
866             Rect childBounds = computeMinimumBoundsForOrphanDescendants(child);
867             child.recycle();
868             if (childBounds != null) {
869                 bounds.union(childBounds);
870             }
871         }
872         return bounds;
873     }
874 
875     /**
876      * Returns a copy of the best candidate from among the given {@code candidates} for a nudge
877      * from {@code sourceNode} in the given {@code direction}. Returns null if none of the {@code
878      * candidates} are in the given {@code direction}. The caller is responsible for recycling the
879      * result.
880      *
881      * @param candidates could be a list of {@link FocusArea}s, or a list of focusable views
882      */
883     @Nullable
chooseBestNudgeCandidate(@onNull Rect sourceBounds, @NonNull Rect sourceFocusAreaBounds, @NonNull List<AccessibilityNodeInfo> candidates, @NonNull List<Rect> candidatesBounds, int direction)884     private AccessibilityNodeInfo chooseBestNudgeCandidate(@NonNull Rect sourceBounds,
885             @NonNull Rect sourceFocusAreaBounds,
886             @NonNull List<AccessibilityNodeInfo> candidates,
887             @NonNull List<Rect> candidatesBounds,
888             int direction) {
889         if (candidates.isEmpty()) {
890             return null;
891         }
892         AccessibilityNodeInfo bestNode = null;
893         Rect bestBounds = new Rect();
894         for (int i = 0; i < candidates.size(); i++) {
895             AccessibilityNodeInfo candidate = candidates.get(i);
896             Rect candidateBounds = candidatesBounds.get(i);
897             if (isCandidate(sourceBounds, sourceFocusAreaBounds, candidate, candidateBounds,
898                     direction)) {
899                 if (bestNode == null || FocusFinder.isBetterCandidate(
900                         direction, sourceBounds, candidateBounds, bestBounds)) {
901                     bestNode = candidate;
902                     bestBounds.set(candidateBounds);
903                 }
904             }
905         }
906         return copyNode(bestNode);
907     }
908 
909     /**
910      * Returns whether the given {@code node} is a candidate from {@code sourceBounds} to the given
911      * {@code direction}.
912      * <p>
913      * To be a candidate, the node
914      * <ul>
915      *     <li>must be considered a candidate by {@link FocusFinder#isCandidate} if it represents a
916      *         focusable view within a focus area
917      *     <li>must be in the {@code direction} of the {@code sourceFocusAreaBounds} and one of its
918      *         focusable descendants must be a candidate if it represents a focus area
919      * </ul>
920      */
isCandidate(@onNull Rect sourceBounds, @NonNull Rect sourceFocusAreaBounds, @NonNull AccessibilityNodeInfo node, @NonNull Rect nodeBounds, int direction)921     private boolean isCandidate(@NonNull Rect sourceBounds,
922             @NonNull Rect sourceFocusAreaBounds,
923             @NonNull AccessibilityNodeInfo node,
924             @NonNull Rect nodeBounds,
925             int direction) {
926         AccessibilityNodeInfo candidate = mTreeTraverser.depthFirstSearch(node,
927                 /* skipPredicate= */ candidateNode -> {
928                     if (Utils.canTakeFocus(candidateNode)) {
929                         return false;
930                     }
931                     // If a node can't take focus, it represents a focus area. If the focus area
932                     // doesn't intersect with sourceFocusAreaBounds, and it's not in the given
933                     // direction of sourceFocusAreaBounds, it's not a candidate, so we should return
934                     // true to stop searching.
935                     return !Rect.intersects(nodeBounds, sourceFocusAreaBounds)
936                             && !FocusFinder.isInDirection(
937                                 sourceFocusAreaBounds, nodeBounds, direction);
938                 },
939                 /* targetPredicate= */ candidateNode -> {
940                     // RotaryService can navigate to nodes in a WebView or a ComposeView even when
941                     // off-screen, so we use canPerformFocus() to skip the bounds check.
942                     if (isInVirtualNodeHierarchy(candidateNode)) {
943                         return Utils.canPerformFocus(candidateNode);
944                     }
945                     // If a node isn't visible to the user, e.g. another window is obscuring it,
946                     // skip it.
947                     if (!candidateNode.isVisibleToUser()) {
948                         return false;
949                     }
950                     // If a node can't take focus, it represents a focus area, so we return false to
951                     // skip the node and let it search its descendants.
952                     if (!Utils.canTakeFocus(candidateNode)) {
953                         return false;
954                     }
955                     // The node represents a focusable view in a focus area, so check the geometry.
956                     return FocusFinder.isCandidate(sourceBounds, nodeBounds, direction);
957                 });
958         if (candidate == null) {
959             return false;
960         }
961         candidate.recycle();
962         return true;
963     }
964 
copyNode(@ullable AccessibilityNodeInfo node)965     private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
966         return mNodeCopier.copy(node);
967     }
968 
969     /**
970      * Returns the closest ancestor focus area of the given {@code node}.
971      * <ul>
972      *     <li> If the given {@code node} is a {@link FocusArea} node or a descendant of a {@link
973      *          FocusArea} node, returns the {@link FocusArea} node.
974      *     <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this
975      *          view, and this view is not in an embedded view hierarchy, returns the root node.
976      *     <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this
977      *          view, and this view is in an embedded view hierarchy, returns the root node of
978      *          embedded view hierarchy.
979      * </ul>
980      * The caller is responsible for recycling the result.
981      */
982     @NonNull
getAncestorFocusArea(@onNull AccessibilityNodeInfo node)983     AccessibilityNodeInfo getAncestorFocusArea(@NonNull AccessibilityNodeInfo node) {
984         Predicate<AccessibilityNodeInfo> isFocusAreaOrRoot = candidateNode -> {
985             if (Utils.isFocusArea(candidateNode)) {
986                 // The candidateNode is a focus area.
987                 return true;
988             }
989             AccessibilityNodeInfo parent = candidateNode.getParent();
990             if (parent == null) {
991                 // The candidateNode is the root node.
992                 return true;
993             }
994             if (Utils.isSurfaceView(parent)) {
995                 // Treat the root of embedded view hierarchy (i.e., the only child of the
996                 // SurfaceView) as an implicit focus area.
997                 return true;
998             }
999             parent.recycle();
1000             return false;
1001         };
1002         AccessibilityNodeInfo result = mTreeTraverser.findNodeOrAncestor(node, isFocusAreaOrRoot);
1003         if (result == null || !Utils.isFocusArea(result)) {
1004             L.w("Ancestor focus area for node " + node + " is not an explicit FocusArea: "
1005                     + result);
1006         }
1007         return result;
1008     }
1009 
1010     /**
1011      * Returns a copy of {@code node} or the nearest ancestor that represents a {@code WebView}.
1012      * Returns null if {@code node} isn't a {@code WebView} and isn't a descendant of a {@code
1013      * WebView}.
1014      */
1015     @Nullable
findWebViewAncestor(@onNull AccessibilityNodeInfo node)1016     private AccessibilityNodeInfo findWebViewAncestor(@NonNull AccessibilityNodeInfo node) {
1017         return mTreeTraverser.findNodeOrAncestor(node, Utils::isWebView);
1018     }
1019 
1020     /**
1021      * Returns a copy of {@code node} or the nearest ancestor that represents a {@code ComposeView}
1022      * or a {@code WebView}. Returns null if {@code node} isn't a {@code ComposeView} or a
1023      * {@code WebView} and is not a descendant of a {@code ComposeView} or a {@code WebView}.
1024      *
1025      * TODO(b/192274274): This method may not be necessary anymore if Compose supports focusSearch.
1026      */
1027     @Nullable
findVirtualViewAncestor(@onNull AccessibilityNodeInfo node)1028     private AccessibilityNodeInfo findVirtualViewAncestor(@NonNull AccessibilityNodeInfo node) {
1029         return mTreeTraverser.findNodeOrAncestor(node, /* targetPredicate= */ (nodeInfo) ->
1030             Utils.isComposeView(nodeInfo) || Utils.isWebView(nodeInfo));
1031     }
1032 
1033     /** Returns whether {@code node} is a {@code WebView} or is a descendant of one. */
isInWebView(@onNull AccessibilityNodeInfo node)1034     boolean isInWebView(@NonNull AccessibilityNodeInfo node) {
1035         AccessibilityNodeInfo webView = findWebViewAncestor(node);
1036         if (webView == null) {
1037             return false;
1038         }
1039         webView.recycle();
1040         return true;
1041     }
1042 
1043     /**
1044      * Returns whether {@code node} is a {@code ComposeView}, is a {@code WebView}, or is a
1045      * descendant of either.
1046      */
isInVirtualNodeHierarchy(@onNull AccessibilityNodeInfo node)1047     boolean isInVirtualNodeHierarchy(@NonNull AccessibilityNodeInfo node) {
1048         AccessibilityNodeInfo virtualViewAncestor = findVirtualViewAncestor(node);
1049         if (virtualViewAncestor == null) {
1050             return false;
1051         }
1052         virtualViewAncestor.recycle();
1053         return true;
1054     }
1055 
1056     /**
1057      * Returns the next focusable node after {@code candidate} in {@code direction} in {@code
1058      * root} or null if none. This handles navigating into a WebView as well as within a WebView.
1059      * This also handles navigating into a ComposeView, as well as within a ComposeView.
1060      */
1061     @Nullable
findNextFocusableInVirtualRoot( @onNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo candidate, int direction)1062     private AccessibilityNodeInfo findNextFocusableInVirtualRoot(
1063             @NonNull AccessibilityNodeInfo root,
1064             @NonNull AccessibilityNodeInfo candidate, int direction) {
1065         // focusSearch() doesn't work in WebViews or ComposeViews so use tree traversal instead.
1066         if (Utils.isWebView(candidate) || Utils.isComposeView(candidate)) {
1067             if (direction == View.FOCUS_FORWARD) {
1068                 // When entering into the root of a virtual node hierarchy, find the first focusable
1069                 // child node of the root if any.
1070                 return findFirstFocusableDescendantInVirtualRoot(candidate);
1071             } else {
1072                 // When backing into the root of a virtual node hierarchy, find the last focusable
1073                 // child node of the root if any.
1074                 return findLastFocusableDescendantInVirtualRoot(candidate);
1075             }
1076         } else {
1077             // When navigating within a virtual view hierarchy, find the next or previous focusable
1078             // node in depth-first order.
1079             if (direction == View.FOCUS_FORWARD) {
1080                 return findFirstFocusDescendantInVirtualRootAfter(root, candidate);
1081             } else {
1082                 return findFirstFocusDescendantInVirtualRootBefore(root, candidate);
1083             }
1084         }
1085     }
1086 
1087     /**
1088      * Returns the first descendant of {@code webView} which can perform focus. This includes off-
1089      * screen descendants. The nodes are searched in in depth-first order, not including
1090      * {@code root} itself. If no descendant can perform focus, null is returned. The caller is
1091      * responsible for recycling the result.
1092      */
1093     @Nullable
findFirstFocusableDescendantInVirtualRoot( @onNull AccessibilityNodeInfo root)1094     private AccessibilityNodeInfo findFirstFocusableDescendantInVirtualRoot(
1095             @NonNull AccessibilityNodeInfo root) {
1096         return mTreeTraverser.depthFirstSearch(root,
1097                 candidateNode -> candidateNode != root && Utils.canPerformFocus(candidateNode));
1098     }
1099 
1100     /**
1101      * Returns the last descendant of {@code root} which can perform focus. This includes off-
1102      * screen descendants. The nodes are searched in reverse depth-first order, not including
1103      * {@code root} itself. If no descendant can perform focus, null is returned. The caller is
1104      * responsible for recycling the result.
1105      */
1106     @Nullable
findLastFocusableDescendantInVirtualRoot( @onNull AccessibilityNodeInfo root)1107     private AccessibilityNodeInfo findLastFocusableDescendantInVirtualRoot(
1108             @NonNull AccessibilityNodeInfo root) {
1109         return mTreeTraverser.reverseDepthFirstSearch(root,
1110                 candidateNode -> candidateNode != root && Utils.canPerformFocus(candidateNode));
1111     }
1112 
1113     @Nullable
findFirstFocusDescendantInVirtualRootBefore( @onNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo beforeNode)1114     private AccessibilityNodeInfo findFirstFocusDescendantInVirtualRootBefore(
1115             @NonNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo beforeNode) {
1116         boolean[] foundBeforeNode = new boolean[1];
1117         return mTreeTraverser.reverseDepthFirstSearch(root,
1118                 node -> {
1119                     if (foundBeforeNode[0] && Utils.canPerformFocus(node)) {
1120                         return true;
1121                     }
1122                     if (node.equals(beforeNode)) {
1123                         foundBeforeNode[0] = true;
1124                     }
1125                     return false;
1126                 });
1127     }
1128 
1129     @Nullable
1130     private AccessibilityNodeInfo findFirstFocusDescendantInVirtualRootAfter(
1131             @NonNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo afterNode) {
1132         boolean[] foundAfterNode = new boolean[1];
1133         return mTreeTraverser.depthFirstSearch(root,
1134                 node -> {
1135                     if (foundAfterNode[0] && Utils.canPerformFocus(node)) {
1136                         return true;
1137                     }
1138                     if (node.equals(afterNode)) {
1139                         foundAfterNode[0] = true;
1140                     }
1141                     return false;
1142                 });
1143     }
1144 
1145     void dump(@NonNull DualDumpOutputStream dumpOutputStream, boolean dumpAsProto,
1146             @NonNull String fieldName, long fieldId) {
1147         long fieldToken = dumpOutputStream.start(fieldName, fieldId);
1148         dumpOutputStream.write("hunLeft", RotaryProtos.Navigator.HUN_LEFT, mHunLeft);
1149         dumpOutputStream.write("hunRight", RotaryProtos.Navigator.HUN_RIGHT, mHunRight);
1150         DumpUtils.writeFocusDirection(dumpOutputStream, dumpAsProto, "hunNudgeDirection",
1151                 RotaryProtos.Navigator.HUN_NUDGE_DIRECTION, mHunNudgeDirection);
1152         DumpUtils.writeRect(dumpOutputStream, mAppWindowBounds, "appWindowBounds",
1153                 RotaryProtos.Navigator.APP_WINDOW_BOUNDS);
1154         mSurfaceViewHelper.dump(dumpOutputStream, dumpAsProto, "surfaceViewHelper",
1155                 RotaryProtos.Navigator.SURFACE_VIEW_HELPER);
1156         dumpOutputStream.end(fieldToken);
1157     }
1158 
1159     static String directionToString(@View.FocusRealDirection int direction) {
1160         switch (direction) {
1161             case FOCUS_UP:
1162                 return "FOCUS_UP";
1163             case FOCUS_DOWN:
1164                 return "FOCUS_DOWN";
1165             case FOCUS_LEFT:
1166                 return "FOCUS_LEFT";
1167             case FOCUS_RIGHT:
1168                 return "FOCUS_RIGHT";
1169             default:
1170                 return "<unknown direction " + direction + ">";
1171         }
1172     }
1173 
1174     /** Result from {@link #findRotateTarget}. */
1175     static class FindRotateTargetResult {
1176         @NonNull final AccessibilityNodeInfo node;
1177         final int advancedCount;
1178 
1179         FindRotateTargetResult(@NonNull AccessibilityNodeInfo node, int advancedCount) {
1180             this.node = node;
1181             this.advancedCount = advancedCount;
1182         }
1183     }
1184 }
1185