• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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