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