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