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