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 if (webViewHasFocusableDescendants(webView)) { 563 hasFocusableDescendant = true; 564 break; 565 } 566 } 567 Utils.recycleNodes(webViews); 568 return hasFocusableDescendant; 569 } 570 webViewHasFocusableDescendants(@onNull AccessibilityNodeInfo webView)571 private boolean webViewHasFocusableDescendants(@NonNull AccessibilityNodeInfo webView) { 572 AccessibilityNodeInfo focusableDescendant = mTreeTraverser.depthFirstSearch(webView, 573 Utils::canPerformFocus); 574 if (focusableDescendant == null) { 575 return false; 576 } 577 focusableDescendant.recycle(); 578 return true; 579 } 580 isWebViewWithFocusableDescendants(@onNull AccessibilityNodeInfo node)581 private boolean isWebViewWithFocusableDescendants(@NonNull AccessibilityNodeInfo node) { 582 return Utils.isWebView(node) && webViewHasFocusableDescendants(node); 583 } 584 585 /** 586 * Adds all the {@code windows} in the given {@code direction} of the given {@code source} 587 * window to the given list if the {@code source} window is not an overlay. If it's an overlay 588 * and the source node is editable, adds the IME window only. Otherwise does nothing. 589 */ addWindowsInDirection(@onNull List<AccessibilityWindowInfo> windows, @NonNull AccessibilityWindowInfo source, @NonNull List<AccessibilityWindowInfo> results, int direction, boolean isSourceNodeEditable)590 private void addWindowsInDirection(@NonNull List<AccessibilityWindowInfo> windows, 591 @NonNull AccessibilityWindowInfo source, 592 @NonNull List<AccessibilityWindowInfo> results, 593 int direction, 594 boolean isSourceNodeEditable) { 595 Rect sourceBounds = new Rect(); 596 source.getBoundsInScreen(sourceBounds); 597 boolean isSourceWindowOverlayWindow = isOverlayWindow(source, sourceBounds); 598 Rect destBounds = new Rect(); 599 for (AccessibilityWindowInfo window : windows) { 600 if (window.equals(source)) { 601 continue; 602 } 603 // Nudging out of the overlay window is not allowed unless the source node is editable 604 // and the target window is an IME window. E.g., nudging from the EditText in the Dialog 605 // to the IME is allowed, while nudging from the Button in the Dialog to the IME is not 606 // allowed. 607 if (isSourceWindowOverlayWindow 608 && (!isSourceNodeEditable || window.getType() != TYPE_INPUT_METHOD)) { 609 continue; 610 } 611 612 window.getBoundsInScreen(destBounds); 613 // Even if only part of destBounds is in the given direction of sourceBounds, we 614 // still include it because that part may contain the target focus area. 615 if (FocusFinder.isPartiallyInDirection(sourceBounds, destBounds, direction)) { 616 results.add(window); 617 } 618 } 619 } 620 621 /** 622 * Returns whether the given {@code window} with the given {@code bounds} is an overlay window. 623 * <p> 624 * If the source window is an application window on the default display and it's smaller than 625 the display, then it's either a TaskView window or an overlay window (such as a Dialog 626 window). The ID of a TaskView task is different from the full screen application, while 627 the ID of an overlay task is the same with the full screen application, so task ID is used 628 to decide whether it's an overlay window. 629 */ isOverlayWindow(@onNull AccessibilityWindowInfo window, @NonNull Rect bounds)630 private boolean isOverlayWindow(@NonNull AccessibilityWindowInfo window, @NonNull Rect bounds) { 631 return window.getType() == TYPE_APPLICATION 632 && window.getDisplayId() == Display.DEFAULT_DISPLAY 633 && !mAppWindowBounds.equals(bounds) 634 && window.getTaskId() == mAppWindowTaskId; 635 } 636 637 /** 638 * Returns whether nudging to the given {@code direction} can dismiss the given {@code window} 639 * with the given {@code bounds}. 640 */ isDismissible(@onNull AccessibilityWindowInfo window, @NonNull Rect bounds, @View.FocusRealDirection int direction)641 boolean isDismissible(@NonNull AccessibilityWindowInfo window, 642 @NonNull Rect bounds, 643 @View.FocusRealDirection int direction) { 644 // Only overlay windows can be dismissed. 645 if (!isOverlayWindow(window, bounds)) { 646 return false; 647 } 648 // The window can be dismissed when part of the underlying window is not covered by it in 649 // the given direction. 650 switch (direction) { 651 case FOCUS_UP: 652 return mAppWindowBounds.top < bounds.top; 653 case FOCUS_DOWN: 654 return mAppWindowBounds.bottom > bounds.bottom; 655 case FOCUS_LEFT: 656 return mAppWindowBounds.left < bounds.left; 657 case FOCUS_RIGHT: 658 return mAppWindowBounds.right > bounds.right; 659 } 660 return false; 661 } 662 663 /** 664 * Scans the view hierarchy of the given {@code window} looking for explicit focus areas with 665 * focusable descendants and returns the focus areas. The caller is responsible for recycling 666 * the result. 667 */ 668 @NonNull 669 @VisibleForTesting findNonEmptyFocusAreas(@onNull AccessibilityWindowInfo window)670 List<AccessibilityNodeInfo> findNonEmptyFocusAreas(@NonNull AccessibilityWindowInfo window) { 671 List<AccessibilityNodeInfo> results = new ArrayList<>(); 672 AccessibilityNodeInfo rootNode = window.getRoot(); 673 if (rootNode == null) { 674 L.e("No root node for " + window); 675 } else if (!isClientNode(rootNode)) { 676 addNonEmptyFocusAreas(rootNode, results); 677 } 678 // If the root node is in the client app, it won't contain any explicit focus areas, so 679 // skip it. 680 681 Utils.recycleNode(rootNode); 682 return results; 683 } 684 685 /** 686 * Searches from {@code clientNode}, and returns the root of the embedded view hierarchy if any, 687 * or returns null if not found. The caller is responsible for recycling the result. 688 */ 689 @Nullable getDescendantHostRoot(@onNull AccessibilityNodeInfo clientNode)690 AccessibilityNodeInfo getDescendantHostRoot(@NonNull AccessibilityNodeInfo clientNode) { 691 return mTreeTraverser.depthFirstSearch(clientNode, this::isHostNode); 692 } 693 694 /** 695 * Returns whether the given window is the Heads-up Notification (HUN) window. The HUN window 696 * is identified by the left and right edges. The top and bottom vary depending on whether the 697 * HUN appears at the top or bottom of the screen and on the height of the notification being 698 * displayed so they aren't used. 699 */ isHunWindow(@ullable AccessibilityWindowInfo window)700 boolean isHunWindow(@Nullable AccessibilityWindowInfo window) { 701 if (window == null || window.getType() != AccessibilityWindowInfo.TYPE_SYSTEM) { 702 return false; 703 } 704 Rect bounds = new Rect(); 705 window.getBoundsInScreen(bounds); 706 return bounds.left == mHunLeft && bounds.right == mHunRight; 707 } 708 709 /** 710 * Searches from the given node up through its ancestors to the containing focus area, looking 711 * for a node that's marked as horizontally or vertically scrollable. Returns a copy of the 712 * first such node or null if none is found. The caller is responsible for recycling the result. 713 */ 714 @Nullable findScrollableContainer(@onNull AccessibilityNodeInfo node)715 AccessibilityNodeInfo findScrollableContainer(@NonNull AccessibilityNodeInfo node) { 716 return mTreeTraverser.findNodeOrAncestor(node, /* stopPredicate= */ Utils::isFocusArea, 717 /* targetPredicate= */ Utils::isScrollableContainer); 718 } 719 720 /** 721 * Returns the previous node before {@code referenceNode} in Tab order that can take focus or 722 * the next node after {@code referenceNode} in Tab order that can take focus, depending on 723 * {@code direction}. The search is limited to descendants of {@code containerNode}. Returns 724 * null if there are no descendants that can take focus in the given direction. The caller is 725 * responsible for recycling the result. 726 * 727 * @param containerNode the node with descendants 728 * @param referenceNode a descendant of {@code containerNode} to start from 729 * @param direction {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD} 730 * @return the node before or after {@code referenceNode} or null if none 731 */ 732 @Nullable findFocusableDescendantInDirection( @onNull AccessibilityNodeInfo containerNode, @NonNull AccessibilityNodeInfo referenceNode, int direction)733 AccessibilityNodeInfo findFocusableDescendantInDirection( 734 @NonNull AccessibilityNodeInfo containerNode, 735 @NonNull AccessibilityNodeInfo referenceNode, 736 int direction) { 737 AccessibilityNodeInfo targetNode = copyNode(referenceNode); 738 do { 739 AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(direction); 740 if (nextTargetNode == null 741 || nextTargetNode.equals(containerNode) 742 || !Utils.isDescendant(containerNode, nextTargetNode)) { 743 Utils.recycleNode(nextTargetNode); 744 Utils.recycleNode(targetNode); 745 return null; 746 } 747 if (nextTargetNode.equals(referenceNode) || nextTargetNode.equals(targetNode)) { 748 L.w((direction == View.FOCUS_FORWARD ? "Next" : "Previous") 749 + " node is the same node: " + referenceNode); 750 Utils.recycleNode(nextTargetNode); 751 Utils.recycleNode(targetNode); 752 return null; 753 } 754 targetNode.recycle(); 755 targetNode = nextTargetNode; 756 } while (!Utils.canTakeFocus(targetNode)); 757 return targetNode; 758 } 759 760 /** 761 * Returns the first descendant of {@code node} which can take focus. The nodes are searched in 762 * in depth-first order, not including {@code node} itself. If no descendant can take focus, 763 * null is returned. The caller is responsible for recycling the result. 764 */ 765 @Nullable findFirstFocusableDescendant(@onNull AccessibilityNodeInfo node)766 AccessibilityNodeInfo findFirstFocusableDescendant(@NonNull AccessibilityNodeInfo node) { 767 return mTreeTraverser.depthFirstSearch(node, 768 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode)); 769 } 770 771 /** 772 * Returns the first orphan descendant (focusable descendant not inside any focus areas) of 773 * {@code node}. The nodes are searched in depth-first order, not including {@code node} itself. 774 * If not found, null is returned. The caller is responsible for recycling the result. 775 */ 776 @Nullable findFirstOrphan(@onNull AccessibilityNodeInfo node)777 AccessibilityNodeInfo findFirstOrphan(@NonNull AccessibilityNodeInfo node) { 778 return mTreeTraverser.depthFirstSearch(node, 779 /* skipPredicate= */ Utils::isFocusArea, 780 /* targetPredicate= */ candidateNode -> candidateNode != node 781 && Utils.canTakeFocus(candidateNode)); 782 } 783 784 /** 785 * Returns the last descendant of {@code node} which can take focus. The nodes are searched in 786 * reverse depth-first order, not including {@code node} itself. If no descendant can take 787 * focus, null is returned. The caller is responsible for recycling the result. 788 */ 789 @Nullable findLastFocusableDescendant(@onNull AccessibilityNodeInfo node)790 AccessibilityNodeInfo findLastFocusableDescendant(@NonNull AccessibilityNodeInfo node) { 791 return mTreeTraverser.reverseDepthFirstSearch(node, 792 candidateNode -> candidateNode != node && Utils.canTakeFocus(candidateNode)); 793 } 794 795 /** 796 * Scans descendants of the given {@code rootNode} looking for explicit focus areas with 797 * focusable descendants and adds the focus areas to the given list. It doesn't scan inside 798 * focus areas since nested focus areas aren't allowed. It ignores focus areas without 799 * focusable descendants, because once we found the best candidate focus area, we don't dig 800 * into other ones. If it has no descendants to take focus, the nudge will fail. The caller is 801 * responsible for recycling added nodes. 802 * 803 * @param rootNode the root to start scanning from 804 * @param results a list of focus areas to add to 805 */ addNonEmptyFocusAreas(@onNull AccessibilityNodeInfo rootNode, @NonNull List<AccessibilityNodeInfo> results)806 private void addNonEmptyFocusAreas(@NonNull AccessibilityNodeInfo rootNode, 807 @NonNull List<AccessibilityNodeInfo> results) { 808 mTreeTraverser.depthFirstSelect(rootNode, 809 (focusArea) -> Utils.isFocusArea(focusArea) && hasFocusableDescendants(focusArea), 810 results); 811 } 812 hasFocusableDescendants(@onNull AccessibilityNodeInfo focusArea)813 private boolean hasFocusableDescendants(@NonNull AccessibilityNodeInfo focusArea) { 814 return Utils.canHaveFocus(focusArea) || containsWebViewWithFocusableDescendants(focusArea); 815 } 816 817 /** 818 * Returns the minimum rectangle wrapping the given {@code node}'s orphan descendants. If 819 * {@code node} has no orphan descendants, returns an empty {@link Rect}. 820 */ 821 @NonNull 822 @VisibleForTesting computeMinimumBoundsForOrphanDescendants( @onNull AccessibilityNodeInfo node)823 Rect computeMinimumBoundsForOrphanDescendants( 824 @NonNull AccessibilityNodeInfo node) { 825 Rect bounds = new Rect(); 826 if (Utils.isFocusArea(node) || Utils.isFocusParkingView(node)) { 827 return bounds; 828 } 829 if (Utils.canTakeFocus(node) || isWebViewWithFocusableDescendants(node)) { 830 return Utils.getBoundsInScreen(node); 831 } 832 for (int i = 0; i < node.getChildCount(); i++) { 833 AccessibilityNodeInfo child = node.getChild(i); 834 if (child == null) { 835 continue; 836 } 837 Rect childBounds = computeMinimumBoundsForOrphanDescendants(child); 838 child.recycle(); 839 if (childBounds != null) { 840 bounds.union(childBounds); 841 } 842 } 843 return bounds; 844 } 845 846 /** 847 * Returns a copy of the best candidate from among the given {@code candidates} for a nudge 848 * from {@code sourceNode} in the given {@code direction}. Returns null if none of the {@code 849 * candidates} are in the given {@code direction}. The caller is responsible for recycling the 850 * result. 851 * 852 * @param candidates could be a list of {@link FocusArea}s, or a list of focusable views 853 */ 854 @Nullable chooseBestNudgeCandidate(@onNull Rect sourceBounds, @NonNull Rect sourceFocusAreaBounds, @NonNull List<AccessibilityNodeInfo> candidates, @NonNull List<Rect> candidatesBounds, int direction)855 private AccessibilityNodeInfo chooseBestNudgeCandidate(@NonNull Rect sourceBounds, 856 @NonNull Rect sourceFocusAreaBounds, 857 @NonNull List<AccessibilityNodeInfo> candidates, 858 @NonNull List<Rect> candidatesBounds, 859 int direction) { 860 if (candidates.isEmpty()) { 861 return null; 862 } 863 AccessibilityNodeInfo bestNode = null; 864 Rect bestBounds = new Rect(); 865 for (int i = 0; i < candidates.size(); i++) { 866 AccessibilityNodeInfo candidate = candidates.get(i); 867 Rect candidateBounds = candidatesBounds.get(i); 868 if (isCandidate(sourceBounds, sourceFocusAreaBounds, candidate, candidateBounds, 869 direction)) { 870 if (bestNode == null || FocusFinder.isBetterCandidate( 871 direction, sourceBounds, candidateBounds, bestBounds)) { 872 bestNode = candidate; 873 bestBounds.set(candidateBounds); 874 } 875 } 876 } 877 return copyNode(bestNode); 878 } 879 880 /** 881 * Returns whether the given {@code node} is a candidate from {@code sourceBounds} to the given 882 * {@code direction}. 883 * <p> 884 * To be a candidate, the node 885 * <ul> 886 * <li>must be considered a candidate by {@link FocusFinder#isCandidate} if it represents a 887 * focusable view within a focus area 888 * <li>must be in the {@code direction} of the {@code sourceFocusAreaBounds} and one of its 889 * focusable descendants must be a candidate if it represents a focus area 890 * </ul> 891 */ isCandidate(@onNull Rect sourceBounds, @NonNull Rect sourceFocusAreaBounds, @NonNull AccessibilityNodeInfo node, @NonNull Rect nodeBounds, int direction)892 private boolean isCandidate(@NonNull Rect sourceBounds, 893 @NonNull Rect sourceFocusAreaBounds, 894 @NonNull AccessibilityNodeInfo node, 895 @NonNull Rect nodeBounds, 896 int direction) { 897 AccessibilityNodeInfo candidate = mTreeTraverser.depthFirstSearch(node, 898 /* skipPredicate= */ candidateNode -> { 899 if (Utils.canTakeFocus(candidateNode)) { 900 return false; 901 } 902 // If a node can't take focus, it represents a focus area. If the focus area 903 // doesn't intersect with sourceFocusAreaBounds, and it's not in the given 904 // direction of sourceFocusAreaBounds, it's not a candidate, so we should return 905 // true to stop searching. 906 return !Rect.intersects(nodeBounds, sourceFocusAreaBounds) 907 && !FocusFinder.isInDirection( 908 sourceFocusAreaBounds, nodeBounds, direction); 909 }, 910 /* targetPredicate= */ candidateNode -> { 911 // RotaryService can navigate to nodes in a WebView or a ComposeView even when 912 // off-screen, so we use canPerformFocus() to skip the bounds check. 913 if (isInVirtualNodeHierarchy(candidateNode)) { 914 return Utils.canPerformFocus(candidateNode); 915 } 916 // If a node isn't visible to the user, e.g. another window is obscuring it, 917 // skip it. 918 if (!candidateNode.isVisibleToUser()) { 919 return false; 920 } 921 // If a node can't take focus, it represents a focus area, so we return false to 922 // skip the node and let it search its descendants. 923 if (!Utils.canTakeFocus(candidateNode)) { 924 return false; 925 } 926 // The node represents a focusable view in a focus area, so check the geometry. 927 return FocusFinder.isCandidate(sourceBounds, nodeBounds, direction); 928 }); 929 if (candidate == null) { 930 return false; 931 } 932 candidate.recycle(); 933 return true; 934 } 935 copyNode(@ullable AccessibilityNodeInfo node)936 private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) { 937 return mNodeCopier.copy(node); 938 } 939 940 /** 941 * Returns the closest ancestor focus area of the given {@code node}. 942 * <ul> 943 * <li> If the given {@code node} is a {@link FocusArea} node or a descendant of a {@link 944 * FocusArea} node, returns the {@link FocusArea} node. 945 * <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this 946 * view, and this view is not in an embedded view hierarchy, returns the root node. 947 * <li> If there are no explicitly declared {@link FocusArea}s among the ancestors of this 948 * view, and this view is in an embedded view hierarchy, returns the root node of 949 * embedded view hierarchy. 950 * </ul> 951 * The caller is responsible for recycling the result. 952 */ 953 @NonNull getAncestorFocusArea(@onNull AccessibilityNodeInfo node)954 AccessibilityNodeInfo getAncestorFocusArea(@NonNull AccessibilityNodeInfo node) { 955 Predicate<AccessibilityNodeInfo> isFocusAreaOrRoot = candidateNode -> { 956 if (Utils.isFocusArea(candidateNode)) { 957 // The candidateNode is a focus area. 958 return true; 959 } 960 AccessibilityNodeInfo parent = candidateNode.getParent(); 961 if (parent == null) { 962 // The candidateNode is the root node. 963 return true; 964 } 965 if (Utils.isSurfaceView(parent)) { 966 // Treat the root of embedded view hierarchy (i.e., the only child of the 967 // SurfaceView) as an implicit focus area. 968 return true; 969 } 970 parent.recycle(); 971 return false; 972 }; 973 AccessibilityNodeInfo result = mTreeTraverser.findNodeOrAncestor(node, isFocusAreaOrRoot); 974 if (result == null || !Utils.isFocusArea(result)) { 975 L.w("Couldn't find ancestor focus area for given node: " + node); 976 } 977 return result; 978 } 979 980 /** 981 * Returns a copy of {@code node} or the nearest ancestor that represents a {@code WebView}. 982 * Returns null if {@code node} isn't a {@code WebView} and isn't a descendant of a {@code 983 * WebView}. 984 */ 985 @Nullable findWebViewAncestor(@onNull AccessibilityNodeInfo node)986 private AccessibilityNodeInfo findWebViewAncestor(@NonNull AccessibilityNodeInfo node) { 987 return mTreeTraverser.findNodeOrAncestor(node, Utils::isWebView); 988 } 989 990 /** 991 * Returns a copy of {@code node} or the nearest ancestor that represents a {@code ComposeView} 992 * or a {@code WebView}. Returns null if {@code node} isn't a {@code ComposeView} or a 993 * {@code WebView} and is not a descendant of a {@code ComposeView} or a {@code WebView}. 994 * 995 * TODO(b/192274274): This method may not be necessary anymore if Compose supports focusSearch. 996 */ 997 @Nullable findVirtualViewAncestor(@onNull AccessibilityNodeInfo node)998 private AccessibilityNodeInfo findVirtualViewAncestor(@NonNull AccessibilityNodeInfo node) { 999 return mTreeTraverser.findNodeOrAncestor(node, /* targetPredicate= */ (nodeInfo) -> 1000 Utils.isComposeView(nodeInfo) || Utils.isWebView(nodeInfo)); 1001 } 1002 1003 /** Returns whether {@code node} is a {@code WebView} or is a descendant of one. */ isInWebView(@onNull AccessibilityNodeInfo node)1004 boolean isInWebView(@NonNull AccessibilityNodeInfo node) { 1005 AccessibilityNodeInfo webView = findWebViewAncestor(node); 1006 if (webView == null) { 1007 return false; 1008 } 1009 webView.recycle(); 1010 return true; 1011 } 1012 1013 /** 1014 * Returns whether {@code node} is a {@code ComposeView}, is a {@code WebView}, or is a 1015 * descendant of either. 1016 */ isInVirtualNodeHierarchy(@onNull AccessibilityNodeInfo node)1017 boolean isInVirtualNodeHierarchy(@NonNull AccessibilityNodeInfo node) { 1018 AccessibilityNodeInfo virtualViewAncestor = findVirtualViewAncestor(node); 1019 if (virtualViewAncestor == null) { 1020 return false; 1021 } 1022 virtualViewAncestor.recycle(); 1023 return true; 1024 } 1025 1026 /** 1027 * Returns the next focusable node after {@code candidate} in {@code direction} in {@code 1028 * root} or null if none. This handles navigating into a WebView as well as within a WebView. 1029 * This also handles navigating into a ComposeView, as well as within a ComposeView. 1030 */ 1031 @Nullable findNextFocusableInVirtualRoot( @onNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo candidate, int direction)1032 private AccessibilityNodeInfo findNextFocusableInVirtualRoot( 1033 @NonNull AccessibilityNodeInfo root, 1034 @NonNull AccessibilityNodeInfo candidate, int direction) { 1035 // focusSearch() doesn't work in WebViews or ComposeViews so use tree traversal instead. 1036 if (Utils.isWebView(candidate) || Utils.isComposeView(candidate)) { 1037 if (direction == View.FOCUS_FORWARD) { 1038 // When entering into the root of a virtual node hierarchy, find the first focusable 1039 // child node of the root if any. 1040 return findFirstFocusableDescendantInVirtualRoot(candidate); 1041 } else { 1042 // When backing into the root of a virtual node hierarchy, find the last focusable 1043 // child node of the root if any. 1044 return findLastFocusableDescendantInVirtualRoot(candidate); 1045 } 1046 } else { 1047 // When navigating within a virtual view hierarchy, find the next or previous focusable 1048 // node in depth-first order. 1049 if (direction == View.FOCUS_FORWARD) { 1050 return findFirstFocusDescendantInVirtualRootAfter(root, candidate); 1051 } else { 1052 return findFirstFocusDescendantInVirtualRootBefore(root, candidate); 1053 } 1054 } 1055 } 1056 1057 /** 1058 * Returns the first descendant of {@code webView} which can perform focus. This includes off- 1059 * screen descendants. The nodes are searched in in depth-first order, not including 1060 * {@code root} itself. If no descendant can perform focus, null is returned. The caller is 1061 * responsible for recycling the result. 1062 */ 1063 @Nullable findFirstFocusableDescendantInVirtualRoot( @onNull AccessibilityNodeInfo root)1064 private AccessibilityNodeInfo findFirstFocusableDescendantInVirtualRoot( 1065 @NonNull AccessibilityNodeInfo root) { 1066 return mTreeTraverser.depthFirstSearch(root, 1067 candidateNode -> candidateNode != root && Utils.canPerformFocus(candidateNode)); 1068 } 1069 1070 /** 1071 * Returns the last descendant of {@code root} which can perform focus. This includes off- 1072 * screen descendants. The nodes are searched in reverse depth-first order, not including 1073 * {@code root} itself. If no descendant can perform focus, null is returned. The caller is 1074 * responsible for recycling the result. 1075 */ 1076 @Nullable findLastFocusableDescendantInVirtualRoot( @onNull AccessibilityNodeInfo root)1077 private AccessibilityNodeInfo findLastFocusableDescendantInVirtualRoot( 1078 @NonNull AccessibilityNodeInfo root) { 1079 return mTreeTraverser.reverseDepthFirstSearch(root, 1080 candidateNode -> candidateNode != root && Utils.canPerformFocus(candidateNode)); 1081 } 1082 1083 @Nullable findFirstFocusDescendantInVirtualRootBefore( @onNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo beforeNode)1084 private AccessibilityNodeInfo findFirstFocusDescendantInVirtualRootBefore( 1085 @NonNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo beforeNode) { 1086 boolean[] foundBeforeNode = new boolean[1]; 1087 return mTreeTraverser.reverseDepthFirstSearch(root, 1088 node -> { 1089 if (foundBeforeNode[0] && Utils.canPerformFocus(node)) { 1090 return true; 1091 } 1092 if (node.equals(beforeNode)) { 1093 foundBeforeNode[0] = true; 1094 } 1095 return false; 1096 }); 1097 } 1098 1099 @Nullable 1100 private AccessibilityNodeInfo findFirstFocusDescendantInVirtualRootAfter( 1101 @NonNull AccessibilityNodeInfo root, @NonNull AccessibilityNodeInfo afterNode) { 1102 boolean[] foundAfterNode = new boolean[1]; 1103 return mTreeTraverser.depthFirstSearch(root, 1104 node -> { 1105 if (foundAfterNode[0] && Utils.canPerformFocus(node)) { 1106 return true; 1107 } 1108 if (node.equals(afterNode)) { 1109 foundAfterNode[0] = true; 1110 } 1111 return false; 1112 }); 1113 } 1114 1115 void dump(@NonNull DualDumpOutputStream dumpOutputStream, boolean dumpAsProto, 1116 @NonNull String fieldName, long fieldId) { 1117 long fieldToken = dumpOutputStream.start(fieldName, fieldId); 1118 dumpOutputStream.write("hunLeft", RotaryProtos.Navigator.HUN_LEFT, mHunLeft); 1119 dumpOutputStream.write("hunRight", RotaryProtos.Navigator.HUN_RIGHT, mHunRight); 1120 DumpUtils.writeFocusDirection(dumpOutputStream, dumpAsProto, "hunNudgeDirection", 1121 RotaryProtos.Navigator.HUN_NUDGE_DIRECTION, mHunNudgeDirection); 1122 DumpUtils.writeRect(dumpOutputStream, mAppWindowBounds, "appWindowBounds", 1123 RotaryProtos.Navigator.APP_WINDOW_BOUNDS); 1124 mSurfaceViewHelper.dump(dumpOutputStream, dumpAsProto, "surfaceViewHelper", 1125 RotaryProtos.Navigator.SURFACE_VIEW_HELPER); 1126 dumpOutputStream.end(fieldToken); 1127 } 1128 1129 static String directionToString(@View.FocusRealDirection int direction) { 1130 switch (direction) { 1131 case FOCUS_UP: 1132 return "FOCUS_UP"; 1133 case FOCUS_DOWN: 1134 return "FOCUS_DOWN"; 1135 case FOCUS_LEFT: 1136 return "FOCUS_LEFT"; 1137 case FOCUS_RIGHT: 1138 return "FOCUS_RIGHT"; 1139 default: 1140 return "<unknown direction " + direction + ">"; 1141 } 1142 } 1143 1144 /** Result from {@link #findRotateTarget}. */ 1145 static class FindRotateTargetResult { 1146 @NonNull final AccessibilityNodeInfo node; 1147 final int advancedCount; 1148 1149 FindRotateTargetResult(@NonNull AccessibilityNodeInfo node, int advancedCount) { 1150 this.node = node; 1151 this.advancedCount = advancedCount; 1152 } 1153 } 1154 } 1155