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