1 /* 2 * Copyright (C) 2012 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 17 package android.view.accessibility; 18 19 import android.os.Build; 20 import android.util.ArraySet; 21 import android.util.Log; 22 import android.util.LongArray; 23 import android.util.LongSparseArray; 24 import android.util.SparseArray; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 29 /** 30 * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos. 31 * It is updated when windows change or nodes change. 32 * @hide 33 */ 34 public class AccessibilityCache { 35 36 private static final String LOG_TAG = "AccessibilityCache"; 37 38 private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE; 39 40 private static final boolean VERBOSE = 41 Log.isLoggable(LOG_TAG, Log.VERBOSE) && Build.IS_DEBUGGABLE; 42 43 private static final boolean CHECK_INTEGRITY = Build.IS_ENG; 44 45 /** 46 * {@link AccessibilityEvent} types that are critical for the cache to stay up to date 47 * 48 * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to 49 * make sure that the events are delivered to cache regardless of 50 * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes} 51 */ 52 public static final int CACHE_CRITICAL_EVENTS_MASK = 53 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED 54 | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 55 | AccessibilityEvent.TYPE_VIEW_FOCUSED 56 | AccessibilityEvent.TYPE_VIEW_SELECTED 57 | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED 58 | AccessibilityEvent.TYPE_VIEW_CLICKED 59 | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED 60 | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED 61 | AccessibilityEvent.TYPE_VIEW_SCROLLED 62 | AccessibilityEvent.TYPE_WINDOWS_CHANGED 63 | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; 64 65 private final Object mLock = new Object(); 66 67 private final AccessibilityNodeRefresher mAccessibilityNodeRefresher; 68 69 private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 70 private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 71 72 private int mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 73 74 private boolean mIsAllWindowsCached; 75 76 // The SparseArray of all {@link AccessibilityWindowInfo}s on all displays. 77 // The key of outer SparseArray is display ID and the key of inner SparseArray is window ID. 78 private final SparseArray<SparseArray<AccessibilityWindowInfo>> mWindowCacheByDisplay = 79 new SparseArray<>(); 80 81 private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache = 82 new SparseArray<>(); 83 84 private final SparseArray<AccessibilityWindowInfo> mTempWindowArray = 85 new SparseArray<>(); 86 AccessibilityCache(AccessibilityNodeRefresher nodeRefresher)87 public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) { 88 mAccessibilityNodeRefresher = nodeRefresher; 89 } 90 91 /** 92 * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache. 93 * The key of SparseArray is display ID. 94 * 95 * @param windowsOnAllDisplays The accessibility windows of all displays. 96 */ setWindowsOnAllDisplays( SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays)97 public void setWindowsOnAllDisplays( 98 SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays) { 99 synchronized (mLock) { 100 if (DEBUG) { 101 Log.i(LOG_TAG, "Set windows"); 102 } 103 clearWindowCacheLocked(); 104 if (windowsOnAllDisplays == null) { 105 return; 106 } 107 108 final int displayCounts = windowsOnAllDisplays.size(); 109 for (int i = 0; i < displayCounts; i++) { 110 final List<AccessibilityWindowInfo> windowsOfDisplay = 111 windowsOnAllDisplays.valueAt(i); 112 113 if (windowsOfDisplay == null) { 114 continue; 115 } 116 117 final int displayId = windowsOnAllDisplays.keyAt(i); 118 final int windowCount = windowsOfDisplay.size(); 119 for (int j = 0; j < windowCount; j++) { 120 addWindowByDisplayLocked(displayId, windowsOfDisplay.get(j)); 121 } 122 } 123 mIsAllWindowsCached = true; 124 } 125 } 126 127 /** 128 * Sets an {@link AccessibilityWindowInfo} into the cache. 129 * 130 * @param window The accessibility window. 131 */ addWindow(AccessibilityWindowInfo window)132 public void addWindow(AccessibilityWindowInfo window) { 133 synchronized (mLock) { 134 if (DEBUG) { 135 Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ " 136 + window.getDisplayId() + " ]"); 137 } 138 addWindowByDisplayLocked(window.getDisplayId(), window); 139 } 140 } 141 addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window)142 private void addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window) { 143 SparseArray<AccessibilityWindowInfo> windows = mWindowCacheByDisplay.get(displayId); 144 if (windows == null) { 145 windows = new SparseArray<>(); 146 mWindowCacheByDisplay.put(displayId, windows); 147 } 148 final int windowId = window.getId(); 149 windows.put(windowId, new AccessibilityWindowInfo(window)); 150 } 151 /** 152 * Notifies the cache that the something in the UI changed. As a result 153 * the cache will either refresh some nodes or evict some nodes. 154 * 155 * Note: any event that ends up affecting the cache should also be present in 156 * {@link #CACHE_CRITICAL_EVENTS_MASK} 157 * 158 * @param event An event. 159 */ onAccessibilityEvent(AccessibilityEvent event)160 public void onAccessibilityEvent(AccessibilityEvent event) { 161 AccessibilityNodeInfo nodeToRefresh = null; 162 synchronized (mLock) { 163 if (DEBUG) { 164 Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")"); 165 } 166 final int eventType = event.getEventType(); 167 switch (eventType) { 168 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { 169 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 170 removeCachedNodeLocked(mAccessibilityFocusedWindow, mAccessibilityFocus); 171 } 172 mAccessibilityFocus = event.getSourceNodeId(); 173 mAccessibilityFocusedWindow = event.getWindowId(); 174 nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow, 175 mAccessibilityFocus); 176 } break; 177 178 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { 179 if (mAccessibilityFocus == event.getSourceNodeId() 180 && mAccessibilityFocusedWindow == event.getWindowId()) { 181 nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow, 182 mAccessibilityFocus); 183 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 184 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 185 } 186 } break; 187 188 case AccessibilityEvent.TYPE_VIEW_FOCUSED: { 189 if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 190 removeCachedNodeLocked(event.getWindowId(), mInputFocus); 191 } 192 mInputFocus = event.getSourceNodeId(); 193 nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), mInputFocus); 194 } break; 195 196 case AccessibilityEvent.TYPE_VIEW_SELECTED: 197 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: 198 case AccessibilityEvent.TYPE_VIEW_CLICKED: 199 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { 200 nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), 201 event.getSourceNodeId()); 202 } break; 203 204 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { 205 synchronized (mLock) { 206 final int windowId = event.getWindowId(); 207 final long sourceId = event.getSourceNodeId(); 208 if ((event.getContentChangeTypes() 209 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { 210 clearSubTreeLocked(windowId, sourceId); 211 } else { 212 nodeToRefresh = removeCachedNodeLocked(windowId, sourceId); 213 } 214 } 215 } break; 216 217 case AccessibilityEvent.TYPE_VIEW_SCROLLED: { 218 clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId()); 219 } break; 220 221 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 222 if (event.getWindowChanges() 223 == AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED) { 224 // Don't need to clear all cache. Unless the changes are related to 225 // content, we won't clear all cache here with clear(). 226 clearWindowCacheLocked(); 227 break; 228 } 229 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { 230 clear(); 231 } break; 232 } 233 } 234 235 if (nodeToRefresh != null) { 236 if (DEBUG) { 237 Log.i(LOG_TAG, "Refreshing and re-adding cached node."); 238 } 239 if (mAccessibilityNodeRefresher.refreshNode(nodeToRefresh, true)) { 240 add(nodeToRefresh); 241 } 242 } 243 if (CHECK_INTEGRITY) { 244 checkIntegrity(); 245 } 246 } 247 removeCachedNodeLocked(int windowId, long sourceId)248 private AccessibilityNodeInfo removeCachedNodeLocked(int windowId, long sourceId) { 249 if (DEBUG) { 250 Log.i(LOG_TAG, "Removing cached node."); 251 } 252 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 253 if (nodes == null) { 254 return null; 255 } 256 AccessibilityNodeInfo cachedInfo = nodes.get(sourceId); 257 // If the source is not in the cache - nothing to do. 258 if (cachedInfo == null) { 259 return null; 260 } 261 nodes.remove(sourceId); 262 return cachedInfo; 263 } 264 265 /** 266 * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting 267 * window and the accessibility id of the node. 268 * 269 * @param windowId The id of the window hosting the node. 270 * @param accessibilityNodeId The info accessibility node id. 271 * @return The cached {@link AccessibilityNodeInfo} or null if such not found. 272 */ getNode(int windowId, long accessibilityNodeId)273 public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) { 274 synchronized(mLock) { 275 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 276 if (nodes == null) { 277 return null; 278 } 279 AccessibilityNodeInfo info = nodes.get(accessibilityNodeId); 280 if (info != null) { 281 // Return a copy since the client calls to AccessibilityNodeInfo#recycle() 282 // will wipe the data of the cached info. 283 info = new AccessibilityNodeInfo(info); 284 } 285 if (VERBOSE) { 286 Log.i(LOG_TAG, "get(0x" + Long.toHexString(accessibilityNodeId) + ") = " + info); 287 } 288 return info; 289 } 290 } 291 292 /** 293 * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache. 294 * 295 * @return All cached {@link AccessibilityWindowInfo}s of all displays 296 * or null if such not found. The key of SparseArray is display ID. 297 */ getWindowsOnAllDisplays()298 public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() { 299 synchronized (mLock) { 300 if (!mIsAllWindowsCached) { 301 return null; 302 } 303 final SparseArray<List<AccessibilityWindowInfo>> returnWindows = new SparseArray<>(); 304 final int displayCounts = mWindowCacheByDisplay.size(); 305 306 if (displayCounts > 0) { 307 for (int i = 0; i < displayCounts; i++) { 308 final int displayId = mWindowCacheByDisplay.keyAt(i); 309 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 310 mWindowCacheByDisplay.valueAt(i); 311 312 if (windowsOfDisplay == null) { 313 continue; 314 } 315 316 final int windowCount = windowsOfDisplay.size(); 317 if (windowCount > 0) { 318 // Careful to return the windows in a decreasing layer order. 319 SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray; 320 sortedWindows.clear(); 321 322 for (int j = 0; j < windowCount; j++) { 323 AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j); 324 sortedWindows.put(window.getLayer(), window); 325 } 326 327 // It's possible in transient conditions for two windows to share the same 328 // layer, which results in sortedWindows being smaller than 329 // mWindowCacheByDisplay 330 final int sortedWindowCount = sortedWindows.size(); 331 List<AccessibilityWindowInfo> windows = 332 new ArrayList<>(sortedWindowCount); 333 for (int j = sortedWindowCount - 1; j >= 0; j--) { 334 AccessibilityWindowInfo window = sortedWindows.valueAt(j); 335 windows.add(new AccessibilityWindowInfo(window)); 336 sortedWindows.removeAt(j); 337 } 338 returnWindows.put(displayId, windows); 339 } 340 } 341 return returnWindows; 342 } 343 return null; 344 } 345 } 346 347 /** 348 * Gets an {@link AccessibilityWindowInfo} by windowId. 349 * 350 * @param windowId The id of the window. 351 * 352 * @return The {@link AccessibilityWindowInfo} or null if such not found. 353 */ getWindow(int windowId)354 public AccessibilityWindowInfo getWindow(int windowId) { 355 synchronized (mLock) { 356 final int displayCounts = mWindowCacheByDisplay.size(); 357 for (int i = 0; i < displayCounts; i++) { 358 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 359 mWindowCacheByDisplay.valueAt(i); 360 if (windowsOfDisplay == null) { 361 continue; 362 } 363 364 AccessibilityWindowInfo window = windowsOfDisplay.get(windowId); 365 if (window != null) { 366 return new AccessibilityWindowInfo(window); 367 } 368 } 369 return null; 370 } 371 } 372 373 /** 374 * Caches an {@link AccessibilityNodeInfo}. 375 * 376 * @param info The node to cache. 377 */ add(AccessibilityNodeInfo info)378 public void add(AccessibilityNodeInfo info) { 379 synchronized(mLock) { 380 if (VERBOSE) { 381 Log.i(LOG_TAG, "add(" + info + ")"); 382 } 383 384 final int windowId = info.getWindowId(); 385 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 386 if (nodes == null) { 387 nodes = new LongSparseArray<>(); 388 mNodeCache.put(windowId, nodes); 389 } 390 391 final long sourceId = info.getSourceNodeId(); 392 AccessibilityNodeInfo oldInfo = nodes.get(sourceId); 393 if (oldInfo != null) { 394 // If the added node is in the cache we have to be careful if 395 // the new one represents a source state where some of the 396 // children have been removed to remove the descendants that 397 // are no longer present. 398 final LongArray newChildrenIds = info.getChildNodeIds(); 399 400 final int oldChildCount = oldInfo.getChildCount(); 401 for (int i = 0; i < oldChildCount; i++) { 402 final long oldChildId = oldInfo.getChildId(i); 403 // If the child is no longer present, remove the sub-tree. 404 if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) { 405 clearSubTreeLocked(windowId, oldChildId); 406 } 407 if (nodes.get(sourceId) == null) { 408 // We've removed (and thus recycled) this node because it was its own 409 // ancestor (the app gave us bad data), we can't continue using it. 410 // Clear the cache for this window and give up on adding the node. 411 clearNodesForWindowLocked(windowId); 412 return; 413 } 414 } 415 416 // Also be careful if the parent has changed since the new 417 // parent may be a predecessor of the old parent which will 418 // add cycles to the cache. 419 final long oldParentId = oldInfo.getParentNodeId(); 420 if (info.getParentNodeId() != oldParentId) { 421 clearSubTreeLocked(windowId, oldParentId); 422 } 423 } 424 425 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() 426 // will wipe the data of the cached info. 427 AccessibilityNodeInfo clone = new AccessibilityNodeInfo(info); 428 nodes.put(sourceId, clone); 429 if (clone.isAccessibilityFocused()) { 430 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID 431 && mAccessibilityFocus != sourceId) { 432 removeCachedNodeLocked(windowId, mAccessibilityFocus); 433 } 434 mAccessibilityFocus = sourceId; 435 mAccessibilityFocusedWindow = windowId; 436 } else if (mAccessibilityFocus == sourceId) { 437 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 438 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 439 } 440 if (clone.isFocused()) { 441 mInputFocus = sourceId; 442 } 443 } 444 } 445 446 /** 447 * Clears the cache. 448 */ clear()449 public void clear() { 450 synchronized(mLock) { 451 if (DEBUG) { 452 Log.i(LOG_TAG, "clear()"); 453 } 454 clearWindowCacheLocked(); 455 final int nodesForWindowCount = mNodeCache.size(); 456 for (int i = nodesForWindowCount - 1; i >= 0; i--) { 457 final int windowId = mNodeCache.keyAt(i); 458 clearNodesForWindowLocked(windowId); 459 } 460 461 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 462 mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 463 464 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 465 } 466 } 467 clearWindowCacheLocked()468 private void clearWindowCacheLocked() { 469 if (DEBUG) { 470 Log.i(LOG_TAG, "clearWindowCacheLocked"); 471 } 472 final int displayCounts = mWindowCacheByDisplay.size(); 473 474 if (displayCounts > 0) { 475 for (int i = displayCounts - 1; i >= 0; i--) { 476 final int displayId = mWindowCacheByDisplay.keyAt(i); 477 final SparseArray<AccessibilityWindowInfo> windows = 478 mWindowCacheByDisplay.get(displayId); 479 if (windows != null) { 480 windows.clear(); 481 } 482 mWindowCacheByDisplay.remove(displayId); 483 } 484 } 485 mIsAllWindowsCached = false; 486 } 487 488 /** 489 * Clears nodes for the window with the given id 490 */ clearNodesForWindowLocked(int windowId)491 private void clearNodesForWindowLocked(int windowId) { 492 if (DEBUG) { 493 Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")"); 494 } 495 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 496 if (nodes == null) { 497 return; 498 } 499 mNodeCache.remove(windowId); 500 } 501 502 /** 503 * Clears a subtree rooted at the node with the given id that is 504 * hosted in a given window. 505 * 506 * @param windowId The id of the hosting window. 507 * @param rootNodeId The root id. 508 */ clearSubTreeLocked(int windowId, long rootNodeId)509 private void clearSubTreeLocked(int windowId, long rootNodeId) { 510 if (DEBUG) { 511 Log.i(LOG_TAG, "Clearing cached subtree."); 512 } 513 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 514 if (nodes != null) { 515 clearSubTreeRecursiveLocked(nodes, rootNodeId); 516 } 517 } 518 519 /** 520 * Clears a subtree given a pointer to the root id and the nodes 521 * in the hosting window. 522 * 523 * @param nodes The nodes in the hosting window. 524 * @param rootNodeId The id of the root to evict. 525 * 526 * @return {@code true} if the cache was cleared 527 */ clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, long rootNodeId)528 private boolean clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, 529 long rootNodeId) { 530 AccessibilityNodeInfo current = nodes.get(rootNodeId); 531 if (current == null) { 532 // The node isn't in the cache, but its descendents might be. 533 clear(); 534 return true; 535 } 536 nodes.remove(rootNodeId); 537 final int childCount = current.getChildCount(); 538 for (int i = 0; i < childCount; i++) { 539 final long childNodeId = current.getChildId(i); 540 if (clearSubTreeRecursiveLocked(nodes, childNodeId)) { 541 return true; 542 } 543 } 544 return false; 545 } 546 547 /** 548 * Check the integrity of the cache which is nodes from different windows 549 * are not mixed, there is a single active window, there is a single focused 550 * window, for every window there are no duplicates nodes, all nodes for a 551 * window are connected, for every window there is a single input focused 552 * node, and for every window there is a single accessibility focused node. 553 */ checkIntegrity()554 public void checkIntegrity() { 555 synchronized (mLock) { 556 // Get the root. 557 if (mWindowCacheByDisplay.size() <= 0 && mNodeCache.size() == 0) { 558 return; 559 } 560 561 AccessibilityWindowInfo focusedWindow = null; 562 AccessibilityWindowInfo activeWindow = null; 563 564 final int displayCounts = mWindowCacheByDisplay.size(); 565 for (int i = 0; i < displayCounts; i++) { 566 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 567 mWindowCacheByDisplay.valueAt(i); 568 569 if (windowsOfDisplay == null) { 570 continue; 571 } 572 573 final int windowCount = windowsOfDisplay.size(); 574 for (int j = 0; j < windowCount; j++) { 575 final AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j); 576 577 // Check for one active window. 578 if (window.isActive()) { 579 if (activeWindow != null) { 580 Log.e(LOG_TAG, "Duplicate active window:" + window); 581 } else { 582 activeWindow = window; 583 } 584 } 585 // Check for one focused window. 586 if (window.isFocused()) { 587 if (focusedWindow != null) { 588 Log.e(LOG_TAG, "Duplicate focused window:" + window); 589 } else { 590 focusedWindow = window; 591 } 592 } 593 } 594 } 595 596 // Traverse the tree and do some checks. 597 AccessibilityNodeInfo accessFocus = null; 598 AccessibilityNodeInfo inputFocus = null; 599 600 final int nodesForWindowCount = mNodeCache.size(); 601 for (int i = 0; i < nodesForWindowCount; i++) { 602 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i); 603 if (nodes.size() <= 0) { 604 continue; 605 } 606 607 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>(); 608 final int windowId = mNodeCache.keyAt(i); 609 610 final int nodeCount = nodes.size(); 611 for (int j = 0; j < nodeCount; j++) { 612 AccessibilityNodeInfo node = nodes.valueAt(j); 613 614 // Check for duplicates 615 if (!seen.add(node)) { 616 Log.e(LOG_TAG, "Duplicate node: " + node 617 + " in window:" + windowId); 618 // Stop now as we potentially found a loop. 619 continue; 620 } 621 622 // Check for one accessibility focus. 623 if (node.isAccessibilityFocused()) { 624 if (accessFocus != null) { 625 Log.e(LOG_TAG, "Duplicate accessibility focus:" + node 626 + " in window:" + windowId); 627 } else { 628 accessFocus = node; 629 } 630 } 631 632 // Check for one input focus. 633 if (node.isFocused()) { 634 if (inputFocus != null) { 635 Log.e(LOG_TAG, "Duplicate input focus: " + node 636 + " in window:" + windowId); 637 } else { 638 inputFocus = node; 639 } 640 } 641 642 // The node should be a child of its parent if we have the parent. 643 AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId()); 644 if (nodeParent != null) { 645 boolean childOfItsParent = false; 646 final int childCount = nodeParent.getChildCount(); 647 for (int k = 0; k < childCount; k++) { 648 AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k)); 649 if (child == node) { 650 childOfItsParent = true; 651 break; 652 } 653 } 654 if (!childOfItsParent) { 655 Log.e(LOG_TAG, "Invalid parent-child relation between parent: " 656 + nodeParent + " and child: " + node); 657 } 658 } 659 660 // The node should be the parent of its child if we have the child. 661 final int childCount = node.getChildCount(); 662 for (int k = 0; k < childCount; k++) { 663 AccessibilityNodeInfo child = nodes.get(node.getChildId(k)); 664 if (child != null) { 665 AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId()); 666 if (parent != node) { 667 Log.e(LOG_TAG, "Invalid child-parent relation between child: " 668 + node + " and parent: " + nodeParent); 669 } 670 } 671 } 672 } 673 } 674 } 675 } 676 677 // Layer of indirection included to break dependency chain for testing 678 public static class AccessibilityNodeRefresher { 679 /** Refresh the given AccessibilityNodeInfo object. */ refreshNode(AccessibilityNodeInfo info, boolean bypassCache)680 public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) { 681 return info.refresh(null, bypassCache); 682 } 683 684 /** Refresh the given AccessibilityWindowInfo object. */ refreshWindow(AccessibilityWindowInfo info)685 public boolean refreshWindow(AccessibilityWindowInfo info) { 686 return info.refresh(); 687 } 688 } 689 } 690