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