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