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 17package android.view.accessibility; 18 19import android.os.Build; 20import android.util.ArraySet; 21import android.util.Log; 22import android.util.LongArray; 23import android.util.LongSparseArray; 24import android.util.SparseArray; 25 26import com.android.internal.annotations.VisibleForTesting; 27 28import java.util.ArrayList; 29import java.util.List; 30 31/** 32 * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos. 33 * It is updated when windows change or nodes change. 34 * @hide 35 */ 36@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 37public final class AccessibilityCache { 38 39 private static final String LOG_TAG = "AccessibilityCache"; 40 41 private static final boolean DEBUG = false; 42 43 private static final boolean CHECK_INTEGRITY = "eng".equals(Build.TYPE); 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 boolean mIsAllWindowsCached; 73 74 private final SparseArray<AccessibilityWindowInfo> mWindowCache = 75 new SparseArray<>(); 76 77 private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache = 78 new SparseArray<>(); 79 80 private final SparseArray<AccessibilityWindowInfo> mTempWindowArray = 81 new SparseArray<>(); 82 83 public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) { 84 mAccessibilityNodeRefresher = nodeRefresher; 85 } 86 87 public void setWindows(List<AccessibilityWindowInfo> windows) { 88 synchronized (mLock) { 89 if (DEBUG) { 90 Log.i(LOG_TAG, "Set windows"); 91 } 92 clearWindowCache(); 93 if (windows == null) { 94 return; 95 } 96 final int windowCount = windows.size(); 97 for (int i = 0; i < windowCount; i++) { 98 final AccessibilityWindowInfo window = windows.get(i); 99 addWindow(window); 100 } 101 mIsAllWindowsCached = true; 102 } 103 } 104 105 public void addWindow(AccessibilityWindowInfo window) { 106 synchronized (mLock) { 107 if (DEBUG) { 108 Log.i(LOG_TAG, "Caching window: " + window.getId()); 109 } 110 final int windowId = window.getId(); 111 AccessibilityWindowInfo oldWindow = mWindowCache.get(windowId); 112 if (oldWindow != null) { 113 oldWindow.recycle(); 114 } 115 mWindowCache.put(windowId, AccessibilityWindowInfo.obtain(window)); 116 } 117 } 118 119 /** 120 * Notifies the cache that the something in the UI changed. As a result 121 * the cache will either refresh some nodes or evict some nodes. 122 * 123 * Note: any event that ends up affecting the cache should also be present in 124 * {@link #CACHE_CRITICAL_EVENTS_MASK} 125 * 126 * @param event An event. 127 */ 128 public void onAccessibilityEvent(AccessibilityEvent event) { 129 synchronized (mLock) { 130 final int eventType = event.getEventType(); 131 switch (eventType) { 132 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { 133 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 134 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus); 135 } 136 mAccessibilityFocus = event.getSourceNodeId(); 137 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus); 138 } break; 139 140 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { 141 if (mAccessibilityFocus == event.getSourceNodeId()) { 142 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus); 143 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 144 } 145 } break; 146 147 case AccessibilityEvent.TYPE_VIEW_FOCUSED: { 148 if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { 149 refreshCachedNodeLocked(event.getWindowId(), mInputFocus); 150 } 151 mInputFocus = event.getSourceNodeId(); 152 refreshCachedNodeLocked(event.getWindowId(), mInputFocus); 153 } break; 154 155 case AccessibilityEvent.TYPE_VIEW_SELECTED: 156 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: 157 case AccessibilityEvent.TYPE_VIEW_CLICKED: 158 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { 159 refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId()); 160 } break; 161 162 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { 163 synchronized (mLock) { 164 final int windowId = event.getWindowId(); 165 final long sourceId = event.getSourceNodeId(); 166 if ((event.getContentChangeTypes() 167 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { 168 clearSubTreeLocked(windowId, sourceId); 169 } else { 170 refreshCachedNodeLocked(windowId, sourceId); 171 } 172 } 173 } break; 174 175 case AccessibilityEvent.TYPE_VIEW_SCROLLED: { 176 clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId()); 177 } break; 178 179 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 180 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { 181 clear(); 182 } break; 183 } 184 } 185 186 if (CHECK_INTEGRITY) { 187 checkIntegrity(); 188 } 189 } 190 191 private void refreshCachedNodeLocked(int windowId, long sourceId) { 192 if (DEBUG) { 193 Log.i(LOG_TAG, "Refreshing cached node."); 194 } 195 196 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 197 if (nodes == null) { 198 return; 199 } 200 AccessibilityNodeInfo cachedInfo = nodes.get(sourceId); 201 // If the source is not in the cache - nothing to do. 202 if (cachedInfo == null) { 203 return; 204 } 205 // The node changed so we will just refresh it right now. 206 if (mAccessibilityNodeRefresher.refreshNode(cachedInfo, true)) { 207 return; 208 } 209 // Weird, we could not refresh. Just evict the entire sub-tree. 210 clearSubTreeLocked(windowId, sourceId); 211 } 212 213 /** 214 * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting 215 * window and the accessibility id of the node. 216 * 217 * @param windowId The id of the window hosting the node. 218 * @param accessibilityNodeId The info accessibility node id. 219 * @return The cached {@link AccessibilityNodeInfo} or null if such not found. 220 */ 221 public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) { 222 synchronized(mLock) { 223 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 224 if (nodes == null) { 225 return null; 226 } 227 AccessibilityNodeInfo info = nodes.get(accessibilityNodeId); 228 if (info != null) { 229 // Return a copy since the client calls to AccessibilityNodeInfo#recycle() 230 // will wipe the data of the cached info. 231 info = AccessibilityNodeInfo.obtain(info); 232 } 233 if (DEBUG) { 234 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); 235 } 236 return info; 237 } 238 } 239 240 public List<AccessibilityWindowInfo> getWindows() { 241 synchronized (mLock) { 242 if (!mIsAllWindowsCached) { 243 return null; 244 } 245 246 final int windowCount = mWindowCache.size(); 247 if (windowCount > 0) { 248 // Careful to return the windows in a decreasing layer order. 249 SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray; 250 sortedWindows.clear(); 251 252 for (int i = 0; i < windowCount; i++) { 253 AccessibilityWindowInfo window = mWindowCache.valueAt(i); 254 sortedWindows.put(window.getLayer(), window); 255 } 256 257 // It's possible in transient conditions for two windows to share the same 258 // layer, which results in sortedWindows being smaller than mWindowCache 259 final int sortedWindowCount = sortedWindows.size(); 260 List<AccessibilityWindowInfo> windows = new ArrayList<>(sortedWindowCount); 261 for (int i = sortedWindowCount - 1; i >= 0; i--) { 262 AccessibilityWindowInfo window = sortedWindows.valueAt(i); 263 windows.add(AccessibilityWindowInfo.obtain(window)); 264 sortedWindows.removeAt(i); 265 } 266 267 return windows; 268 } 269 return null; 270 } 271 } 272 273 public AccessibilityWindowInfo getWindow(int windowId) { 274 synchronized (mLock) { 275 AccessibilityWindowInfo window = mWindowCache.get(windowId); 276 if (window != null) { 277 return AccessibilityWindowInfo.obtain(window); 278 } 279 return null; 280 } 281 } 282 283 /** 284 * Caches an {@link AccessibilityNodeInfo}. 285 * 286 * @param info The node to cache. 287 */ 288 public void add(AccessibilityNodeInfo info) { 289 synchronized(mLock) { 290 if (DEBUG) { 291 Log.i(LOG_TAG, "add(" + info + ")"); 292 } 293 294 final int windowId = info.getWindowId(); 295 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 296 if (nodes == null) { 297 nodes = new LongSparseArray<>(); 298 mNodeCache.put(windowId, nodes); 299 } 300 301 final long sourceId = info.getSourceNodeId(); 302 AccessibilityNodeInfo oldInfo = nodes.get(sourceId); 303 if (oldInfo != null) { 304 // If the added node is in the cache we have to be careful if 305 // the new one represents a source state where some of the 306 // children have been removed to remove the descendants that 307 // are no longer present. 308 final LongArray newChildrenIds = info.getChildNodeIds(); 309 310 final int oldChildCount = oldInfo.getChildCount(); 311 for (int i = 0; i < oldChildCount; i++) { 312 if (nodes.get(sourceId) == null) { 313 // We've removed (and thus recycled) this node because it was its own 314 // ancestor (the app gave us bad data), we can't continue using it. 315 // Clear the cache for this window and give up on adding the node. 316 clearNodesForWindowLocked(windowId); 317 return; 318 } 319 final long oldChildId = oldInfo.getChildId(i); 320 // If the child is no longer present, remove the sub-tree. 321 if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) { 322 clearSubTreeLocked(windowId, oldChildId); 323 } 324 } 325 326 // Also be careful if the parent has changed since the new 327 // parent may be a predecessor of the old parent which will 328 // add cycles to the cache. 329 final long oldParentId = oldInfo.getParentNodeId(); 330 if (info.getParentNodeId() != oldParentId) { 331 clearSubTreeLocked(windowId, oldParentId); 332 } else { 333 oldInfo.recycle(); 334 } 335 } 336 337 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() 338 // will wipe the data of the cached info. 339 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info); 340 nodes.put(sourceId, clone); 341 if (clone.isAccessibilityFocused()) { 342 mAccessibilityFocus = sourceId; 343 } 344 if (clone.isFocused()) { 345 mInputFocus = sourceId; 346 } 347 } 348 } 349 350 /** 351 * Clears the cache. 352 */ 353 public void clear() { 354 synchronized(mLock) { 355 if (DEBUG) { 356 Log.i(LOG_TAG, "clear()"); 357 } 358 clearWindowCache(); 359 final int nodesForWindowCount = mNodeCache.size(); 360 for (int i = 0; i < nodesForWindowCount; i++) { 361 final int windowId = mNodeCache.keyAt(i); 362 clearNodesForWindowLocked(windowId); 363 } 364 365 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 366 mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; 367 } 368 } 369 370 private void clearWindowCache() { 371 final int windowCount = mWindowCache.size(); 372 for (int i = windowCount - 1; i >= 0; i--) { 373 AccessibilityWindowInfo window = mWindowCache.valueAt(i); 374 window.recycle(); 375 mWindowCache.removeAt(i); 376 } 377 mIsAllWindowsCached = false; 378 } 379 380 /** 381 * Clears nodes for the window with the given id 382 */ 383 private void clearNodesForWindowLocked(int windowId) { 384 if (DEBUG) { 385 Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")"); 386 } 387 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 388 if (nodes == null) { 389 return; 390 } 391 // Recycle the nodes before clearing the cache. 392 final int nodeCount = nodes.size(); 393 for (int i = nodeCount - 1; i >= 0; i--) { 394 AccessibilityNodeInfo info = nodes.valueAt(i); 395 nodes.removeAt(i); 396 info.recycle(); 397 } 398 mNodeCache.remove(windowId); 399 } 400 401 /** 402 * Clears a subtree rooted at the node with the given id that is 403 * hosted in a given window. 404 * 405 * @param windowId The id of the hosting window. 406 * @param rootNodeId The root id. 407 */ 408 private void clearSubTreeLocked(int windowId, long rootNodeId) { 409 if (DEBUG) { 410 Log.i(LOG_TAG, "Clearing cached subtree."); 411 } 412 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); 413 if (nodes != null) { 414 clearSubTreeRecursiveLocked(nodes, rootNodeId); 415 } 416 } 417 418 /** 419 * Clears a subtree given a pointer to the root id and the nodes 420 * in the hosting window. 421 * 422 * @param nodes The nodes in the hosting window. 423 * @param rootNodeId The id of the root to evict. 424 */ 425 private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, 426 long rootNodeId) { 427 AccessibilityNodeInfo current = nodes.get(rootNodeId); 428 if (current == null) { 429 return; 430 } 431 nodes.remove(rootNodeId); 432 final int childCount = current.getChildCount(); 433 for (int i = 0; i < childCount; i++) { 434 final long childNodeId = current.getChildId(i); 435 clearSubTreeRecursiveLocked(nodes, childNodeId); 436 } 437 current.recycle(); 438 } 439 440 /** 441 * Check the integrity of the cache which is nodes from different windows 442 * are not mixed, there is a single active window, there is a single focused 443 * window, for every window there are no duplicates nodes, all nodes for a 444 * window are connected, for every window there is a single input focused 445 * node, and for every window there is a single accessibility focused node. 446 */ 447 public void checkIntegrity() { 448 synchronized (mLock) { 449 // Get the root. 450 if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) { 451 return; 452 } 453 454 AccessibilityWindowInfo focusedWindow = null; 455 AccessibilityWindowInfo activeWindow = null; 456 457 final int windowCount = mWindowCache.size(); 458 for (int i = 0; i < windowCount; i++) { 459 AccessibilityWindowInfo window = mWindowCache.valueAt(i); 460 461 // Check for one active window. 462 if (window.isActive()) { 463 if (activeWindow != null) { 464 Log.e(LOG_TAG, "Duplicate active window:" + window); 465 } else { 466 activeWindow = window; 467 } 468 } 469 470 // Check for one focused window. 471 if (window.isFocused()) { 472 if (focusedWindow != null) { 473 Log.e(LOG_TAG, "Duplicate focused window:" + window); 474 } else { 475 focusedWindow = window; 476 } 477 } 478 } 479 480 // Traverse the tree and do some checks. 481 AccessibilityNodeInfo accessFocus = null; 482 AccessibilityNodeInfo inputFocus = null; 483 484 final int nodesForWindowCount = mNodeCache.size(); 485 for (int i = 0; i < nodesForWindowCount; i++) { 486 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i); 487 if (nodes.size() <= 0) { 488 continue; 489 } 490 491 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>(); 492 final int windowId = mNodeCache.keyAt(i); 493 494 final int nodeCount = nodes.size(); 495 for (int j = 0; j < nodeCount; j++) { 496 AccessibilityNodeInfo node = nodes.valueAt(j); 497 498 // Check for duplicates 499 if (!seen.add(node)) { 500 Log.e(LOG_TAG, "Duplicate node: " + node 501 + " in window:" + windowId); 502 // Stop now as we potentially found a loop. 503 continue; 504 } 505 506 // Check for one accessibility focus. 507 if (node.isAccessibilityFocused()) { 508 if (accessFocus != null) { 509 Log.e(LOG_TAG, "Duplicate accessibility focus:" + node 510 + " in window:" + windowId); 511 } else { 512 accessFocus = node; 513 } 514 } 515 516 // Check for one input focus. 517 if (node.isFocused()) { 518 if (inputFocus != null) { 519 Log.e(LOG_TAG, "Duplicate input focus: " + node 520 + " in window:" + windowId); 521 } else { 522 inputFocus = node; 523 } 524 } 525 526 // The node should be a child of its parent if we have the parent. 527 AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId()); 528 if (nodeParent != null) { 529 boolean childOfItsParent = false; 530 final int childCount = nodeParent.getChildCount(); 531 for (int k = 0; k < childCount; k++) { 532 AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k)); 533 if (child == node) { 534 childOfItsParent = true; 535 break; 536 } 537 } 538 if (!childOfItsParent) { 539 Log.e(LOG_TAG, "Invalid parent-child relation between parent: " 540 + nodeParent + " and child: " + node); 541 } 542 } 543 544 // The node should be the parent of its child if we have the child. 545 final int childCount = node.getChildCount(); 546 for (int k = 0; k < childCount; k++) { 547 AccessibilityNodeInfo child = nodes.get(node.getChildId(k)); 548 if (child != null) { 549 AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId()); 550 if (parent != node) { 551 Log.e(LOG_TAG, "Invalid child-parent relation between child: " 552 + node + " and parent: " + nodeParent); 553 } 554 } 555 } 556 } 557 } 558 } 559 } 560 561 // Layer of indirection included to break dependency chain for testing 562 public static class AccessibilityNodeRefresher { 563 public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) { 564 return info.refresh(null, bypassCache); 565 } 566 } 567} 568