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