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