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 
setWindows(List<AccessibilityWindowInfo> windows)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 
addWindow(AccessibilityWindowInfo window)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      */
onAccessibilityEvent(AccessibilityEvent event)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 
refreshCachedNodeLocked(int windowId, long sourceId)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      */
getNode(int windowId, long accessibilityNodeId)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 
getWindows()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 
getWindow(int windowId)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      */
add(AccessibilityNodeInfo info)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      */
clear()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 
clearWindowCache()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 
clearNodesForWindowLocked(int windowId)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      */
clearSubTreeLocked(int windowId, long rootNodeId)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      */
clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, long rootNodeId)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      */
checkIntegrity()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