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