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