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 = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE;
39 
40     private static final boolean VERBOSE =
41             Log.isLoggable(LOG_TAG, Log.VERBOSE) && Build.IS_DEBUGGABLE;
42 
43     private static final boolean CHECK_INTEGRITY = Build.IS_ENG;
44 
45     /**
46      * {@link AccessibilityEvent} types that are critical for the cache to stay up to date
47      *
48      * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to
49      * make sure that the events are delivered to cache regardless of
50      * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes}
51      */
52     public static final int CACHE_CRITICAL_EVENTS_MASK =
53             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
54                     | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
55                     | AccessibilityEvent.TYPE_VIEW_FOCUSED
56                     | AccessibilityEvent.TYPE_VIEW_SELECTED
57                     | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
58                     | AccessibilityEvent.TYPE_VIEW_CLICKED
59                     | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
60                     | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
61                     | AccessibilityEvent.TYPE_VIEW_SCROLLED
62                     | AccessibilityEvent.TYPE_WINDOWS_CHANGED
63                     | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
64 
65     private final Object mLock = new Object();
66 
67     private final AccessibilityNodeRefresher mAccessibilityNodeRefresher;
68 
69     private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
70     private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
71 
72     private int mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
73 
74     private boolean mIsAllWindowsCached;
75 
76     // The SparseArray of all {@link AccessibilityWindowInfo}s on all displays.
77     // The key of outer SparseArray is display ID and the key of inner SparseArray is window ID.
78     private final SparseArray<SparseArray<AccessibilityWindowInfo>> mWindowCacheByDisplay =
79             new SparseArray<>();
80 
81     private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
82             new SparseArray<>();
83 
84     private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
85             new SparseArray<>();
86 
AccessibilityCache(AccessibilityNodeRefresher nodeRefresher)87     public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
88         mAccessibilityNodeRefresher = nodeRefresher;
89     }
90 
91     /**
92      * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache.
93      * The key of SparseArray is display ID.
94      *
95      * @param windowsOnAllDisplays The accessibility windows of all displays.
96      */
setWindowsOnAllDisplays( SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays)97     public void setWindowsOnAllDisplays(
98             SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays) {
99         synchronized (mLock) {
100             if (DEBUG) {
101                 Log.i(LOG_TAG, "Set windows");
102             }
103             clearWindowCacheLocked();
104             if (windowsOnAllDisplays == null) {
105                 return;
106             }
107 
108             final int displayCounts = windowsOnAllDisplays.size();
109             for (int i = 0; i < displayCounts; i++) {
110                 final List<AccessibilityWindowInfo> windowsOfDisplay =
111                         windowsOnAllDisplays.valueAt(i);
112 
113                 if (windowsOfDisplay == null) {
114                     continue;
115                 }
116 
117                 final int displayId = windowsOnAllDisplays.keyAt(i);
118                 final int windowCount = windowsOfDisplay.size();
119                 for (int j = 0; j < windowCount; j++) {
120                     addWindowByDisplayLocked(displayId, windowsOfDisplay.get(j));
121                 }
122             }
123             mIsAllWindowsCached = true;
124         }
125     }
126 
127     /**
128      * Sets an {@link AccessibilityWindowInfo} into the cache.
129      *
130      * @param window The accessibility window.
131      */
addWindow(AccessibilityWindowInfo window)132     public void addWindow(AccessibilityWindowInfo window) {
133         synchronized (mLock) {
134             if (DEBUG) {
135                 Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ "
136                         + window.getDisplayId() + " ]");
137             }
138             addWindowByDisplayLocked(window.getDisplayId(), window);
139         }
140     }
141 
addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window)142     private void addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window) {
143         SparseArray<AccessibilityWindowInfo> windows = mWindowCacheByDisplay.get(displayId);
144         if (windows == null) {
145             windows = new SparseArray<>();
146             mWindowCacheByDisplay.put(displayId, windows);
147         }
148         final int windowId = window.getId();
149         windows.put(windowId, new AccessibilityWindowInfo(window));
150     }
151     /**
152      * Notifies the cache that the something in the UI changed. As a result
153      * the cache will either refresh some nodes or evict some nodes.
154      *
155      * Note: any event that ends up affecting the cache should also be present in
156      * {@link #CACHE_CRITICAL_EVENTS_MASK}
157      *
158      * @param event An event.
159      */
onAccessibilityEvent(AccessibilityEvent event)160     public void onAccessibilityEvent(AccessibilityEvent event) {
161         AccessibilityNodeInfo nodeToRefresh = null;
162         synchronized (mLock) {
163             if (DEBUG) {
164                 Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")");
165             }
166             final int eventType = event.getEventType();
167             switch (eventType) {
168                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
169                     if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
170                         removeCachedNodeLocked(mAccessibilityFocusedWindow, mAccessibilityFocus);
171                     }
172                     mAccessibilityFocus = event.getSourceNodeId();
173                     mAccessibilityFocusedWindow = event.getWindowId();
174                     nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
175                             mAccessibilityFocus);
176                 } break;
177 
178                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
179                     if (mAccessibilityFocus == event.getSourceNodeId()
180                             && mAccessibilityFocusedWindow == event.getWindowId()) {
181                         nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
182                                 mAccessibilityFocus);
183                         mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
184                         mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
185                     }
186                 } break;
187 
188                 case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
189                     if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
190                         removeCachedNodeLocked(event.getWindowId(), mInputFocus);
191                     }
192                     mInputFocus = event.getSourceNodeId();
193                     nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), mInputFocus);
194                 } break;
195 
196                 case AccessibilityEvent.TYPE_VIEW_SELECTED:
197                 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
198                 case AccessibilityEvent.TYPE_VIEW_CLICKED:
199                 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
200                     nodeToRefresh = removeCachedNodeLocked(event.getWindowId(),
201                             event.getSourceNodeId());
202                 } break;
203 
204                 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
205                     synchronized (mLock) {
206                         final int windowId = event.getWindowId();
207                         final long sourceId = event.getSourceNodeId();
208                         if ((event.getContentChangeTypes()
209                                 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
210                             clearSubTreeLocked(windowId, sourceId);
211                         } else {
212                             nodeToRefresh = removeCachedNodeLocked(windowId, sourceId);
213                         }
214                     }
215                 } break;
216 
217                 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
218                     clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
219                 } break;
220 
221                 case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
222                     if (event.getWindowChanges()
223                             == AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED) {
224                         // Don't need to clear all cache. Unless the changes are related to
225                         // content, we won't clear all cache here with clear().
226                         clearWindowCacheLocked();
227                         break;
228                     }
229                 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
230                     clear();
231                 } break;
232             }
233         }
234 
235         if (nodeToRefresh != null) {
236             if (DEBUG) {
237                 Log.i(LOG_TAG, "Refreshing and re-adding cached node.");
238             }
239             if (mAccessibilityNodeRefresher.refreshNode(nodeToRefresh, true)) {
240                 add(nodeToRefresh);
241             }
242         }
243         if (CHECK_INTEGRITY) {
244             checkIntegrity();
245         }
246     }
247 
removeCachedNodeLocked(int windowId, long sourceId)248     private AccessibilityNodeInfo removeCachedNodeLocked(int windowId, long sourceId) {
249         if (DEBUG) {
250             Log.i(LOG_TAG, "Removing cached node.");
251         }
252         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
253         if (nodes == null) {
254             return null;
255         }
256         AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
257         // If the source is not in the cache - nothing to do.
258         if (cachedInfo == null) {
259             return null;
260         }
261         nodes.remove(sourceId);
262         return cachedInfo;
263     }
264 
265     /**
266      * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
267      * window and the accessibility id of the node.
268      *
269      * @param windowId The id of the window hosting the node.
270      * @param accessibilityNodeId The info accessibility node id.
271      * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
272      */
getNode(int windowId, long accessibilityNodeId)273     public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
274         synchronized(mLock) {
275             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
276             if (nodes == null) {
277                 return null;
278             }
279             AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
280             if (info != null) {
281                 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
282                 // will wipe the data of the cached info.
283                 info = new AccessibilityNodeInfo(info);
284             }
285             if (VERBOSE) {
286                 Log.i(LOG_TAG, "get(0x" + Long.toHexString(accessibilityNodeId) + ") = " + info);
287             }
288             return info;
289         }
290     }
291 
292     /**
293      * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache.
294      *
295      * @return All cached {@link AccessibilityWindowInfo}s of all displays
296      *         or null if such not found. The key of SparseArray is display ID.
297      */
getWindowsOnAllDisplays()298     public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
299         synchronized (mLock) {
300             if (!mIsAllWindowsCached) {
301                 return null;
302             }
303             final SparseArray<List<AccessibilityWindowInfo>> returnWindows = new SparseArray<>();
304             final int displayCounts = mWindowCacheByDisplay.size();
305 
306             if (displayCounts > 0) {
307                 for (int i = 0; i < displayCounts; i++) {
308                     final int displayId = mWindowCacheByDisplay.keyAt(i);
309                     final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
310                             mWindowCacheByDisplay.valueAt(i);
311 
312                     if (windowsOfDisplay == null) {
313                         continue;
314                     }
315 
316                     final int windowCount = windowsOfDisplay.size();
317                     if (windowCount > 0) {
318                         // Careful to return the windows in a decreasing layer order.
319                         SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
320                         sortedWindows.clear();
321 
322                         for (int j = 0; j < windowCount; j++) {
323                             AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
324                             sortedWindows.put(window.getLayer(), window);
325                         }
326 
327                         // It's possible in transient conditions for two windows to share the same
328                         // layer, which results in sortedWindows being smaller than
329                         // mWindowCacheByDisplay
330                         final int sortedWindowCount = sortedWindows.size();
331                         List<AccessibilityWindowInfo> windows =
332                                 new ArrayList<>(sortedWindowCount);
333                         for (int j = sortedWindowCount - 1; j >= 0; j--) {
334                             AccessibilityWindowInfo window = sortedWindows.valueAt(j);
335                             windows.add(new AccessibilityWindowInfo(window));
336                             sortedWindows.removeAt(j);
337                         }
338                         returnWindows.put(displayId, windows);
339                     }
340                 }
341                 return returnWindows;
342             }
343             return null;
344         }
345     }
346 
347     /**
348      * Gets an {@link AccessibilityWindowInfo} by windowId.
349      *
350      * @param windowId The id of the window.
351      *
352      * @return The {@link AccessibilityWindowInfo} or null if such not found.
353      */
getWindow(int windowId)354     public AccessibilityWindowInfo getWindow(int windowId) {
355         synchronized (mLock) {
356             final int displayCounts = mWindowCacheByDisplay.size();
357             for (int i = 0; i < displayCounts; i++) {
358                 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
359                         mWindowCacheByDisplay.valueAt(i);
360                 if (windowsOfDisplay == null) {
361                     continue;
362                 }
363 
364                 AccessibilityWindowInfo window = windowsOfDisplay.get(windowId);
365                 if (window != null) {
366                     return new AccessibilityWindowInfo(window);
367                 }
368             }
369             return null;
370         }
371     }
372 
373     /**
374      * Caches an {@link AccessibilityNodeInfo}.
375      *
376      * @param info The node to cache.
377      */
add(AccessibilityNodeInfo info)378     public void add(AccessibilityNodeInfo info) {
379         synchronized(mLock) {
380             if (VERBOSE) {
381                 Log.i(LOG_TAG, "add(" + info + ")");
382             }
383 
384             final int windowId = info.getWindowId();
385             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
386             if (nodes == null) {
387                 nodes = new LongSparseArray<>();
388                 mNodeCache.put(windowId, nodes);
389             }
390 
391             final long sourceId = info.getSourceNodeId();
392             AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
393             if (oldInfo != null) {
394                 // If the added node is in the cache we have to be careful if
395                 // the new one represents a source state where some of the
396                 // children have been removed to remove the descendants that
397                 // are no longer present.
398                 final LongArray newChildrenIds = info.getChildNodeIds();
399 
400                 final int oldChildCount = oldInfo.getChildCount();
401                 for (int i = 0; i < oldChildCount; i++) {
402                     final long oldChildId = oldInfo.getChildId(i);
403                     // If the child is no longer present, remove the sub-tree.
404                     if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
405                         clearSubTreeLocked(windowId, oldChildId);
406                     }
407                     if (nodes.get(sourceId) == null) {
408                         // We've removed (and thus recycled) this node because it was its own
409                         // ancestor (the app gave us bad data), we can't continue using it.
410                         // Clear the cache for this window and give up on adding the node.
411                         clearNodesForWindowLocked(windowId);
412                         return;
413                     }
414                 }
415 
416                 // Also be careful if the parent has changed since the new
417                 // parent may be a predecessor of the old parent which will
418                 // add cycles to the cache.
419                 final long oldParentId = oldInfo.getParentNodeId();
420                 if (info.getParentNodeId() != oldParentId) {
421                     clearSubTreeLocked(windowId, oldParentId);
422                 }
423            }
424 
425             // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
426             // will wipe the data of the cached info.
427             AccessibilityNodeInfo clone = new AccessibilityNodeInfo(info);
428             nodes.put(sourceId, clone);
429             if (clone.isAccessibilityFocused()) {
430                 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID
431                         && mAccessibilityFocus != sourceId) {
432                     removeCachedNodeLocked(windowId, mAccessibilityFocus);
433                 }
434                 mAccessibilityFocus = sourceId;
435                 mAccessibilityFocusedWindow = windowId;
436             } else if (mAccessibilityFocus == sourceId) {
437                 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
438                 mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
439             }
440             if (clone.isFocused()) {
441                 mInputFocus = sourceId;
442             }
443         }
444     }
445 
446     /**
447      * Clears the cache.
448      */
clear()449     public void clear() {
450         synchronized(mLock) {
451             if (DEBUG) {
452                 Log.i(LOG_TAG, "clear()");
453             }
454             clearWindowCacheLocked();
455             final int nodesForWindowCount = mNodeCache.size();
456             for (int i = nodesForWindowCount - 1; i >= 0; i--) {
457                 final int windowId = mNodeCache.keyAt(i);
458                 clearNodesForWindowLocked(windowId);
459             }
460 
461             mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
462             mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
463 
464             mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
465         }
466     }
467 
clearWindowCacheLocked()468     private void clearWindowCacheLocked() {
469         if (DEBUG) {
470             Log.i(LOG_TAG, "clearWindowCacheLocked");
471         }
472         final int displayCounts = mWindowCacheByDisplay.size();
473 
474         if (displayCounts > 0) {
475             for (int i = displayCounts - 1; i >= 0; i--) {
476                 final int displayId = mWindowCacheByDisplay.keyAt(i);
477                 final SparseArray<AccessibilityWindowInfo> windows =
478                         mWindowCacheByDisplay.get(displayId);
479                 if (windows != null) {
480                     windows.clear();
481                 }
482                 mWindowCacheByDisplay.remove(displayId);
483             }
484         }
485         mIsAllWindowsCached = false;
486     }
487 
488     /**
489      * Clears nodes for the window with the given id
490      */
clearNodesForWindowLocked(int windowId)491     private void clearNodesForWindowLocked(int windowId) {
492         if (DEBUG) {
493             Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
494         }
495         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
496         if (nodes == null) {
497             return;
498         }
499         mNodeCache.remove(windowId);
500     }
501 
502     /**
503      * Clears a subtree rooted at the node with the given id that is
504      * hosted in a given window.
505      *
506      * @param windowId The id of the hosting window.
507      * @param rootNodeId The root id.
508      */
clearSubTreeLocked(int windowId, long rootNodeId)509     private void clearSubTreeLocked(int windowId, long rootNodeId) {
510         if (DEBUG) {
511             Log.i(LOG_TAG, "Clearing cached subtree.");
512         }
513         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
514         if (nodes != null) {
515             clearSubTreeRecursiveLocked(nodes, rootNodeId);
516         }
517     }
518 
519     /**
520      * Clears a subtree given a pointer to the root id and the nodes
521      * in the hosting window.
522      *
523      * @param nodes The nodes in the hosting window.
524      * @param rootNodeId The id of the root to evict.
525      *
526      * @return {@code true} if the cache was cleared
527      */
clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, long rootNodeId)528     private boolean clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
529             long rootNodeId) {
530         AccessibilityNodeInfo current = nodes.get(rootNodeId);
531         if (current == null) {
532             // The node isn't in the cache, but its descendents might be.
533             clear();
534             return true;
535         }
536         nodes.remove(rootNodeId);
537         final int childCount = current.getChildCount();
538         for (int i = 0; i < childCount; i++) {
539             final long childNodeId = current.getChildId(i);
540             if (clearSubTreeRecursiveLocked(nodes, childNodeId)) {
541                 return true;
542             }
543         }
544         return false;
545     }
546 
547     /**
548      * Check the integrity of the cache which is nodes from different windows
549      * are not mixed, there is a single active window, there is a single focused
550      * window, for every window there are no duplicates nodes, all nodes for a
551      * window are connected, for every window there is a single input focused
552      * node, and for every window there is a single accessibility focused node.
553      */
checkIntegrity()554     public void checkIntegrity() {
555         synchronized (mLock) {
556             // Get the root.
557             if (mWindowCacheByDisplay.size() <= 0 && mNodeCache.size() == 0) {
558                 return;
559             }
560 
561             AccessibilityWindowInfo focusedWindow = null;
562             AccessibilityWindowInfo activeWindow = null;
563 
564             final int displayCounts = mWindowCacheByDisplay.size();
565             for (int i = 0; i < displayCounts; i++) {
566                 final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
567                         mWindowCacheByDisplay.valueAt(i);
568 
569                 if (windowsOfDisplay == null) {
570                     continue;
571                 }
572 
573                 final int windowCount = windowsOfDisplay.size();
574                 for (int j = 0; j < windowCount; j++) {
575                     final AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
576 
577                     // Check for one active window.
578                     if (window.isActive()) {
579                         if (activeWindow != null) {
580                             Log.e(LOG_TAG, "Duplicate active window:" + window);
581                         } else {
582                             activeWindow = window;
583                         }
584                     }
585                     // Check for one focused window.
586                     if (window.isFocused()) {
587                         if (focusedWindow != null) {
588                             Log.e(LOG_TAG, "Duplicate focused window:" + window);
589                         } else {
590                             focusedWindow = window;
591                         }
592                     }
593                 }
594             }
595 
596             // Traverse the tree and do some checks.
597             AccessibilityNodeInfo accessFocus = null;
598             AccessibilityNodeInfo inputFocus = null;
599 
600             final int nodesForWindowCount = mNodeCache.size();
601             for (int i = 0; i < nodesForWindowCount; i++) {
602                 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
603                 if (nodes.size() <= 0) {
604                     continue;
605                 }
606 
607                 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
608                 final int windowId = mNodeCache.keyAt(i);
609 
610                 final int nodeCount = nodes.size();
611                 for (int j = 0; j < nodeCount; j++) {
612                     AccessibilityNodeInfo node = nodes.valueAt(j);
613 
614                     // Check for duplicates
615                     if (!seen.add(node)) {
616                         Log.e(LOG_TAG, "Duplicate node: " + node
617                                 + " in window:" + windowId);
618                         // Stop now as we potentially found a loop.
619                         continue;
620                     }
621 
622                     // Check for one accessibility focus.
623                     if (node.isAccessibilityFocused()) {
624                         if (accessFocus != null) {
625                             Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
626                                     + " in window:" + windowId);
627                         } else {
628                             accessFocus = node;
629                         }
630                     }
631 
632                     // Check for one input focus.
633                     if (node.isFocused()) {
634                         if (inputFocus != null) {
635                             Log.e(LOG_TAG, "Duplicate input focus: " + node
636                                     + " in window:" + windowId);
637                         } else {
638                             inputFocus = node;
639                         }
640                     }
641 
642                     // The node should be a child of its parent if we have the parent.
643                     AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
644                     if (nodeParent != null) {
645                         boolean childOfItsParent = false;
646                         final int childCount = nodeParent.getChildCount();
647                         for (int k = 0; k < childCount; k++) {
648                             AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
649                             if (child == node) {
650                                 childOfItsParent = true;
651                                 break;
652                             }
653                         }
654                         if (!childOfItsParent) {
655                             Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
656                                     + nodeParent + " and child: " + node);
657                         }
658                     }
659 
660                     // The node should be the parent of its child if we have the child.
661                     final int childCount = node.getChildCount();
662                     for (int k = 0; k < childCount; k++) {
663                         AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
664                         if (child != null) {
665                             AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
666                             if (parent != node) {
667                                 Log.e(LOG_TAG, "Invalid child-parent relation between child: "
668                                         + node + " and parent: " + nodeParent);
669                             }
670                         }
671                     }
672                 }
673             }
674         }
675     }
676 
677     // Layer of indirection included to break dependency chain for testing
678     public static class AccessibilityNodeRefresher {
679         /** Refresh the given AccessibilityNodeInfo object. */
refreshNode(AccessibilityNodeInfo info, boolean bypassCache)680         public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
681             return info.refresh(null, bypassCache);
682         }
683 
684         /** Refresh the given AccessibilityWindowInfo object. */
refreshWindow(AccessibilityWindowInfo info)685         public boolean refreshWindow(AccessibilityWindowInfo info) {
686             return info.refresh();
687         }
688     }
689 }
690