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