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 20 import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_ACCESSIBILITY; 21 22 import android.os.Build; 23 import android.os.SystemClock; 24 import android.util.ArraySet; 25 import android.util.Log; 26 import android.util.LongArray; 27 import android.util.LongSparseArray; 28 import android.util.SparseArray; 29 30 import java.util.ArrayList; 31 import java.util.List; 32 33 /** 34 * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos. 35 * It is updated when windows change or nodes change. 36 * @hide 37 */ 38 public class AccessibilityCache { 39 40 private static final String LOG_TAG = "AccessibilityCache"; 41 42 private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE; 43 44 private static final boolean VERBOSE = 45 Log.isLoggable(LOG_TAG, Log.VERBOSE) && Build.IS_DEBUGGABLE; 46 47 private static final boolean CHECK_INTEGRITY = Build.IS_ENG; 48 49 private boolean mEnabled = true; 50 51 /** 52 * {@link AccessibilityEvent} types that are critical for the cache to stay up to date 53 * 54 * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to 55 * make sure that the events are delivered to cache regardless of 56 * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes} 57 */ 58 public static final int CACHE_CRITICAL_EVENTS_MASK = 59 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED 60 | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 61 | AccessibilityEvent.TYPE_VIEW_FOCUSED 62 | AccessibilityEvent.TYPE_VIEW_SELECTED 63 | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED 64 | AccessibilityEvent.TYPE_VIEW_CLICKED 65 | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED 66 | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED 67 | AccessibilityEvent.TYPE_VIEW_SCROLLED 68 | AccessibilityEvent.TYPE_WINDOWS_CHANGED 69 | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; 70 71 private final Object mLock = new Object(); 72 73 private final AccessibilityNodeRefresher mAccessibilityNodeRefresher; 74 75 private OnNodeAddedListener mOnNodeAddedListener; 76 77 private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 78 private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 79 /** 80 * The event time of the {@link AccessibilityEvent} which presents the populated windows cache 81 * before it is stale. 82 */ 83 private long mValidWindowCacheTimeStamp = 0; 84 85 private int mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 86 private int mInputFocusWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 87 88 private boolean mIsAllWindowsCached; 89 90 // The SparseArray of all {@link AccessibilityWindowInfo}s on all displays. 91 // The key of outer SparseArray is display ID and the key of inner SparseArray is window ID. 92 private final SparseArray<SparseArray<AccessibilityWindowInfo>> mWindowCacheByDisplay = 93 new SparseArray<>(); 94 95 private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache = 96 new SparseArray<>(); 97 98 private final SparseArray<AccessibilityWindowInfo> mTempWindowArray = 99 new SparseArray<>(); 100 AccessibilityCache(AccessibilityNodeRefresher nodeRefresher)101 public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) { 102 mAccessibilityNodeRefresher = nodeRefresher; 103 } 104 105 /** Returns if the cache is enabled. */ isEnabled()106 public boolean isEnabled() { 107 synchronized (mLock) { 108 return mEnabled; 109 } 110 } 111 112 /** Sets enabled status. */ setEnabled(boolean enabled)113 public void setEnabled(boolean enabled) { 114 synchronized (mLock) { 115 mEnabled = enabled; 116 clear(); 117 } 118 } 119 120 /** 121 * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache. 122 * The key of SparseArray is display ID. 123 * 124 * @param windowsOnAllDisplays The accessibility windows of all displays. 125 * @param populationTimeStamp The timestamp from {@link SystemClock#uptimeMillis()} when the 126 * client requests the data. 127 */ setWindowsOnAllDisplays( SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays, long populationTimeStamp)128 public void setWindowsOnAllDisplays( 129 SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays, 130 long populationTimeStamp) { 131 synchronized (mLock) { 132 if (!mEnabled) { 133 if (DEBUG) { 134 Log.i(LOG_TAG, "Cache is disabled"); 135 } 136 return; 137 } 138 if (DEBUG) { 139 Log.i(LOG_TAG, "Set windows"); 140 } 141 if (mValidWindowCacheTimeStamp > populationTimeStamp) { 142 // Discard the windows because it might be stale. 143 return; 144 } 145 clearWindowCacheLocked(); 146 if (windowsOnAllDisplays == null) { 147 return; 148 } 149 150 final int displayCounts = windowsOnAllDisplays.size(); 151 for (int i = 0; i < displayCounts; i++) { 152 final List<AccessibilityWindowInfo> windowsOfDisplay = 153 windowsOnAllDisplays.valueAt(i); 154 155 if (windowsOfDisplay == null) { 156 continue; 157 } 158 159 final int displayId = windowsOnAllDisplays.keyAt(i); 160 final int windowCount = windowsOfDisplay.size(); 161 for (int j = 0; j < windowCount; j++) { 162 addWindowByDisplayLocked(displayId, windowsOfDisplay.get(j)); 163 } 164 } 165 mIsAllWindowsCached = true; 166 } 167 } 168 169 /** 170 * Sets an {@link AccessibilityWindowInfo} into the cache. 171 * 172 * @param window The accessibility window. 173 */ addWindow(AccessibilityWindowInfo window)174 public void addWindow(AccessibilityWindowInfo window) { 175 synchronized (mLock) { 176 if (!mEnabled) { 177 if (DEBUG) { 178 Log.i(LOG_TAG, "Cache is disabled"); 179 } 180 return; 181 } 182 if (DEBUG) { 183 Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ " 184 + window.getDisplayId() + " ]"); 185 } 186 addWindowByDisplayLocked(window.getDisplayId(), window); 187 } 188 } 189 addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window)190 private void addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window) { 191 SparseArray<AccessibilityWindowInfo> windows = mWindowCacheByDisplay.get(displayId); 192 if (windows == null) { 193 windows = new SparseArray<>(); 194 mWindowCacheByDisplay.put(displayId, windows); 195 } 196 final int windowId = window.getId(); 197 windows.put(windowId, new AccessibilityWindowInfo(window)); 198 } 199 /** 200 * Notifies the cache that the something in the UI changed. As a result 201 * the cache will either refresh some nodes or evict some nodes. 202 * 203 * Note: any event that ends up affecting the cache should also be present in 204 * {@link #CACHE_CRITICAL_EVENTS_MASK} 205 * 206 * @param event An event. 207 */ onAccessibilityEvent(AccessibilityEvent event)208 public void onAccessibilityEvent(AccessibilityEvent event) { 209 AccessibilityNodeInfo nodeToRefresh = null; 210 synchronized (mLock) { 211 if (!mEnabled) { 212 if (DEBUG) { 213 Log.i(LOG_TAG, "Cache is disabled"); 214 } 215 return; 216 } 217 if (DEBUG) { 218 Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")"); 219 } 220 final int eventType = event.getEventType(); 221 switch (eventType) { 222 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { 223 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 224 removeCachedNodeLocked(mAccessibilityFocusedWindow, mAccessibilityFocus); 225 } 226 mAccessibilityFocus = event.getSourceNodeId(); 227 mAccessibilityFocusedWindow = event.getWindowId(); 228 nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow, 229 mAccessibilityFocus); 230 } break; 231 232 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { 233 if (mAccessibilityFocus == event.getSourceNodeId() 234 && mAccessibilityFocusedWindow == event.getWindowId()) { 235 nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow, 236 mAccessibilityFocus); 237 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 238 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 239 } 240 } break; 241 242 case AccessibilityEvent.TYPE_VIEW_FOCUSED: { 243 if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 244 removeCachedNodeLocked(event.getWindowId(), mInputFocus); 245 } 246 mInputFocus = event.getSourceNodeId(); 247 mInputFocusWindow = event.getWindowId(); 248 nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), mInputFocus); 249 } break; 250 251 case AccessibilityEvent.TYPE_VIEW_SELECTED: 252 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: 253 case AccessibilityEvent.TYPE_VIEW_CLICKED: 254 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { 255 nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), 256 event.getSourceNodeId()); 257 } break; 258 259 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { 260 synchronized (mLock) { 261 final int windowId = event.getWindowId(); 262 final long sourceId = event.getSourceNodeId(); 263 if ((event.getContentChangeTypes() 264 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { 265 clearSubTreeLocked(windowId, sourceId); 266 } else { 267 nodeToRefresh = removeCachedNodeLocked(windowId, sourceId); 268 } 269 } 270 } break; 271 272 case AccessibilityEvent.TYPE_VIEW_SCROLLED: { 273 clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId()); 274 } break; 275 276 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 277 mValidWindowCacheTimeStamp = event.getEventTime(); 278 if (event.getWindowChanges() 279 == AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED) { 280 // Don't need to clear all cache. Unless the changes are related to 281 // content, we won't clear all cache here with clear(). 282 clearWindowCacheLocked(); 283 break; 284 } 285 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { 286 mValidWindowCacheTimeStamp = event.getEventTime(); 287 clear(); 288 } break; 289 } 290 } 291 292 if (nodeToRefresh != null) { 293 if (DEBUG) { 294 Log.i(LOG_TAG, "Refreshing and re-adding cached node."); 295 } 296 if (mAccessibilityNodeRefresher.refreshNode(nodeToRefresh, true)) { 297 add(nodeToRefresh); 298 } 299 } 300 if (CHECK_INTEGRITY) { 301 checkIntegrity(); 302 } 303 } 304 removeCachedNodeLocked(int windowId, long sourceId)305 private AccessibilityNodeInfo removeCachedNodeLocked(int windowId, long sourceId) { 306 if (DEBUG) { 307 Log.i(LOG_TAG, "Removing cached node."); 308 } 309 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 310 if (nodes == null) { 311 return null; 312 } 313 AccessibilityNodeInfo cachedInfo = nodes.get(sourceId); 314 // If the source is not in the cache - nothing to do. 315 if (cachedInfo == null) { 316 return null; 317 } 318 nodes.remove(sourceId); 319 return cachedInfo; 320 } 321 322 /** 323 * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting 324 * window and the accessibility id of the node. 325 * 326 * @param windowId The id of the window hosting the node. 327 * @param accessibilityNodeId The info accessibility node id. 328 * @return The cached {@link AccessibilityNodeInfo} or null if such not found. 329 */ getNode(int windowId, long accessibilityNodeId)330 public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) { 331 synchronized(mLock) { 332 if (!mEnabled) { 333 if (DEBUG) { 334 Log.i(LOG_TAG, "Cache is disabled"); 335 } 336 return null; 337 } 338 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 339 if (nodes == null) { 340 return null; 341 } 342 AccessibilityNodeInfo info = nodes.get(accessibilityNodeId); 343 if (info != null) { 344 // Return a copy since the client calls to AccessibilityNodeInfo#recycle() 345 // will wipe the data of the cached info. 346 info = new AccessibilityNodeInfo(info); 347 } 348 if (VERBOSE) { 349 Log.i(LOG_TAG, "get(0x" + Long.toHexString(accessibilityNodeId) + ") = " + info); 350 } 351 return info; 352 } 353 } 354 355 /** Returns {@code true} if {@code info} is in the cache. */ isNodeInCache(AccessibilityNodeInfo info)356 public boolean isNodeInCache(AccessibilityNodeInfo info) { 357 if (info == null) { 358 return false; 359 } 360 int windowId = info.getWindowId(); 361 long accessibilityNodeId = info.getSourceNodeId(); 362 synchronized (mLock) { 363 if (!mEnabled) { 364 if (DEBUG) { 365 Log.i(LOG_TAG, "Cache is disabled"); 366 } 367 return false; 368 } 369 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 370 if (nodes == null) { 371 return false; 372 } 373 return nodes.get(accessibilityNodeId) != null; 374 } 375 } 376 377 /** 378 * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache. 379 * 380 * @return All cached {@link AccessibilityWindowInfo}s of all displays 381 * or null if such not found. The key of SparseArray is display ID. 382 */ getWindowsOnAllDisplays()383 public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() { 384 synchronized (mLock) { 385 if (!mEnabled) { 386 if (DEBUG) { 387 Log.i(LOG_TAG, "Cache is disabled"); 388 } 389 return null; 390 } 391 if (!mIsAllWindowsCached) { 392 return null; 393 } 394 final SparseArray<List<AccessibilityWindowInfo>> returnWindows = new SparseArray<>(); 395 final int displayCounts = mWindowCacheByDisplay.size(); 396 397 if (displayCounts > 0) { 398 for (int i = 0; i < displayCounts; i++) { 399 final int displayId = mWindowCacheByDisplay.keyAt(i); 400 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 401 mWindowCacheByDisplay.valueAt(i); 402 403 if (windowsOfDisplay == null) { 404 continue; 405 } 406 407 final int windowCount = windowsOfDisplay.size(); 408 if (windowCount > 0) { 409 // Careful to return the windows in a decreasing layer order. 410 SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray; 411 sortedWindows.clear(); 412 413 for (int j = 0; j < windowCount; j++) { 414 AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j); 415 sortedWindows.put(window.getLayer(), window); 416 } 417 418 // It's possible in transient conditions for two windows to share the same 419 // layer, which results in sortedWindows being smaller than 420 // mWindowCacheByDisplay 421 final int sortedWindowCount = sortedWindows.size(); 422 List<AccessibilityWindowInfo> windows = 423 new ArrayList<>(sortedWindowCount); 424 for (int j = sortedWindowCount - 1; j >= 0; j--) { 425 AccessibilityWindowInfo window = sortedWindows.valueAt(j); 426 windows.add(new AccessibilityWindowInfo(window)); 427 sortedWindows.removeAt(j); 428 } 429 returnWindows.put(displayId, windows); 430 } 431 } 432 return returnWindows; 433 } 434 return null; 435 } 436 } 437 438 /** 439 * Gets an {@link AccessibilityWindowInfo} by windowId. 440 * 441 * @param windowId The id of the window. 442 * 443 * @return The {@link AccessibilityWindowInfo} or null if such not found. 444 */ getWindow(int windowId)445 public AccessibilityWindowInfo getWindow(int windowId) { 446 synchronized (mLock) { 447 if (!mEnabled) { 448 if (DEBUG) { 449 Log.i(LOG_TAG, "Cache is disabled"); 450 } 451 return null; 452 } 453 final int displayCounts = mWindowCacheByDisplay.size(); 454 for (int i = 0; i < displayCounts; i++) { 455 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 456 mWindowCacheByDisplay.valueAt(i); 457 if (windowsOfDisplay == null) { 458 continue; 459 } 460 461 AccessibilityWindowInfo window = windowsOfDisplay.get(windowId); 462 if (window != null) { 463 return new AccessibilityWindowInfo(window); 464 } 465 } 466 return null; 467 } 468 } 469 470 /** 471 * Caches an {@link AccessibilityNodeInfo}. 472 * 473 * @param info The node to cache. 474 */ add(AccessibilityNodeInfo info)475 public void add(AccessibilityNodeInfo info) { 476 synchronized(mLock) { 477 if (!mEnabled) { 478 if (DEBUG) { 479 Log.i(LOG_TAG, "Cache is disabled"); 480 } 481 return; 482 } 483 if (VERBOSE) { 484 Log.i(LOG_TAG, "add(" + info + ")"); 485 } 486 487 final int windowId = info.getWindowId(); 488 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 489 if (nodes == null) { 490 nodes = new LongSparseArray<>(); 491 mNodeCache.put(windowId, nodes); 492 } 493 494 final long sourceId = info.getSourceNodeId(); 495 AccessibilityNodeInfo oldInfo = nodes.get(sourceId); 496 if (oldInfo != null) { 497 // If the added node is in the cache we have to be careful if 498 // the new one represents a source state where some of the 499 // children have been removed to remove the descendants that 500 // are no longer present. 501 final LongArray newChildrenIds = info.getChildNodeIds(); 502 503 final int oldChildCount = oldInfo.getChildCount(); 504 for (int i = 0; i < oldChildCount; i++) { 505 final long oldChildId = oldInfo.getChildId(i); 506 // If the child is no longer present, remove the sub-tree. 507 if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) { 508 clearSubTreeLocked(windowId, oldChildId); 509 } 510 if (nodes.get(sourceId) == null) { 511 // We've removed (and thus recycled) this node because it was its own 512 // ancestor (the app gave us bad data), we can't continue using it. 513 // Clear the cache for this window and give up on adding the node. 514 clearNodesForWindowLocked(windowId); 515 return; 516 } 517 } 518 519 // Also be careful if the parent has changed since the new 520 // parent may be a predecessor of the old parent which will 521 // add cycles to the cache. 522 final long oldParentId = oldInfo.getParentNodeId(); 523 if (info.getParentNodeId() != oldParentId) { 524 clearSubTreeLocked(windowId, oldParentId); 525 } 526 } 527 528 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() 529 // will wipe the data of the cached info. 530 AccessibilityNodeInfo clone = new AccessibilityNodeInfo(info); 531 nodes.put(sourceId, clone); 532 if (clone.isAccessibilityFocused()) { 533 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID 534 && mAccessibilityFocus != sourceId) { 535 removeCachedNodeLocked(windowId, mAccessibilityFocus); 536 } 537 mAccessibilityFocus = sourceId; 538 mAccessibilityFocusedWindow = windowId; 539 } else if (mAccessibilityFocus == sourceId) { 540 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 541 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 542 } 543 if (clone.isFocused()) { 544 mInputFocus = sourceId; 545 mInputFocusWindow = windowId; 546 } 547 548 if (mOnNodeAddedListener != null) { 549 mOnNodeAddedListener.onNodeAdded(clone); 550 } 551 } 552 } 553 554 /** 555 * Clears the cache. 556 */ clear()557 public void clear() { 558 synchronized(mLock) { 559 if (DEBUG) { 560 Log.i(LOG_TAG, "clear()"); 561 } 562 clearWindowCacheLocked(); 563 final int nodesForWindowCount = mNodeCache.size(); 564 for (int i = nodesForWindowCount - 1; i >= 0; i--) { 565 final int windowId = mNodeCache.keyAt(i); 566 clearNodesForWindowLocked(windowId); 567 } 568 569 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 570 mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 571 572 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 573 mInputFocusWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; 574 } 575 } 576 clearWindowCacheLocked()577 private void clearWindowCacheLocked() { 578 if (DEBUG) { 579 Log.i(LOG_TAG, "clearWindowCacheLocked"); 580 } 581 final int displayCounts = mWindowCacheByDisplay.size(); 582 583 if (displayCounts > 0) { 584 for (int i = displayCounts - 1; i >= 0; i--) { 585 final int displayId = mWindowCacheByDisplay.keyAt(i); 586 final SparseArray<AccessibilityWindowInfo> windows = 587 mWindowCacheByDisplay.get(displayId); 588 if (windows != null) { 589 windows.clear(); 590 } 591 mWindowCacheByDisplay.remove(displayId); 592 } 593 } 594 mIsAllWindowsCached = false; 595 } 596 597 /** 598 * Gets a cached {@link AccessibilityNodeInfo} with focus according to focus type. 599 * 600 * Note: {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} will return 601 * null. 602 * 603 * @param focusType The focus type. 604 * @param windowId A unique window id. 605 * @param initialNodeId A unique view id or virtual descendant id from where to start the 606 * search. 607 * @return The cached {@link AccessibilityNodeInfo} if it has a11y focus or null if such not 608 * found. 609 */ getFocus(int focusType, long initialNodeId, int windowId)610 public AccessibilityNodeInfo getFocus(int focusType, long initialNodeId, int windowId) { 611 synchronized (mLock) { 612 if (!mEnabled) { 613 if (DEBUG) { 614 Log.i(LOG_TAG, "Cache is disabled"); 615 } 616 return null; 617 } 618 int currentFocusWindowId; 619 long currentFocusId; 620 if (focusType == FOCUS_ACCESSIBILITY) { 621 currentFocusWindowId = mAccessibilityFocusedWindow; 622 currentFocusId = mAccessibilityFocus; 623 } else { 624 currentFocusWindowId = mInputFocusWindow; 625 currentFocusId = mInputFocus; 626 } 627 628 if (currentFocusWindowId == AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) { 629 return null; 630 } 631 632 if (windowId != AccessibilityWindowInfo.ANY_WINDOW_ID 633 && windowId != currentFocusWindowId) { 634 return null; 635 } 636 637 LongSparseArray<AccessibilityNodeInfo> nodes = 638 mNodeCache.get(currentFocusWindowId); 639 if (nodes == null) { 640 return null; 641 } 642 643 final AccessibilityNodeInfo currentFocusedNode = nodes.get(currentFocusId); 644 if (currentFocusedNode == null) { 645 return null; 646 } 647 648 if (initialNodeId == currentFocusId || (isCachedNodeOrDescendantLocked( 649 currentFocusedNode.getParentNodeId(), initialNodeId, nodes))) { 650 if (VERBOSE) { 651 Log.i(LOG_TAG, "getFocus(0x" + Long.toHexString(currentFocusId) + ") = " 652 + currentFocusedNode + " with type: " 653 + (focusType == FOCUS_ACCESSIBILITY 654 ? "FOCUS_ACCESSIBILITY" 655 : "FOCUS_INPUT")); 656 } 657 // Return a copy since the client calls to AccessibilityNodeInfo#recycle() 658 // will wipe the data of the cached info. 659 return new AccessibilityNodeInfo(currentFocusedNode); 660 } 661 662 if (VERBOSE) { 663 Log.i(LOG_TAG, "getFocus is null with type: " 664 + (focusType == FOCUS_ACCESSIBILITY 665 ? "FOCUS_ACCESSIBILITY" 666 : "FOCUS_INPUT")); 667 } 668 return null; 669 } 670 } 671 isCachedNodeOrDescendantLocked(long nodeId, long ancestorId, LongSparseArray<AccessibilityNodeInfo> nodes)672 private boolean isCachedNodeOrDescendantLocked(long nodeId, long ancestorId, 673 LongSparseArray<AccessibilityNodeInfo> nodes) { 674 if (ancestorId == nodeId) { 675 return true; 676 } 677 AccessibilityNodeInfo node = nodes.get(nodeId); 678 if (node == null) { 679 return false; 680 } 681 return isCachedNodeOrDescendantLocked(node.getParentNodeId(), ancestorId, nodes); 682 } 683 684 /** 685 * Clears nodes for the window with the given id 686 */ clearNodesForWindowLocked(int windowId)687 private void clearNodesForWindowLocked(int windowId) { 688 if (DEBUG) { 689 Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")"); 690 } 691 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 692 if (nodes == null) { 693 return; 694 } 695 mNodeCache.remove(windowId); 696 } 697 698 /** Clears a subtree rooted at {@code info}. */ clearSubTree(AccessibilityNodeInfo info)699 public boolean clearSubTree(AccessibilityNodeInfo info) { 700 if (info == null) { 701 return false; 702 } 703 synchronized (mLock) { 704 if (!mEnabled) { 705 if (DEBUG) { 706 Log.i(LOG_TAG, "Cache is disabled"); 707 } 708 return false; 709 } 710 clearSubTreeLocked(info.getWindowId(), info.getSourceNodeId()); 711 return true; 712 } 713 } 714 715 /** 716 * Clears a subtree rooted at the node with the given id that is 717 * hosted in a given window. 718 * 719 * @param windowId The id of the hosting window. 720 * @param rootNodeId The root id. 721 */ clearSubTreeLocked(int windowId, long rootNodeId)722 private void clearSubTreeLocked(int windowId, long rootNodeId) { 723 if (DEBUG) { 724 Log.i(LOG_TAG, "Clearing cached subtree."); 725 } 726 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 727 if (nodes != null) { 728 clearSubTreeRecursiveLocked(nodes, rootNodeId); 729 } 730 } 731 732 /** 733 * Clears a subtree given a pointer to the root id and the nodes 734 * in the hosting window. 735 * 736 * @param nodes The nodes in the hosting window. 737 * @param rootNodeId The id of the root to evict. 738 * 739 * @return {@code true} if the cache was cleared 740 */ clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, long rootNodeId)741 private boolean clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, 742 long rootNodeId) { 743 AccessibilityNodeInfo current = nodes.get(rootNodeId); 744 if (current == null) { 745 // The node isn't in the cache, but its descendents might be. 746 clear(); 747 return true; 748 } 749 nodes.remove(rootNodeId); 750 final int childCount = current.getChildCount(); 751 for (int i = 0; i < childCount; i++) { 752 final long childNodeId = current.getChildId(i); 753 if (clearSubTreeRecursiveLocked(nodes, childNodeId)) { 754 return true; 755 } 756 } 757 return false; 758 } 759 760 /** 761 * Check the integrity of the cache which is nodes from different windows 762 * are not mixed, there is a single active window, there is a single focused 763 * window, for every window there are no duplicates nodes, all nodes for a 764 * window are connected, for every window there is a single input focused 765 * node, and for every window there is a single accessibility focused node. 766 */ checkIntegrity()767 public void checkIntegrity() { 768 synchronized (mLock) { 769 // Get the root. 770 if (mWindowCacheByDisplay.size() <= 0 && mNodeCache.size() == 0) { 771 return; 772 } 773 774 AccessibilityWindowInfo focusedWindow = null; 775 AccessibilityWindowInfo activeWindow = null; 776 777 final int displayCounts = mWindowCacheByDisplay.size(); 778 for (int i = 0; i < displayCounts; i++) { 779 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay = 780 mWindowCacheByDisplay.valueAt(i); 781 782 if (windowsOfDisplay == null) { 783 continue; 784 } 785 786 final int windowCount = windowsOfDisplay.size(); 787 for (int j = 0; j < windowCount; j++) { 788 final AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j); 789 790 // Check for one active window. 791 if (window.isActive()) { 792 if (activeWindow != null) { 793 Log.e(LOG_TAG, "Duplicate active window:" + window); 794 } else { 795 activeWindow = window; 796 } 797 } 798 // Check for one focused window. 799 if (window.isFocused()) { 800 if (focusedWindow != null) { 801 Log.e(LOG_TAG, "Duplicate focused window:" + window); 802 } else { 803 focusedWindow = window; 804 } 805 } 806 } 807 } 808 809 // Traverse the tree and do some checks. 810 AccessibilityNodeInfo accessFocus = null; 811 AccessibilityNodeInfo inputFocus = null; 812 813 final int nodesForWindowCount = mNodeCache.size(); 814 for (int i = 0; i < nodesForWindowCount; i++) { 815 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i); 816 if (nodes.size() <= 0) { 817 continue; 818 } 819 820 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>(); 821 final int windowId = mNodeCache.keyAt(i); 822 823 final int nodeCount = nodes.size(); 824 for (int j = 0; j < nodeCount; j++) { 825 AccessibilityNodeInfo node = nodes.valueAt(j); 826 827 // Check for duplicates 828 if (!seen.add(node)) { 829 Log.e(LOG_TAG, "Duplicate node: " + node 830 + " in window:" + windowId); 831 // Stop now as we potentially found a loop. 832 continue; 833 } 834 835 // Check for one accessibility focus. 836 if (node.isAccessibilityFocused()) { 837 if (accessFocus != null) { 838 Log.e(LOG_TAG, "Duplicate accessibility focus:" + node 839 + " in window:" + windowId); 840 } else { 841 accessFocus = node; 842 } 843 } 844 845 // Check for one input focus. 846 if (node.isFocused()) { 847 if (inputFocus != null) { 848 Log.e(LOG_TAG, "Duplicate input focus: " + node 849 + " in window:" + windowId); 850 } else { 851 inputFocus = node; 852 } 853 } 854 855 // The node should be a child of its parent if we have the parent. 856 AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId()); 857 if (nodeParent != null) { 858 boolean childOfItsParent = false; 859 final int childCount = nodeParent.getChildCount(); 860 for (int k = 0; k < childCount; k++) { 861 AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k)); 862 if (child == node) { 863 childOfItsParent = true; 864 break; 865 } 866 } 867 if (!childOfItsParent) { 868 Log.e(LOG_TAG, "Invalid parent-child relation between parent: " 869 + nodeParent + " and child: " + node); 870 } 871 } 872 873 // The node should be the parent of its child if we have the child. 874 final int childCount = node.getChildCount(); 875 for (int k = 0; k < childCount; k++) { 876 AccessibilityNodeInfo child = nodes.get(node.getChildId(k)); 877 if (child != null) { 878 AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId()); 879 if (parent != node) { 880 Log.e(LOG_TAG, "Invalid child-parent relation between child: " 881 + node + " and parent: " + nodeParent); 882 } 883 } 884 } 885 } 886 } 887 } 888 } 889 890 /** 891 * Registers a listener to receive callbacks whenever nodes are added to cache. 892 * 893 * @param listener the listener to be registered. 894 */ registerOnNodeAddedListener(OnNodeAddedListener listener)895 public void registerOnNodeAddedListener(OnNodeAddedListener listener) { 896 synchronized (mLock) { 897 mOnNodeAddedListener = listener; 898 } 899 } 900 901 /** 902 * Clears the current reference to an OnNodeAddedListener, if one exists. 903 */ clearOnNodeAddedListener()904 public void clearOnNodeAddedListener() { 905 synchronized (mLock) { 906 mOnNodeAddedListener = null; 907 } 908 } 909 910 // Layer of indirection included to break dependency chain for testing 911 public static class AccessibilityNodeRefresher { 912 /** Refresh the given AccessibilityNodeInfo object. */ refreshNode(AccessibilityNodeInfo info, boolean bypassCache)913 public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) { 914 return info.refresh(null, bypassCache); 915 } 916 917 /** Refresh the given AccessibilityWindowInfo object. */ refreshWindow(AccessibilityWindowInfo info)918 public boolean refreshWindow(AccessibilityWindowInfo info) { 919 return info.refresh(); 920 } 921 } 922 923 /** 924 * Listener interface that receives callbacks when nodes are added to cache. 925 */ 926 public interface OnNodeAddedListener { 927 /** Called when a node is added to cache. */ onNodeAdded(AccessibilityNodeInfo node)928 void onNodeAdded(AccessibilityNodeInfo node); 929 } 930 } 931