• 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.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD;
19 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
20 import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_INPUT;
21 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_APPLICATION;
22 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_INPUT_METHOD;
23 
24 import android.content.pm.PackageManager;
25 import android.graphics.Rect;
26 import android.view.Display;
27 import android.view.View;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.view.accessibility.AccessibilityWindowInfo;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.car.ui.FocusArea;
36 import com.android.car.ui.FocusParkingView;
37 
38 import java.io.FileDescriptor;
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.function.Predicate;
44 
45 /**
46  * A helper class used for finding the next focusable node when the rotary controller is rotated or
47  * nudged.
48  */
49 class Navigator {
50 
51     @NonNull
52     private NodeCopier mNodeCopier = new NodeCopier();
53 
54     @NonNull
55     private final TreeTraverser mTreeTraverser = new TreeTraverser();
56 
57     @NonNull
58     @VisibleForTesting
59     final SurfaceViewHelper mSurfaceViewHelper = new SurfaceViewHelper();
60 
61     private final int mHunLeft;
62     private final int mHunRight;
63 
64     @View.FocusRealDirection
65     private int mHunNudgeDirection;
66 
67     @NonNull
68     private final Rect mAppWindowBounds;
69 
Navigator(int displayWidth, int displayHeight, int hunLeft, int hunRight, boolean showHunOnBottom)70     Navigator(int displayWidth, int displayHeight, int hunLeft, int hunRight,
71             boolean showHunOnBottom) {
72         mHunLeft = hunLeft;
73         mHunRight = hunRight;
74         mHunNudgeDirection = showHunOnBottom ? View.FOCUS_DOWN : View.FOCUS_UP;
75         mAppWindowBounds = new Rect(0, 0, displayWidth, displayHeight);
76     }
77 
78     @VisibleForTesting
Navigator()79     Navigator() {
80         this(0, 0, 0, 0, false);
81     }
82 
83     /** Initializes the package name of the host app. */
initHostApp(@onNull PackageManager packageManager)84     void initHostApp(@NonNull PackageManager packageManager) {
85         mSurfaceViewHelper.initHostApp(packageManager);
86     }
87 
88     /** Clears the package name of the host app if the given {@code packageName} matches. */
clearHostApp(@onNull String packageName)89     void clearHostApp(@NonNull String packageName) {
90         mSurfaceViewHelper.clearHostApp(packageName);
91     }
92 
93     /** Adds the package name of the client app. */
addClientApp(@onNull CharSequence clientAppPackageName)94     void addClientApp(@NonNull CharSequence clientAppPackageName) {
95         mSurfaceViewHelper.addClientApp(clientAppPackageName);
96     }
97 
98     /** Returns whether the given {@code node} represents a view of the host app. */
isHostNode(@onNull AccessibilityNodeInfo node)99     boolean isHostNode(@NonNull AccessibilityNodeInfo node) {
100         return mSurfaceViewHelper.isHostNode(node);
101     }
102 
103     /** Returns whether the given {@code node} represents a view of the client app. */
isClientNode(@onNull AccessibilityNodeInfo node)104     boolean isClientNode(@NonNull AccessibilityNodeInfo node) {
105         return mSurfaceViewHelper.isClientNode(node);
106     }
107 
108     @Nullable
findHunWindow(@onNull List<AccessibilityWindowInfo> windows)109     AccessibilityWindowInfo findHunWindow(@NonNull List<AccessibilityWindowInfo> windows) {
110         for (AccessibilityWindowInfo window : windows) {
111             if (isHunWindow(window)) {
112                 return window;
113             }
114         }
115         return null;
116     }
117 
118     /**
119      * Returns the target focusable for a rotate. The caller is responsible for recycling the node
120      * in the result.
121      *
122      * <p>Limits navigation to focusable views within a scrollable container's viewport, if any.
123      *
124      * @param sourceNode    the current focus
125      * @param direction     rotate direction, must be {@link View#FOCUS_FORWARD} or {@link
126      *                      View#FOCUS_BACKWARD}
127      * @param rotationCount the number of "ticks" to rotate. Only count nodes that can take focus
128      *                      (visible, focusable and enabled). If {@code skipNode} is encountered, it
129      *                      isn't counted.
130      * @return a FindRotateTargetResult containing a node and a count of the number of times the
131      *         search advanced to another node. The node represents a focusable view in the given
132      *         {@code direction} from the current focus within the same {@link FocusArea}. If the
133      *         first or last view is reached before counting up to {@code rotationCount}, the first
134      *         or last view is returned. However, if there are no views that can take focus in the
135      *         given {@code direction}, {@code null} is returned.
136      */
137     @Nullable
findRotateTarget( @onNull AccessibilityNodeInfo sourceNode, int direction, int rotationCount)138     FindRotateTargetResult findRotateTarget(
139             @NonNull AccessibilityNodeInfo sourceNode, int direction, int rotationCount) {
140         int advancedCount = 0;
141         AccessibilityNodeInfo currentFocusArea = getAncestorFocusArea(sourceNode);
142         AccessibilityNodeInfo candidate = copyNode(sourceNode);
143         AccessibilityNodeInfo target = null;
144         while (advancedCount < rotationCount) {
145             AccessibilityNodeInfo nextCandidate = null;
146             AccessibilityNodeInfo webView = findWebViewAncestor(candidate);
147             if (webView != null) {
148                 nextCandidate = findNextFocusableInWebView(webView, candidate, direction);
149             }
150             if (nextCandidate == null) {
151                 // If we aren't in a WebView or there aren't any more focusable nodes within the
152                 // WebView, use focusSearch().
153                 nextCandidate = candidate.focusSearch(direction);
154             }
155             AccessibilityNodeInfo candidateFocusArea =
156                     nextCandidate == null ? null : getAncestorFocusArea(nextCandidate);
157 
158             // Only advance to nextCandidate if:
159             // 1. it's in the same focus area,
160             // 2. and it isn't a FocusParkingView (this is to prevent wrap-around when there is only
161             //    one focus area in the window, including when the root node is treated as a focus
162             //    area),
163             // 3. and nextCandidate is different from candidate (if sourceNode is the first
164             //    focusable node in the window, searching backward will return sourceNode itself).
165             if (nextCandidate != null && currentFocusArea.equals(candidateFocusArea)
166                     && !Utils.isFocusParkingView(nextCandidate)
167                     && !nextCandidate.equals(candidate)) {
168                 // We need to skip nextTargetNode if:
169                 // 1. it can't perform focus action (focusSearch() may return a node with zero
170                 //    width and height),
171                 // 2. or it is a scrollable container but it shouldn't be scrolled (i.e., it is not
172                 //    scrollable, or its descendants can take focus).
173                 //    When we want to focus on its element directly, we'll skip the container. When
174                 //    we want to focus on container and scroll it, we won't skip the container.
175                 if (!Utils.canPerformFocus(nextCandidate)
176                         || (Utils.isScrollableContainer(nextCandidate)
177                             && !Utils.canScrollableContainerTakeFocus(nextCandidate))) {
178                     Utils.recycleNode(candidate);
179                     Utils.recycleNode(candidateFocusArea);
180                     candidate = nextCandidate;
181                     continue;
182                 }
183 
184                 // If we're navigating in a scrollable container that can scroll in the specified
185                 // direction and the next candidate is off-screen or there are no more focusable
186                 // views within the scrollable container, stop navigating so that any remaining
187                 // detents are used for scrolling.
188                 AccessibilityNodeInfo scrollableContainer = findScrollableContainer(candidate);
189                 AccessibilityNodeInfo.AccessibilityAction scrollAction =
190                         direction == View.FOCUS_FORWARD
191                                 ? ACTION_SCROLL_FORWARD
192                                 : ACTION_SCROLL_BACKWARD;
193                 if (scrollableContainer != null
194                         && scrollableContainer.getActionList().contains(scrollAction)
195                         && (!Utils.isDescendant(scrollableContainer, nextCandidate)
196                                 || Utils.getBoundsInScreen(nextCandidate).isEmpty())) {
197                     Utils.recycleNode(nextCandidate);
198                     Utils.recycleNode(candidateFocusArea);
199                     break;
200                 }
201                 Utils.recycleNode(scrollableContainer);
202 
203                 Utils.recycleNode(candidate);
204                 Utils.recycleNode(candidateFocusArea);
205                 candidate = nextCandidate;
206                 Utils.recycleNode(target);
207                 target = copyNode(candidate);
208                 advancedCount++;
209             } else {
210                 Utils.recycleNode(nextCandidate);
211                 Utils.recycleNode(candidateFocusArea);
212                 break;
213             }
214         }
215         currentFocusArea.recycle();
216         candidate.recycle();
217         if (sourceNode.equals(target)) {
218             L.e("Wrap-around on the same node");
219             target.recycle();
220             return null;
221         }
222         return target == null ? null : new FindRotateTargetResult(target, advancedCount);
223     }
224 
225     /** Sets a NodeCopier instance for testing. */
226     @VisibleForTesting
setNodeCopier(@onNull NodeCopier nodeCopier)227     void setNodeCopier(@NonNull NodeCopier nodeCopier) {
228         mNodeCopier = nodeCopier;
229         mTreeTraverser.setNodeCopier(nodeCopier);
230     }
231 
232     /**
233      * Returns the root node in the tree containing {@code node}. The caller is responsible for
234      * recycling the result.
235      */
236     @NonNull
getRoot(@onNull AccessibilityNodeInfo node)237     AccessibilityNodeInfo getRoot(@NonNull AccessibilityNodeInfo node) {
238         // If the node represents a view in the embedded view hierarchy hosted by a SurfaceView,
239         // return the root node of the hierarchy, which is the only child of the SurfaceView node.
240         if (isHostNode(node)) {
241             AccessibilityNodeInfo child = mNodeCopier.copy(node);
242             AccessibilityNodeInfo parent = node.getParent();
243             while (parent != null && !Utils.isSurfaceView(parent)) {
244                 child.recycle();
245                 child = parent;
246                 parent = child.getParent();
247             }
248             Utils.recycleNode(parent);
249             return child;
250         }
251 
252         // Get the root node directly via the window.
253         AccessibilityWindowInfo window = node.getWindow();
254         if (window != null) {
255             AccessibilityNodeInfo root = window.getRoot();
256             window.recycle();
257             if (root != null) {
258                 return root;
259             }
260         }
261 
262         // If the root node can't be accessed via the window, navigate up the node tree.
263         AccessibilityNodeInfo child = mNodeCopier.copy(node);
264         AccessibilityNodeInfo parent = node.getParent();
265         while (parent != null) {
266             child.recycle();
267             child = parent;
268             parent = child.getParent();
269         }
270         return child;
271     }
272 
273     /**
274      * Searches {@code root} and its descendants, and returns the currently focused node if it's
275      * not a {@link FocusParkingView}, or returns null in other cases. The caller is responsible
276      * for recycling the result.
277      */
278     @Nullable
findFocusedNodeInRoot(@onNull AccessibilityNodeInfo root)279     AccessibilityNodeInfo findFocusedNodeInRoot(@NonNull AccessibilityNodeInfo root) {
280         AccessibilityNodeInfo focusedNode = findFocusedNodeInRootInternal(root);
281         if (focusedNode != null && Utils.isFocusParkingView(focusedNode)) {
282             focusedNode.recycle();
283             return null;
284         }
285         return focusedNode;
286     }
287 
288     /**
289      * Searches {@code root} and its descendants, and returns the currently focused node, if any,
290      * or returns null if not found. The caller is responsible for recycling the result.
291      */
292     @Nullable
findFocusedNodeInRootInternal( @onNull AccessibilityNodeInfo root)293     private AccessibilityNodeInfo findFocusedNodeInRootInternal(
294             @NonNull AccessibilityNodeInfo root) {
295         AccessibilityNodeInfo surfaceView = null;
296         if (!isClientNode(root)) {
297             AccessibilityNodeInfo focusedNode = root.findFocus(FOCUS_INPUT);
298             if (focusedNode != null && Utils.isSurfaceView(focusedNode)) {
299                 // The focused node represents a SurfaceView. In this case the root node is actually
300                 // a client node but Navigator doesn't know that because SurfaceViewHelper doesn't
301                 // know the package name of the client app.
302                 // Although the package name of the client app will be stored in SurfaceViewHelper
303                 // when RotaryService handles TYPE_WINDOW_STATE_CHANGED event, RotaryService may not
304                 // receive the event. For example, RotaryService may have been killed and restarted.
305                 // In this case, Navigator should store the package name.
306                 surfaceView = focusedNode;
307                 addClientApp(surfaceView.getPackageName());
308             } else {
309                 return focusedNode;
310             }
311         }
312 
313         // The root node is in client app, which contains a SurfaceView to display the embedded
314         // view hierarchy. In this case only search inside the embedded view hierarchy.
315         if (surfaceView == null) {
316             surfaceView = findSurfaceViewInRoot(root);
317         }
318         if (surfaceView == null) {
319             L.w("Failed to find SurfaceView in client app " + root);
320             return null;
321         }
322         if (surfaceView.getChildCount() == 0) {
323             L.d("Host app is not loaded yet");
324             surfaceView.recycle();
325             return null;
326         }
327         AccessibilityNodeInfo embeddedRoot = surfaceView.getChild(0);
328         surfaceView.recycle();
329         if (embeddedRoot == null) {
330             L.w("Failed to get the root of host app");
331             return null;
332         }
333         AccessibilityNodeInfo focusedNode = embeddedRoot.findFocus(FOCUS_INPUT);
334         embeddedRoot.recycle();
335         return focusedNode;
336     }
337 
338     /**
339      * Searches the window containing {@code node}, and returns the node representing a {@link
340      * FocusParkingView}, if any, or returns null if not found. The caller is responsible for
341      * recycling the result.
342      */
343     @Nullable
findFocusParkingView(@onNull AccessibilityNodeInfo node)344     AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityNodeInfo node) {
345         AccessibilityNodeInfo root = getRoot(node);
346         AccessibilityNodeInfo fpv = findFocusParkingViewInRoot(root);
347         root.recycle();
348         return fpv;
349     }
350 
351     /**
352      * Searches {@code root} and its descendants, and returns the node representing a {@link
353      * FocusParkingView}, if any, or returns null if not found. The caller is responsible for
354      * recycling the result.
355      */
356     @Nullable
findFocusParkingViewInRoot(@onNull AccessibilityNodeInfo root)357     AccessibilityNodeInfo findFocusParkingViewInRoot(@NonNull AccessibilityNodeInfo root) {
358         return mTreeTraverser.depthFirstSearch(
359                 root,
360                 /* skipPredicate= */ Utils::isFocusArea,
361                 /* targetPredicate= */ Utils::isFocusParkingView
362         );
363     }
364 
365     /**
366      * Searches {@code root} and its descendants, and returns the node representing a {@link
367      * android.view.SurfaceView}, if any, or returns null if not found. The caller is responsible
368      * for recycling the result.
369      */
370     @Nullable
findSurfaceViewInRoot(@onNull AccessibilityNodeInfo root)371     AccessibilityNodeInfo findSurfaceViewInRoot(@NonNull AccessibilityNodeInfo root) {
372         return mTreeTraverser.depthFirstSearch(root, /* targetPredicate= */ Utils::isSurfaceView);
373     }
374 
375     /**
376      * Returns the best target focus area for a nudge in the given {@code direction}. The caller is
377      * responsible for recycling the result.
378      *
379      * @param windows          a list of windows to search from
380      * @param sourceNode       the current focus
381      * @param currentFocusArea the current focus area
382      * @param direction        nudge direction, must be {@link View#FOCUS_UP}, {@link
383      *                         View#FOCUS_DOWN}, {@link View#FOCUS_LEFT}, or {@link
384      *                         View#FOCUS_RIGHT}
385      */
findNudgeTargetFocusArea( @onNull List<AccessibilityWindowInfo> windows, @NonNull AccessibilityNodeInfo sourceNode, @NonNull AccessibilityNodeInfo currentFocusArea, int direction)386     AccessibilityNodeInfo findNudgeTargetFocusArea(
387             @NonNull List<AccessibilityWindowInfo> windows,
388             @NonNull AccessibilityNodeInfo sourceNode,
389             @NonNull AccessibilityNodeInfo currentFocusArea,
390             int direction) {
391         AccessibilityWindowInfo currentWindow = sourceNode.getWindow();
392         if (currentWindow == null) {
393             L.e("Currently focused window is null");
394             return null;
395         }
396 
397         // Build a list of candidate focus areas, starting with all the other focus areas in the
398         // same window as the current focus area.
399         List<AccessibilityNodeInfo> candidateFocusAreas = findFocusAreas(currentWindow);
400         for (AccessibilityNodeInfo focusArea : candidateFocusAreas) {
401             if (focusArea.equals(currentFocusArea)) {
402                 candidateFocusAreas.remove(focusArea);
403                 focusArea.recycle();
404                 break;
405             }
406         }
407 
408         // Add candidate focus areas in other windows in the given direction.
409         List<AccessibilityWindowInfo> candidateWindows = new ArrayList<>();
410         boolean isSourceNodeEditable = sourceNode.isEditable();
411         addWindowsInDirection(windows, currentWindow, candidateWindows, direction,
412                 isSourceNodeEditable);
413         currentWindow.recycle();
414         for (AccessibilityWindowInfo window : candidateWindows) {
415             List<AccessibilityNodeInfo> focusAreasInAnotherWindow = findFocusAreas(window);
416             candidateFocusAreas.addAll(focusAreasInAnotherWindow);
417         }
418 
419         // Exclude focus areas that have no descendants to take focus, because once we found a best
420         // candidate focus area, we don't dig into other ones. If it has no descendants to take
421         // focus, the nudge will fail.
422         removeEmptyFocusAreas(candidateFocusAreas);
423 
424         // Choose the best candidate as our target focus area.
425         AccessibilityNodeInfo targetFocusArea =
426                 chooseBestNudgeCandidate(sourceNode, candidateFocusAreas, direction);
427         Utils.recycleNodes(candidateFocusAreas);
428         return targetFocusArea;
429     }
430 
removeEmptyFocusAreas(@onNull List<AccessibilityNodeInfo> focusAreas)431     private void removeEmptyFocusAreas(@NonNull List<AccessibilityNodeInfo> focusAreas) {
432         for (Iterator<AccessibilityNodeInfo> iterator = focusAreas.iterator();
433                 iterator.hasNext(); ) {
434             AccessibilityNodeInfo focusArea = iterator.next();
435             if (!Utils.canHaveFocus(focusArea)
436                     && !containsWebViewWithFocusableDescendants(focusArea)) {
437                 iterator.remove();
438                 focusArea.recycle();
439             }
440         }
441     }
442 
containsWebViewWithFocusableDescendants(@onNull AccessibilityNodeInfo node)443     private boolean containsWebViewWithFocusableDescendants(@NonNull AccessibilityNodeInfo node) {
444         List<AccessibilityNodeInfo> webViews = new ArrayList<>();
445         mTreeTraverser.depthFirstSelect(node, Utils::isWebView, webViews);
446         if (webViews.isEmpty()) {
447             return false;
448         }
449         boolean hasFocusableDescendant = false;
450         for (AccessibilityNodeInfo webView : webViews) {
451             AccessibilityNodeInfo focusableDescendant = mTreeTraverser.depthFirstSearch(webView,
452                     Utils::canPerformFocus);
453             if (focusableDescendant != null) {
454                 hasFocusableDescendant = true;
455                 focusableDescendant.recycle();
456                 break;
457             }
458         }
459         Utils.recycleNodes(webViews);
460         return hasFocusableDescendant;
461     }
462 
463     /**
464      * Adds all the {@code windows} in the given {@code direction} of the given {@code source}
465      * window to the given list if the {@code source} window is not an overlay. If it's an overlay
466      * and the source node is editable, adds the IME window only. Otherwise does nothing.
467      */
addWindowsInDirection(@onNull List<AccessibilityWindowInfo> windows, @NonNull AccessibilityWindowInfo source, @NonNull List<AccessibilityWindowInfo> results, int direction, boolean isSourceNodeEditable)468     private void addWindowsInDirection(@NonNull List<AccessibilityWindowInfo> windows,
469             @NonNull AccessibilityWindowInfo source,
470             @NonNull List<AccessibilityWindowInfo> results,
471             int direction,
472             boolean isSourceNodeEditable) {
473         Rect sourceBounds = new Rect();
474         source.getBoundsInScreen(sourceBounds);
475 
476         // If the source window is an application window on the default display and it's smaller
477         // than the display, then it's an overlay window (such as a Dialog window). Nudging out of
478         // the overlay window is not allowed unless the source node is editable and the target
479         // window is an IME window (e.g., nudging from the EditText in the Dialog to the IME is
480         // allowed, while nudging from the Button in the Dialog to the IME is not allowed). Windows
481         // for ActivityViews are on virtual displays so they won't be considered overlay windows.
482         boolean isSourceWindowOverlayWindow = source.getType() == TYPE_APPLICATION
483                 && source.getDisplayId() == Display.DEFAULT_DISPLAY
484                 && !mAppWindowBounds.equals(sourceBounds);
485         Rect destBounds = new Rect();
486         for (AccessibilityWindowInfo window : windows) {
487             if (window.equals(source)) {
488                continue;
489             }
490             if (isSourceWindowOverlayWindow
491                     && (!isSourceNodeEditable || window.getType() != TYPE_INPUT_METHOD)) {
492                 continue;
493             }
494 
495             window.getBoundsInScreen(destBounds);
496             // Even if only part of destBounds is in the given direction of sourceBounds, we
497             // still include it because that part may contain the target focus area.
498             if (FocusFinder.isPartiallyInDirection(sourceBounds, destBounds, direction)) {
499                 results.add(window);
500             }
501         }
502     }
503 
504     /**
505      * Scans the view hierarchy of the given {@code window} looking for focus areas and returns
506      * them. If there are no explicitly declared {@link FocusArea}s, returns the root view. The
507      * caller is responsible for recycling the result.
508      */
509     @NonNull
510     @VisibleForTesting
findFocusAreas(@onNull AccessibilityWindowInfo window)511     List<AccessibilityNodeInfo> findFocusAreas(@NonNull AccessibilityWindowInfo window) {
512         List<AccessibilityNodeInfo> results = new ArrayList<>();
513         AccessibilityNodeInfo rootNode = window.getRoot();
514         if (rootNode != null) {
515             // If the root node is in the client app therefore contains a SurfaceView, skip the view
516             // hierarchy of the client app, and scan the view hierarchy of the host app, which is
517             // embedded in the SurfaceView.
518             if (isClientNode(rootNode)) {
519                 AccessibilityNodeInfo hostRoot = getDescendantHostRoot(rootNode);
520                 rootNode.recycle();
521                 rootNode = hostRoot;
522             }
523 
524             addFocusAreas(rootNode, results);
525             if (results.isEmpty()) {
526                 results.add(copyNode(rootNode));
527             }
528             rootNode.recycle();
529         }
530         return results;
531     }
532 
533     /**
534      * Searches from {@code clientNode}, and returns the root of the embedded view hierarchy if any,
535      * or returns null if not found. The caller is responsible for recycling the result.
536      */
537     @Nullable
getDescendantHostRoot(@onNull AccessibilityNodeInfo clientNode)538     AccessibilityNodeInfo getDescendantHostRoot(@NonNull AccessibilityNodeInfo clientNode) {
539         return mTreeTraverser.depthFirstSearch(clientNode, this::isHostNode);
540     }
541 
542     /**
543      * Returns whether the given window is the Heads-up Notification (HUN) window. The HUN window
544      * is identified by the left and right edges. The top and bottom vary depending on whether the
545      * HUN appears at the top or bottom of the screen and on the height of the notification being
546      * displayed so they aren't used.
547      */
isHunWindow(@ullable AccessibilityWindowInfo window)548     boolean isHunWindow(@Nullable AccessibilityWindowInfo window) {
549         if (window == null || window.getType() != AccessibilityWindowInfo.TYPE_SYSTEM) {
550             return false;
551         }
552         Rect bounds = new Rect();
553         window.getBoundsInScreen(bounds);
554         return bounds.left == mHunLeft && bounds.right == mHunRight;
555     }
556 
557     /**
558      * Returns whether the {@code window} is the main application window. A main application
559      * window is an application window on the default display that takes up the entire display.
560      */
isMainApplicationWindow(@onNull AccessibilityWindowInfo window)561     boolean isMainApplicationWindow(@NonNull AccessibilityWindowInfo window) {
562         Rect windowBounds = new Rect();
563         window.getBoundsInScreen(windowBounds);
564         return window.getType() == TYPE_APPLICATION
565                 && window.getDisplayId() == Display.DEFAULT_DISPLAY
566                 && mAppWindowBounds.equals(windowBounds);
567     }
568 
569     /**
570      * Searches from the given node up through its ancestors to the containing focus area, looking
571      * for a node that's marked as horizontally or vertically scrollable. Returns a copy of the
572      * first such node or null if none is found. The caller is responsible for recycling the result.
573      */
574     @Nullable
findScrollableContainer(@onNull AccessibilityNodeInfo node)575     AccessibilityNodeInfo findScrollableContainer(@NonNull AccessibilityNodeInfo node) {
576         return mTreeTraverser.findNodeOrAncestor(node, /* stopPredicate= */ Utils::isFocusArea,
577                 /* targetPredicate= */ Utils::isScrollableContainer);
578     }
579 
580     /**
581      * Returns the previous node  before {@code referenceNode} in Tab order that can take focus or
582      * the next node after {@code referenceNode} in Tab order that can take focus, depending on
583      * {@code direction}. The search is limited to descendants of {@code containerNode}. Returns
584      * null if there are no descendants that can take focus in the given direction. The caller is
585      * responsible for recycling the result.
586      *
587      * @param containerNode the node with descendants
588      * @param referenceNode a descendant of {@code containerNode} to start from
589      * @param direction     {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}
590      * @return the node before or after {@code referenceNode} or null if none
591      */
592     @Nullable
findFocusableDescendantInDirection( @onNull AccessibilityNodeInfo containerNode, @NonNull AccessibilityNodeInfo referenceNode, int direction)593     AccessibilityNodeInfo findFocusableDescendantInDirection(
594             @NonNull AccessibilityNodeInfo containerNode,
595             @NonNull AccessibilityNodeInfo referenceNode,
596             int direction) {
597         AccessibilityNodeInfo targetNode = copyNode(referenceNode);
598         do {
599             AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(direction);
600             if (nextTargetNode == null
601                     || nextTargetNode.equals(containerNode)
602                     || !Utils.isDescendant(containerNode, nextTargetNode)) {
603                 Utils.recycleNode(nextTargetNode);
604                 Utils.recycleNode(targetNode);
605                 return null;
606             }
607             if (nextTargetNode.equals(referenceNode) || nextTargetNode.equals(targetNode)) {
608                 L.w((direction == View.FOCUS_FORWARD ? "Next" : "Previous")
609                         + " node is the same node: " + referenceNode);
610                 Utils.recycleNode(nextTargetNode);
611                 Utils.recycleNode(targetNode);
612                 return null;
613             }
614             targetNode.recycle();
615             targetNode = nextTargetNode;
616         } while (!Utils.canTakeFocus(targetNode));
617         return targetNode;
618     }
619 
620     /**
621      * Returns the first descendant of {@code node} which can take focus. The nodes are searched in
622      * in depth-first order, not including {@code node} itself. If no descendant can take focus,
623      * null is returned. The caller is responsible for recycling the result.
624      */
625     @Nullable
findFirstFocusableDescendant(@onNull AccessibilityNodeInfo node)626     AccessibilityNodeInfo findFirstFocusableDescendant(@NonNull AccessibilityNodeInfo node) {
627         return mTreeTraverser.depthFirstSearch(node,
628                 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode));
629     }
630 
631     /**
632      * Returns the last descendant of {@code node} which can take focus. The nodes are searched in
633      * reverse depth-first order, not including {@code node} itself. If no descendant can take
634      * focus, null is returned. The caller is responsible for recycling the result.
635      */
636     @Nullable
findLastFocusableDescendant(@onNull AccessibilityNodeInfo node)637     AccessibilityNodeInfo findLastFocusableDescendant(@NonNull AccessibilityNodeInfo node) {
638         return mTreeTraverser.reverseDepthFirstSearch(node,
639                 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode));
640     }
641 
642     /**
643      * Scans descendants of the given {@code rootNode} looking for focus areas and adds them to the
644      * given list. It doesn't scan inside focus areas since nested focus areas aren't allowed. The
645      * caller is responsible for recycling added nodes.
646      *
647      * @param rootNode the root to start scanning from
648      * @param results  a list of focus areas to add to
649      */
addFocusAreas(@onNull AccessibilityNodeInfo rootNode, @NonNull List<AccessibilityNodeInfo> results)650     private void addFocusAreas(@NonNull AccessibilityNodeInfo rootNode,
651             @NonNull List<AccessibilityNodeInfo> results) {
652         mTreeTraverser.depthFirstSelect(rootNode, Utils::isFocusArea, results);
653     }
654 
655     /**
656      * Returns a copy of the best candidate from among the given {@code candidates} for a nudge
657      * from {@code sourceNode} in the given {@code direction}. Returns null if none of the {@code
658      * candidates} are in the given {@code direction}. The caller is responsible for recycling the
659      * result.
660      *
661      * @param candidates could be a list of {@link FocusArea}s, or a list of focusable views
662      */
663     @Nullable
chooseBestNudgeCandidate( @onNull AccessibilityNodeInfo sourceNode, @NonNull List<AccessibilityNodeInfo> candidates, int direction)664     private AccessibilityNodeInfo chooseBestNudgeCandidate(
665             @NonNull AccessibilityNodeInfo sourceNode,
666             @NonNull List<AccessibilityNodeInfo> candidates,
667             int direction) {
668         if (candidates.isEmpty()) {
669             return null;
670         }
671         Rect sourceBounds = Utils.getBoundsInScreen(sourceNode);
672         AccessibilityNodeInfo sourceFocusArea = getAncestorFocusArea(sourceNode);
673         Rect sourceFocusAreaBounds = Utils.getBoundsInScreen(sourceFocusArea);
674         sourceFocusArea.recycle();
675         AccessibilityNodeInfo bestNode = null;
676         Rect bestBounds = new Rect();
677 
678         for (AccessibilityNodeInfo candidate : candidates) {
679             if (isCandidate(sourceBounds, sourceFocusAreaBounds, candidate, direction)) {
680                 Rect candidateBounds = Utils.getBoundsInScreen(candidate);
681                 if (bestNode == null || FocusFinder.isBetterCandidate(
682                         direction, sourceBounds, candidateBounds, bestBounds)) {
683                     bestNode = candidate;
684                     bestBounds.set(candidateBounds);
685                 }
686             }
687         }
688         return copyNode(bestNode);
689     }
690 
691     /**
692      * Returns whether the given {@code node} is a candidate from {@code sourceBounds} to the given
693      * {@code direction}.
694      * <p>
695      * To be a candidate, the node
696      * <ul>
697      *     <li>must be considered a candidate by {@link FocusFinder#isCandidate} if it represents a
698      *         focusable view within a focus area
699      *     <li>must be in the {@code direction} of the {@code sourceFocusAreaBounds} and one of its
700      *         focusable descendants must be a candidate if it represents a focus area
701      * </ul>
702      */
isCandidate(@onNull Rect sourceBounds, @NonNull Rect sourceFocusAreaBounds, @NonNull AccessibilityNodeInfo node, int direction)703     private boolean isCandidate(@NonNull Rect sourceBounds,
704             @NonNull Rect sourceFocusAreaBounds,
705             @NonNull AccessibilityNodeInfo node,
706             int direction) {
707         AccessibilityNodeInfo candidate = mTreeTraverser.depthFirstSearch(node,
708                 /* skipPredicate= */ candidateNode -> {
709                     if (Utils.canTakeFocus(candidateNode)) {
710                         return false;
711                     }
712                     // If a node can't take focus, it represents a focus area. If the focus area
713                     // doesn't intersect with sourceFocusAreaBounds, and it's not in the given
714                     // direction of sourceFocusAreaBounds, it's not a candidate, so we should return
715                     // true to stop searching.
716                     Rect candidateBounds = Utils.getBoundsInScreen(candidateNode);
717                     return !Rect.intersects(candidateBounds,sourceFocusAreaBounds)
718                             && !FocusFinder.isInDirection(
719                                 sourceFocusAreaBounds, candidateBounds, direction);
720                 },
721                 /* targetPredicate= */ candidateNode -> {
722                     // RotaryService can navigate to nodes in a WebView even when off-screen so we
723                     // use canPerformFocus() to skip the bounds check.
724                     if (isInWebView(candidateNode)) {
725                         return Utils.canPerformFocus(candidateNode);
726                     }
727                     // If a node isn't visible to the user, e.g. another window is obscuring it,
728                     // skip it.
729                     if (!candidateNode.isVisibleToUser()) {
730                         return false;
731                     }
732                     // If a node can't take focus, it represents a focus area, so we return false to
733                     // skip the node and let it search its descendants.
734                     if (!Utils.canTakeFocus(candidateNode)) {
735                         return false;
736                     }
737                     // The node represents a focusable view in a focus area, so check the geometry.
738                     Rect candidateBounds = Utils.getBoundsInScreen(candidateNode);
739                     return FocusFinder.isCandidate(sourceBounds, candidateBounds, direction);
740                 });
741         if (candidate == null) {
742             return false;
743         }
744         candidate.recycle();
745         return true;
746     }
747 
copyNode(@ullable AccessibilityNodeInfo node)748     private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
749         return mNodeCopier.copy(node);
750     }
751 
752     /**
753      * Returns the closest ancestor focus area of the given {@code node}.
754      * <ul>
755      *     <li> If the given {@code node} is a {@link FocusArea} node or a descendant of a {@link
756      *          FocusArea} node, returns the {@link FocusArea} node.
757      *     <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this
758      *          view, and this view is not in an embedded view hierarchy, returns the root node.
759      *     <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this
760      *          view, and this view is in an embedded view hierarchy, returns the root node of
761      *          embedded view hierarchy.
762      * </ul>
763      * The caller is responsible for recycling the result.
764      */
765     @NonNull
getAncestorFocusArea(@onNull AccessibilityNodeInfo node)766     AccessibilityNodeInfo getAncestorFocusArea(@NonNull AccessibilityNodeInfo node) {
767         Predicate<AccessibilityNodeInfo> isFocusAreaOrRoot = candidateNode -> {
768             if (Utils.isFocusArea(candidateNode)) {
769                 // The candidateNode is a focus area.
770                 return true;
771             }
772             AccessibilityNodeInfo parent = candidateNode.getParent();
773             if (parent == null) {
774                 // The candidateNode is the root node.
775                 return true;
776             }
777             if (Utils.isSurfaceView(parent)) {
778                 // Treat the root of embedded view hierarchy (i.e., the only child of the
779                 // SurfaceView) as an implicit focus area.
780                 return true;
781             }
782             parent.recycle();
783             return false;
784         };
785         AccessibilityNodeInfo result = mTreeTraverser.findNodeOrAncestor(node, isFocusAreaOrRoot);
786         if (result == null || !Utils.isFocusArea(result)) {
787             L.w("Couldn't find ancestor focus area for given node: " + node);
788         }
789         return result;
790     }
791 
792     /**
793      * Returns a copy of {@code node} or the nearest ancestor that represents a {@code WebView}.
794      * Returns null if {@code node} isn't a {@code WebView} and isn't a descendant of a {@code
795      * WebView}.
796      */
797     @Nullable
findWebViewAncestor(@onNull AccessibilityNodeInfo node)798     private AccessibilityNodeInfo findWebViewAncestor(@NonNull AccessibilityNodeInfo node) {
799         return mTreeTraverser.findNodeOrAncestor(node, Utils::isWebView);
800     }
801 
802     /** Returns whether {@code node} is a {@code WebView} or is a descendant of one. */
isInWebView(@onNull AccessibilityNodeInfo node)803     boolean isInWebView(@NonNull AccessibilityNodeInfo node) {
804         AccessibilityNodeInfo webView = findWebViewAncestor(node);
805         if (webView == null) {
806             return false;
807         }
808         webView.recycle();
809         return true;
810     }
811 
812     /**
813      * Returns the next focusable node after {@code candidate} in {@code direction} in {@code
814      * webView} or null if none. This handles navigating into a WebView as well as within a WebView.
815      */
816     @Nullable
findNextFocusableInWebView(@onNull AccessibilityNodeInfo webView, @NonNull AccessibilityNodeInfo candidate, int direction)817     private AccessibilityNodeInfo findNextFocusableInWebView(@NonNull AccessibilityNodeInfo webView,
818             @NonNull AccessibilityNodeInfo candidate, int direction) {
819         // focusSearch() doesn't work in WebViews so use tree traversal instead.
820         if (Utils.isWebView(candidate)) {
821             if (direction == View.FOCUS_FORWARD) {
822                 // When entering into a WebView, find the first focusable node within the
823                 // WebView if any.
824                 return findFirstFocusableDescendantInWebView(candidate);
825             } else {
826                 // When backing into a WebView, find the last focusable node within the
827                 // WebView if any.
828                 return findLastFocusableDescendantInWebView(candidate);
829             }
830         } else {
831             // When navigating within a WebView, find the next or previous focusable node in
832             // depth-first order.
833             if (direction == View.FOCUS_FORWARD) {
834                 return findFirstFocusDescendantInWebViewAfter(webView, candidate);
835             } else {
836                 return findFirstFocusDescendantInWebViewBefore(webView, candidate);
837             }
838         }
839     }
840 
841     /**
842      * Returns the first descendant of {@code webView} which can perform focus. This includes off-
843      * screen descendants. The nodes are searched in in depth-first order, not including
844      * {@code webView} itself. If no descendant can perform focus, null is returned. The caller is
845      * responsible for recycling the result.
846      */
847     @Nullable
findFirstFocusableDescendantInWebView( @onNull AccessibilityNodeInfo webView)848     private AccessibilityNodeInfo findFirstFocusableDescendantInWebView(
849             @NonNull AccessibilityNodeInfo webView) {
850         return mTreeTraverser.depthFirstSearch(webView,
851                 candidateNode -> candidateNode != webView && Utils.canPerformFocus(candidateNode));
852     }
853 
854     /**
855      * Returns the last descendant of {@code webView} which can perform focus. This includes off-
856      * screen descendants. The nodes are searched in reverse depth-first order, not including
857      * {@code webView} itself. If no descendant can perform focus, null is returned. The caller is
858      * responsible for recycling the result.
859      */
860     @Nullable
findLastFocusableDescendantInWebView( @onNull AccessibilityNodeInfo webView)861     private AccessibilityNodeInfo findLastFocusableDescendantInWebView(
862             @NonNull AccessibilityNodeInfo webView) {
863         return mTreeTraverser.reverseDepthFirstSearch(webView,
864                 candidateNode -> candidateNode != webView && Utils.canPerformFocus(candidateNode));
865     }
866 
867     @Nullable
findFirstFocusDescendantInWebViewBefore( @onNull AccessibilityNodeInfo webView, @NonNull AccessibilityNodeInfo beforeNode)868     private AccessibilityNodeInfo findFirstFocusDescendantInWebViewBefore(
869             @NonNull AccessibilityNodeInfo webView, @NonNull AccessibilityNodeInfo beforeNode) {
870         boolean[] foundBeforeNode = new boolean[1];
871         return mTreeTraverser.reverseDepthFirstSearch(webView,
872                 node -> {
873                     if (foundBeforeNode[0] && Utils.canPerformFocus(node)) {
874                         return true;
875                     }
876                     if (node.equals(beforeNode)) {
877                         foundBeforeNode[0] = true;
878                     }
879                     return false;
880                 });
881     }
882 
883     @Nullable
884     private AccessibilityNodeInfo findFirstFocusDescendantInWebViewAfter(
885             @NonNull AccessibilityNodeInfo webView, @NonNull AccessibilityNodeInfo afterNode) {
886         boolean[] foundAfterNode = new boolean[1];
887         return mTreeTraverser.depthFirstSearch(webView,
888                 node -> {
889                     if (foundAfterNode[0] && Utils.canPerformFocus(node)) {
890                         return true;
891                     }
892                     if (node.equals(afterNode)) {
893                         foundAfterNode[0] = true;
894                     }
895                     return false;
896                 });
897     }
898 
899     void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
900         writer.println("  hunLeft: " + mHunLeft + ", right: " + mHunRight);
901         writer.println("  hunNudgeDirection: " + directionToString(mHunNudgeDirection));
902         writer.println("  appWindowBounds: " + mAppWindowBounds);
903 
904         writer.println("  surfaceViewHelper:");
905         mSurfaceViewHelper.dump(fd, writer, args);
906     }
907 
908     static String directionToString(@View.FocusRealDirection int direction) {
909         switch (direction) {
910             case View.FOCUS_UP:
911                 return "FOCUS_UP";
912             case View.FOCUS_DOWN:
913                 return "FOCUS_DOWN";
914             case View.FOCUS_LEFT:
915                 return "FOCUS_LEFT";
916             case View.FOCUS_RIGHT:
917                 return "FOCUS_RIGHT";
918             default:
919                 return "<unknown direction " + direction + ">";
920         }
921     }
922 
923     /** Result from {@link #findRotateTarget}. */
924     static class FindRotateTargetResult {
925         @NonNull final AccessibilityNodeInfo node;
926         final int advancedCount;
927 
928         FindRotateTargetResult(@NonNull AccessibilityNodeInfo node, int advancedCount) {
929             this.node = node;
930             this.advancedCount = advancedCount;
931         }
932     }
933 }
934