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 
20 import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_ACCESSIBILITY;
21 
22 import android.os.Build;
23 import android.os.SystemClock;
24 import android.util.ArraySet;
25 import android.util.Log;
26 import android.util.LongArray;
27 import android.util.LongSparseArray;
28 import android.util.SparseArray;
29 
30 import java.util.ArrayList;
31 import java.util.List;
32 
33 /**
34  * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos.
35  * It is updated when windows change or nodes change.
36  * @hide
37  */
38 public class AccessibilityCache {
39 
40     private static final String LOG_TAG = "AccessibilityCache";
41 
42     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE;
43 
44     private static final boolean VERBOSE =
45             Log.isLoggable(LOG_TAG, Log.VERBOSE) && Build.IS_DEBUGGABLE;
46 
47     private static final boolean CHECK_INTEGRITY = Build.IS_ENG;
48 
49     private boolean mEnabled = true;
50 
51     /**
52      * {@link AccessibilityEvent} types that are critical for the cache to stay up to date
53      *
54      * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to
55      * make sure that the events are delivered to cache regardless of
56      * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes}
57      */
58     public static final int CACHE_CRITICAL_EVENTS_MASK =
59             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
60                     | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
61                     | AccessibilityEvent.TYPE_VIEW_FOCUSED
62                     | AccessibilityEvent.TYPE_VIEW_SELECTED
63                     | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
64                     | AccessibilityEvent.TYPE_VIEW_CLICKED
65                     | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
66                     | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
67                     | AccessibilityEvent.TYPE_VIEW_SCROLLED
68                     | AccessibilityEvent.TYPE_WINDOWS_CHANGED
69                     | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
70 
71     private final Object mLock = new Object();
72 
73     private final AccessibilityNodeRefresher mAccessibilityNodeRefresher;
74 
75     private OnNodeAddedListener mOnNodeAddedListener;
76 
77     private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
78     private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
79     /**
80      * The event time of the {@link AccessibilityEvent} which presents the populated windows cache
81      * before it is stale.
82      */
83     private long mValidWindowCacheTimeStamp = 0;
84 
85     private int mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
86     private int mInputFocusWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
87 
88     private boolean mIsAllWindowsCached;
89 
90     // The SparseArray of all {@link AccessibilityWindowInfo}s on all displays.
91     // The key of outer SparseArray is display ID and the key of inner SparseArray is window ID.
92     private final SparseArray<SparseArray<AccessibilityWindowInfo>> mWindowCacheByDisplay =
93             new SparseArray<>();
94 
95     private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
96             new SparseArray<>();
97 
98     private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
99             new SparseArray<>();
100 
AccessibilityCache(AccessibilityNodeRefresher nodeRefresher)101     public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
102         mAccessibilityNodeRefresher = nodeRefresher;
103     }
104 
105     /** Returns if the cache is enabled. */
isEnabled()106     public boolean isEnabled() {
107         synchronized (mLock) {
108             return mEnabled;
109         }
110     }
111 
112     /** Sets enabled status. */
setEnabled(boolean enabled)113     public void setEnabled(boolean enabled) {
114         synchronized (mLock) {
115             mEnabled = enabled;
116             clear();
117         }
118     }
119 
120     /**
121      * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache.
122      * The key of SparseArray is display ID.
123      *
124      * @param windowsOnAllDisplays The accessibility windows of all displays.
125      * @param populationTimeStamp The timestamp from {@link SystemClock#uptimeMillis()} when the
126      *                            client requests the data.
127      */
setWindowsOnAllDisplays( SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays, long populationTimeStamp)128     public void setWindowsOnAllDisplays(
129             SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays,
130             long populationTimeStamp) {
131         synchronized (mLock) {
132             if (!mEnabled) {
133                 if (DEBUG) {
134                     Log.i(LOG_TAG, "Cache is disabled");
135                 }
136                 return;
137             }
138             if (DEBUG) {
139                 Log.i(LOG_TAG, "Set windows");
140             }
141             if (mValidWindowCacheTimeStamp > populationTimeStamp) {
142                 // Discard the windows because it might be stale.
143                 return;
144             }
145             clearWindowCacheLocked();
146             if (windowsOnAllDisplays == null) {
147                 return;
148             }
149 
150             final int displayCounts = windowsOnAllDisplays.size();
151             for (int i = 0; i < displayCounts; i++) {
152                 final List<AccessibilityWindowInfo> windowsOfDisplay =
153                         windowsOnAllDisplays.valueAt(i);
154 
155                 if (windowsOfDisplay == null) {
156                     continue;
157                 }
158 
159                 final int displayId = windowsOnAllDisplays.keyAt(i);
160                 final int windowCount = windowsOfDisplay.size();
161                 for (int j = 0; j < windowCount; j++) {
162                     addWindowByDisplayLocked(displayId, windowsOfDisplay.get(j));
163                 }
164             }
165             mIsAllWindowsCached = true;
166         }
167     }
168 
169     /**
170      * Sets an {@link AccessibilityWindowInfo} into the cache.
171      *
172      * @param window The accessibility window.
173      */
addWindow(AccessibilityWindowInfo window)174     public void addWindow(AccessibilityWindowInfo window) {
175         synchronized (mLock) {
176             if (!mEnabled) {
177                 if (DEBUG) {
178                     Log.i(LOG_TAG, "Cache is disabled");
179                 }
180                 return;
181             }
182             if (DEBUG) {
183                 Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ "
184                         + window.getDisplayId() + " ]");
185             }
186             addWindowByDisplayLocked(window.getDisplayId(), window);
187         }
188     }
189 
addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window)190     private void addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window) {
191         SparseArray<AccessibilityWindowInfo> windows = mWindowCacheByDisplay.get(displayId);
192         if (windows == null) {
193             windows = new SparseArray<>();
194             mWindowCacheByDisplay.put(displayId, windows);
195         }
196         final int windowId = window.getId();
197         windows.put(windowId, new AccessibilityWindowInfo(window));
198     }
199     /**
200      * Notifies the cache that the something in the UI changed. As a result
201      * the cache will either refresh some nodes or evict some nodes.
202      *
203      * Note: any event that ends up affecting the cache should also be present in
204      * {@link #CACHE_CRITICAL_EVENTS_MASK}
205      *
206      * @param event An event.
207      */
onAccessibilityEvent(AccessibilityEvent event)208     public void onAccessibilityEvent(AccessibilityEvent event) {
209         AccessibilityNodeInfo nodeToRefresh = null;
210         synchronized (mLock) {
211             if (!mEnabled) {
212                 if (DEBUG) {
213                     Log.i(LOG_TAG, "Cache is disabled");
214                 }
215                 return;
216             }
217             if (DEBUG) {
218                 Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")");
219             }
220             final int eventType = event.getEventType();
221             switch (eventType) {
222                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
223                     if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
224                         removeCachedNodeLocked(mAccessibilityFocusedWindow, mAccessibilityFocus);
225                     }
226                     mAccessibilityFocus = event.getSourceNodeId();
227                     mAccessibilityFocusedWindow = event.getWindowId();
228                     nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
229                             mAccessibilityFocus);
230                 } break;
231 
232                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
233                     if (mAccessibilityFocus == event.getSourceNodeId()
234                             && mAccessibilityFocusedWindow == event.getWindowId()) {
235                         nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
236                                 mAccessibilityFocus);
237                         mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
238                         mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
239                     }
240                 } break;
241 
242                 case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
243                     if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
244                         removeCachedNodeLocked(event.getWindowId(), mInputFocus);
245                     }
246                     mInputFocus = event.getSourceNodeId();
247                     mInputFocusWindow = event.getWindowId();
248                     nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), mInputFocus);
249                 } break;
250 
251                 case AccessibilityEvent.TYPE_VIEW_SELECTED:
252                 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
253                 case AccessibilityEvent.TYPE_VIEW_CLICKED:
254                 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
255                     nodeToRefresh = removeCachedNodeLocked(event.getWindowId(),
256                             event.getSourceNodeId());
257                 } break;
258 
259                 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
260                     synchronized (mLock) {
261                         final int windowId = event.getWindowId();
262                         final long sourceId = event.getSourceNodeId();
263                         if ((event.getContentChangeTypes()
264                                 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
265                             clearSubTreeLocked(windowId, sourceId);
266                         } else {
267                             nodeToRefresh = removeCachedNodeLocked(windowId, sourceId);
268                         }
269                     }
270                 } break;
271 
272                 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
273                     clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
274                 } break;
275 
276                 case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
277                     mValidWindowCacheTimeStamp = event.getEventTime();
278                     if (event.getWindowChanges()
279                             == AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED) {
280                         // Don't need to clear all cache. Unless the changes are related to
281                         // content, we won't clear all cache here with clear().
282                         clearWindowCacheLocked();
283                         break;
284                     }
285                 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
286                     mValidWindowCacheTimeStamp = event.getEventTime();
287                     clear();
288                 } break;
289             }
290         }
291 
292         if (nodeToRefresh != null) {
293             if (DEBUG) {
294                 Log.i(LOG_TAG, "Refreshing and re-adding cached node.");
295             }
296             if (mAccessibilityNodeRefresher.refreshNode(nodeToRefresh, true)) {
297                 add(nodeToRefresh);
298             }
299         }
300         if (CHECK_INTEGRITY) {
301             checkIntegrity();
302         }
303     }
304 
removeCachedNodeLocked(int windowId, long sourceId)305     private AccessibilityNodeInfo removeCachedNodeLocked(int windowId, long sourceId) {
306         if (DEBUG) {
307             Log.i(LOG_TAG, "Removing cached node.");
308         }
309         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
310         if (nodes == null) {
311             return null;
312         }
313         AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
314         // If the source is not in the cache - nothing to do.
315         if (cachedInfo == null) {
316             return null;
317         }
318         nodes.remove(sourceId);
319         return cachedInfo;
320     }
321 
322     /**
323      * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
324      * window and the accessibility id of the node.
325      *
326      * @param windowId The id of the window hosting the node.
327      * @param accessibilityNodeId The info accessibility node id.
328      * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
329      */
getNode(int windowId, long accessibilityNodeId)330     public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
331         synchronized(mLock) {
332             if (!mEnabled) {
333                 if (DEBUG) {
334                     Log.i(LOG_TAG, "Cache is disabled");
335                 }
336                 return null;
337             }
338             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
339             if (nodes == null) {
340                 return null;
341             }
342             AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
343             if (info != null) {
344                 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
345                 // will wipe the data of the cached info.
346                 info = new AccessibilityNodeInfo(info);
347             }
348             if (VERBOSE) {
349                 Log.i(LOG_TAG, "get(0x" + Long.toHexString(accessibilityNodeId) + ") = " + info);
350             }
351             return info;
352         }
353     }
354 
355     /** Returns {@code true} if {@code info} is in the cache. */
isNodeInCache(AccessibilityNodeInfo info)356     public boolean isNodeInCache(AccessibilityNodeInfo info) {
357         if (info == null) {
358             return false;
359         }
360         int windowId = info.getWindowId();
361         long accessibilityNodeId = info.getSourceNodeId();
362         synchronized (mLock) {
363             if (!mEnabled) {
364                 if (DEBUG) {
365                     Log.i(LOG_TAG, "Cache is disabled");
366                 }
367                 return false;
368             }
369             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
370             if (nodes == null) {
371                 return false;
372             }
373             return nodes.get(accessibilityNodeId) != null;
374         }
375     }
376 
377     /**
378      * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache.
379      *
380      * @return All cached {@link AccessibilityWindowInfo}s of all displays
381      *         or null if such not found. The key of SparseArray is display ID.
382      */
getWindowsOnAllDisplays()383     public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
384         synchronized (mLock) {
385             if (!mEnabled) {
386                 if (DEBUG) {
387                     Log.i(LOG_TAG, "Cache is disabled");
388                 }
389                 return null;
390             }
391             if (!mIsAllWindowsCached) {
392                 return null;
393             }
394             final SparseArray<List<AccessibilityWindowInfo>> returnWindows = new SparseArray<>();
395             final int displayCounts = mWindowCacheByDisplay.size();
396 
397             if (displayCounts > 0) {
398                 for (int i = 0; i < displayCounts; i++) {
399                     final int displayId = mWindowCacheByDisplay.keyAt(i);
400                     final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
401                             mWindowCacheByDisplay.valueAt(i);
402 
403                     if (windowsOfDisplay == null) {
404                         continue;
405                     }
406 
407                     final int windowCount = windowsOfDisplay.size();
408                     if (windowCount > 0) {
409                         // Careful to return the windows in a decreasing layer order.
410                         SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
411                         sortedWindows.clear();
412 
413                         for (int j = 0; j < windowCount; j++) {
414                             AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
415                             sortedWindows.put(window.getLayer(), window);
416                         }
417 
418                         // It's possible in transient conditions for two windows to share the same
419                         // layer, which results in sortedWindows being smaller than
420                         // mWindowCacheByDisplay
421                         final int sortedWindowCount = sortedWindows.size();
422                         List<AccessibilityWindowInfo> windows =
423                                 new ArrayList<>(sortedWindowCount);
424                         for (int j = sortedWindowCount - 1; j >= 0; j--) {
425                             AccessibilityWindowInfo window = sortedWindows.valueAt(j);
426                             windows.add(new AccessibilityWindowInfo(window));
427                             sortedWindows.removeAt(j);
428                         }
429                         returnWindows.put(displayId, windows);
430                     }
431                 }
432                 return returnWindows;
433             }
434             return null;
435         }
436     }
437 
438     /**
439      * Gets an {@link AccessibilityWindowInfo} by windowId.
440      *
441      * @param windowId The id of the window.
442      *
443      * @return The {@link AccessibilityWindowInfo} or null if such not found.
444      */
getWindow(int windowId)445     public AccessibilityWindowInfo getWindow(int windowId) {
446         synchronized (mLock) {
447             if (!mEnabled) {
448                 if (DEBUG) {
449                     Log.i(LOG_TAG, "Cache is disabled");
450                 }
451                 return null;
452             }
453             final int displayCounts = mWindowCacheByDisplay.size();
454             for (int i = 0; i < displayCounts; i++) {
455                 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
456                         mWindowCacheByDisplay.valueAt(i);
457                 if (windowsOfDisplay == null) {
458                     continue;
459                 }
460 
461                 AccessibilityWindowInfo window = windowsOfDisplay.get(windowId);
462                 if (window != null) {
463                     return new AccessibilityWindowInfo(window);
464                 }
465             }
466             return null;
467         }
468     }
469 
470     /**
471      * Caches an {@link AccessibilityNodeInfo}.
472      *
473      * @param info The node to cache.
474      */
add(AccessibilityNodeInfo info)475     public void add(AccessibilityNodeInfo info) {
476         synchronized(mLock) {
477             if (!mEnabled) {
478                 if (DEBUG) {
479                     Log.i(LOG_TAG, "Cache is disabled");
480                 }
481                 return;
482             }
483             if (VERBOSE) {
484                 Log.i(LOG_TAG, "add(" + info + ")");
485             }
486 
487             final int windowId = info.getWindowId();
488             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
489             if (nodes == null) {
490                 nodes = new LongSparseArray<>();
491                 mNodeCache.put(windowId, nodes);
492             }
493 
494             final long sourceId = info.getSourceNodeId();
495             AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
496             if (oldInfo != null) {
497                 // If the added node is in the cache we have to be careful if
498                 // the new one represents a source state where some of the
499                 // children have been removed to remove the descendants that
500                 // are no longer present.
501                 final LongArray newChildrenIds = info.getChildNodeIds();
502 
503                 final int oldChildCount = oldInfo.getChildCount();
504                 for (int i = 0; i < oldChildCount; i++) {
505                     final long oldChildId = oldInfo.getChildId(i);
506                     // If the child is no longer present, remove the sub-tree.
507                     if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
508                         clearSubTreeLocked(windowId, oldChildId);
509                     }
510                     if (nodes.get(sourceId) == null) {
511                         // We've removed (and thus recycled) this node because it was its own
512                         // ancestor (the app gave us bad data), we can't continue using it.
513                         // Clear the cache for this window and give up on adding the node.
514                         clearNodesForWindowLocked(windowId);
515                         return;
516                     }
517                 }
518 
519                 // Also be careful if the parent has changed since the new
520                 // parent may be a predecessor of the old parent which will
521                 // add cycles to the cache.
522                 final long oldParentId = oldInfo.getParentNodeId();
523                 if (info.getParentNodeId() != oldParentId) {
524                     clearSubTreeLocked(windowId, oldParentId);
525                 }
526            }
527 
528             // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
529             // will wipe the data of the cached info.
530             AccessibilityNodeInfo clone = new AccessibilityNodeInfo(info);
531             nodes.put(sourceId, clone);
532             if (clone.isAccessibilityFocused()) {
533                 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID
534                         && mAccessibilityFocus != sourceId) {
535                     removeCachedNodeLocked(windowId, mAccessibilityFocus);
536                 }
537                 mAccessibilityFocus = sourceId;
538                 mAccessibilityFocusedWindow = windowId;
539             } else if (mAccessibilityFocus == sourceId) {
540                 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
541                 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
542             }
543             if (clone.isFocused()) {
544                 mInputFocus = sourceId;
545                 mInputFocusWindow = windowId;
546             }
547 
548             if (mOnNodeAddedListener != null) {
549                 mOnNodeAddedListener.onNodeAdded(clone);
550             }
551         }
552     }
553 
554     /**
555      * Clears the cache.
556      */
clear()557     public void clear() {
558         synchronized(mLock) {
559             if (DEBUG) {
560                 Log.i(LOG_TAG, "clear()");
561             }
562             clearWindowCacheLocked();
563             final int nodesForWindowCount = mNodeCache.size();
564             for (int i = nodesForWindowCount - 1; i >= 0; i--) {
565                 final int windowId = mNodeCache.keyAt(i);
566                 clearNodesForWindowLocked(windowId);
567             }
568 
569             mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
570             mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
571 
572             mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
573             mInputFocusWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
574         }
575     }
576 
clearWindowCacheLocked()577     private void clearWindowCacheLocked() {
578         if (DEBUG) {
579             Log.i(LOG_TAG, "clearWindowCacheLocked");
580         }
581         final int displayCounts = mWindowCacheByDisplay.size();
582 
583         if (displayCounts > 0) {
584             for (int i = displayCounts - 1; i >= 0; i--) {
585                 final int displayId = mWindowCacheByDisplay.keyAt(i);
586                 final SparseArray<AccessibilityWindowInfo> windows =
587                         mWindowCacheByDisplay.get(displayId);
588                 if (windows != null) {
589                     windows.clear();
590                 }
591                 mWindowCacheByDisplay.remove(displayId);
592             }
593         }
594         mIsAllWindowsCached = false;
595     }
596 
597     /**
598      * Gets a cached {@link AccessibilityNodeInfo} with focus according to focus type.
599      *
600      * Note: {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} will return
601      * null.
602      *
603      * @param focusType The focus type.
604      * @param windowId A unique window id.
605      * @param initialNodeId A unique view id or virtual descendant id from where to start the
606      *                      search.
607      * @return The cached {@link AccessibilityNodeInfo} if it has a11y focus or null if such not
608      * found.
609      */
getFocus(int focusType, long initialNodeId, int windowId)610     public AccessibilityNodeInfo getFocus(int focusType, long initialNodeId, int windowId) {
611         synchronized (mLock) {
612             if (!mEnabled) {
613                 if (DEBUG) {
614                     Log.i(LOG_TAG, "Cache is disabled");
615                 }
616                 return null;
617             }
618             int currentFocusWindowId;
619             long currentFocusId;
620             if (focusType == FOCUS_ACCESSIBILITY) {
621                 currentFocusWindowId = mAccessibilityFocusedWindow;
622                 currentFocusId = mAccessibilityFocus;
623             } else {
624                 currentFocusWindowId = mInputFocusWindow;
625                 currentFocusId = mInputFocus;
626             }
627 
628             if (currentFocusWindowId == AccessibilityWindowInfo.UNDEFINED_WINDOW_ID) {
629                 return null;
630             }
631 
632             if (windowId != AccessibilityWindowInfo.ANY_WINDOW_ID
633                     && windowId != currentFocusWindowId) {
634                 return null;
635             }
636 
637             LongSparseArray<AccessibilityNodeInfo> nodes =
638                     mNodeCache.get(currentFocusWindowId);
639             if (nodes == null) {
640                 return null;
641             }
642 
643             final AccessibilityNodeInfo currentFocusedNode = nodes.get(currentFocusId);
644             if (currentFocusedNode == null) {
645                 return null;
646             }
647 
648             if (initialNodeId == currentFocusId || (isCachedNodeOrDescendantLocked(
649                     currentFocusedNode.getParentNodeId(), initialNodeId, nodes))) {
650                 if (VERBOSE) {
651                     Log.i(LOG_TAG, "getFocus(0x" + Long.toHexString(currentFocusId) + ") = "
652                             + currentFocusedNode + " with type: "
653                             + (focusType == FOCUS_ACCESSIBILITY
654                             ? "FOCUS_ACCESSIBILITY"
655                             : "FOCUS_INPUT"));
656                 }
657                 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
658                 // will wipe the data of the cached info.
659                 return new AccessibilityNodeInfo(currentFocusedNode);
660             }
661 
662             if (VERBOSE) {
663                 Log.i(LOG_TAG, "getFocus is null with type: "
664                         + (focusType == FOCUS_ACCESSIBILITY
665                         ? "FOCUS_ACCESSIBILITY"
666                         : "FOCUS_INPUT"));
667             }
668             return null;
669         }
670     }
671 
isCachedNodeOrDescendantLocked(long nodeId, long ancestorId, LongSparseArray<AccessibilityNodeInfo> nodes)672     private boolean isCachedNodeOrDescendantLocked(long nodeId, long ancestorId,
673             LongSparseArray<AccessibilityNodeInfo> nodes) {
674         if (ancestorId == nodeId) {
675             return true;
676         }
677         AccessibilityNodeInfo node = nodes.get(nodeId);
678         if (node == null) {
679             return false;
680         }
681         return isCachedNodeOrDescendantLocked(node.getParentNodeId(), ancestorId,  nodes);
682     }
683 
684     /**
685      * Clears nodes for the window with the given id
686      */
clearNodesForWindowLocked(int windowId)687     private void clearNodesForWindowLocked(int windowId) {
688         if (DEBUG) {
689             Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
690         }
691         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
692         if (nodes == null) {
693             return;
694         }
695         mNodeCache.remove(windowId);
696     }
697 
698     /** Clears a subtree rooted at {@code info}. */
clearSubTree(AccessibilityNodeInfo info)699     public boolean clearSubTree(AccessibilityNodeInfo info) {
700         if (info == null) {
701             return false;
702         }
703         synchronized (mLock) {
704             if (!mEnabled) {
705                 if (DEBUG) {
706                     Log.i(LOG_TAG, "Cache is disabled");
707                 }
708                 return false;
709             }
710             clearSubTreeLocked(info.getWindowId(), info.getSourceNodeId());
711             return true;
712         }
713     }
714 
715     /**
716      * Clears a subtree rooted at the node with the given id that is
717      * hosted in a given window.
718      *
719      * @param windowId The id of the hosting window.
720      * @param rootNodeId The root id.
721      */
clearSubTreeLocked(int windowId, long rootNodeId)722     private void clearSubTreeLocked(int windowId, long rootNodeId) {
723         if (DEBUG) {
724             Log.i(LOG_TAG, "Clearing cached subtree.");
725         }
726         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
727         if (nodes != null) {
728             clearSubTreeRecursiveLocked(nodes, rootNodeId);
729         }
730     }
731 
732     /**
733      * Clears a subtree given a pointer to the root id and the nodes
734      * in the hosting window.
735      *
736      * @param nodes The nodes in the hosting window.
737      * @param rootNodeId The id of the root to evict.
738      *
739      * @return {@code true} if the cache was cleared
740      */
clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, long rootNodeId)741     private boolean clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
742             long rootNodeId) {
743         AccessibilityNodeInfo current = nodes.get(rootNodeId);
744         if (current == null) {
745             // The node isn't in the cache, but its descendents might be.
746             clear();
747             return true;
748         }
749         nodes.remove(rootNodeId);
750         final int childCount = current.getChildCount();
751         for (int i = 0; i < childCount; i++) {
752             final long childNodeId = current.getChildId(i);
753             if (clearSubTreeRecursiveLocked(nodes, childNodeId)) {
754                 return true;
755             }
756         }
757         return false;
758     }
759 
760     /**
761      * Check the integrity of the cache which is nodes from different windows
762      * are not mixed, there is a single active window, there is a single focused
763      * window, for every window there are no duplicates nodes, all nodes for a
764      * window are connected, for every window there is a single input focused
765      * node, and for every window there is a single accessibility focused node.
766      */
checkIntegrity()767     public void checkIntegrity() {
768         synchronized (mLock) {
769             // Get the root.
770             if (mWindowCacheByDisplay.size() <= 0 && mNodeCache.size() == 0) {
771                 return;
772             }
773 
774             AccessibilityWindowInfo focusedWindow = null;
775             AccessibilityWindowInfo activeWindow = null;
776 
777             final int displayCounts = mWindowCacheByDisplay.size();
778             for (int i = 0; i < displayCounts; i++) {
779                 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
780                         mWindowCacheByDisplay.valueAt(i);
781 
782                 if (windowsOfDisplay == null) {
783                     continue;
784                 }
785 
786                 final int windowCount = windowsOfDisplay.size();
787                 for (int j = 0; j < windowCount; j++) {
788                     final AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
789 
790                     // Check for one active window.
791                     if (window.isActive()) {
792                         if (activeWindow != null) {
793                             Log.e(LOG_TAG, "Duplicate active window:" + window);
794                         } else {
795                             activeWindow = window;
796                         }
797                     }
798                     // Check for one focused window.
799                     if (window.isFocused()) {
800                         if (focusedWindow != null) {
801                             Log.e(LOG_TAG, "Duplicate focused window:" + window);
802                         } else {
803                             focusedWindow = window;
804                         }
805                     }
806                 }
807             }
808 
809             // Traverse the tree and do some checks.
810             AccessibilityNodeInfo accessFocus = null;
811             AccessibilityNodeInfo inputFocus = null;
812 
813             final int nodesForWindowCount = mNodeCache.size();
814             for (int i = 0; i < nodesForWindowCount; i++) {
815                 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
816                 if (nodes.size() <= 0) {
817                     continue;
818                 }
819 
820                 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
821                 final int windowId = mNodeCache.keyAt(i);
822 
823                 final int nodeCount = nodes.size();
824                 for (int j = 0; j < nodeCount; j++) {
825                     AccessibilityNodeInfo node = nodes.valueAt(j);
826 
827                     // Check for duplicates
828                     if (!seen.add(node)) {
829                         Log.e(LOG_TAG, "Duplicate node: " + node
830                                 + " in window:" + windowId);
831                         // Stop now as we potentially found a loop.
832                         continue;
833                     }
834 
835                     // Check for one accessibility focus.
836                     if (node.isAccessibilityFocused()) {
837                         if (accessFocus != null) {
838                             Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
839                                     + " in window:" + windowId);
840                         } else {
841                             accessFocus = node;
842                         }
843                     }
844 
845                     // Check for one input focus.
846                     if (node.isFocused()) {
847                         if (inputFocus != null) {
848                             Log.e(LOG_TAG, "Duplicate input focus: " + node
849                                     + " in window:" + windowId);
850                         } else {
851                             inputFocus = node;
852                         }
853                     }
854 
855                     // The node should be a child of its parent if we have the parent.
856                     AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
857                     if (nodeParent != null) {
858                         boolean childOfItsParent = false;
859                         final int childCount = nodeParent.getChildCount();
860                         for (int k = 0; k < childCount; k++) {
861                             AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
862                             if (child == node) {
863                                 childOfItsParent = true;
864                                 break;
865                             }
866                         }
867                         if (!childOfItsParent) {
868                             Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
869                                     + nodeParent + " and child: " + node);
870                         }
871                     }
872 
873                     // The node should be the parent of its child if we have the child.
874                     final int childCount = node.getChildCount();
875                     for (int k = 0; k < childCount; k++) {
876                         AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
877                         if (child != null) {
878                             AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
879                             if (parent != node) {
880                                 Log.e(LOG_TAG, "Invalid child-parent relation between child: "
881                                         + node + " and parent: " + nodeParent);
882                             }
883                         }
884                     }
885                 }
886             }
887         }
888     }
889 
890     /**
891      * Registers a listener to receive callbacks whenever nodes are added to cache.
892      *
893      * @param listener the listener to be registered.
894      */
registerOnNodeAddedListener(OnNodeAddedListener listener)895     public void registerOnNodeAddedListener(OnNodeAddedListener listener) {
896         synchronized (mLock) {
897             mOnNodeAddedListener = listener;
898         }
899     }
900 
901     /**
902      * Clears the current reference to an OnNodeAddedListener, if one exists.
903      */
clearOnNodeAddedListener()904     public void clearOnNodeAddedListener() {
905         synchronized (mLock) {
906             mOnNodeAddedListener = null;
907         }
908     }
909 
910     // Layer of indirection included to break dependency chain for testing
911     public static class AccessibilityNodeRefresher {
912         /** Refresh the given AccessibilityNodeInfo object. */
refreshNode(AccessibilityNodeInfo info, boolean bypassCache)913         public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
914             return info.refresh(null, bypassCache);
915         }
916 
917         /** Refresh the given AccessibilityWindowInfo object. */
refreshWindow(AccessibilityWindowInfo info)918         public boolean refreshWindow(AccessibilityWindowInfo info) {
919             return info.refresh();
920         }
921     }
922 
923     /**
924      * Listener interface that receives callbacks when nodes are added to cache.
925      */
926     public interface OnNodeAddedListener {
927         /** Called when a node is added to cache. */
onNodeAdded(AccessibilityNodeInfo node)928         void onNodeAdded(AccessibilityNodeInfo node);
929     }
930 }
931