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
17package android.view.accessibility;
18
19import android.os.Build;
20import android.util.ArraySet;
21import android.util.Log;
22import android.util.LongArray;
23import android.util.LongSparseArray;
24import android.util.SparseArray;
25
26import com.android.internal.annotations.VisibleForTesting;
27
28import java.util.ArrayList;
29import java.util.List;
30
31/**
32 * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos.
33 * It is updated when windows change or nodes change.
34 * @hide
35 */
36@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
37public final class AccessibilityCache {
38
39    private static final String LOG_TAG = "AccessibilityCache";
40
41    private static final boolean DEBUG = false;
42
43    private static final boolean CHECK_INTEGRITY = "eng".equals(Build.TYPE);
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 boolean mIsAllWindowsCached;
73
74    private final SparseArray<AccessibilityWindowInfo> mWindowCache =
75            new SparseArray<>();
76
77    private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
78            new SparseArray<>();
79
80    private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
81            new SparseArray<>();
82
83    public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
84        mAccessibilityNodeRefresher = nodeRefresher;
85    }
86
87    public void setWindows(List<AccessibilityWindowInfo> windows) {
88        synchronized (mLock) {
89            if (DEBUG) {
90                Log.i(LOG_TAG, "Set windows");
91            }
92            clearWindowCache();
93            if (windows == null) {
94                return;
95            }
96            final int windowCount = windows.size();
97            for (int i = 0; i < windowCount; i++) {
98                final AccessibilityWindowInfo window = windows.get(i);
99                addWindow(window);
100            }
101            mIsAllWindowsCached = true;
102        }
103    }
104
105    public void addWindow(AccessibilityWindowInfo window) {
106        synchronized (mLock) {
107            if (DEBUG) {
108                Log.i(LOG_TAG, "Caching window: " + window.getId());
109            }
110            final int windowId = window.getId();
111            AccessibilityWindowInfo oldWindow = mWindowCache.get(windowId);
112            if (oldWindow != null) {
113                oldWindow.recycle();
114            }
115            mWindowCache.put(windowId, AccessibilityWindowInfo.obtain(window));
116        }
117    }
118
119    /**
120     * Notifies the cache that the something in the UI changed. As a result
121     * the cache will either refresh some nodes or evict some nodes.
122     *
123     * Note: any event that ends up affecting the cache should also be present in
124     * {@link #CACHE_CRITICAL_EVENTS_MASK}
125     *
126     * @param event An event.
127     */
128    public void onAccessibilityEvent(AccessibilityEvent event) {
129        synchronized (mLock) {
130            final int eventType = event.getEventType();
131            switch (eventType) {
132                case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
133                    if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
134                        refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
135                    }
136                    mAccessibilityFocus = event.getSourceNodeId();
137                    refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
138                } break;
139
140                case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
141                    if (mAccessibilityFocus == event.getSourceNodeId()) {
142                        refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
143                        mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
144                    }
145                } break;
146
147                case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
148                    if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
149                        refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
150                    }
151                    mInputFocus = event.getSourceNodeId();
152                    refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
153                } break;
154
155                case AccessibilityEvent.TYPE_VIEW_SELECTED:
156                case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
157                case AccessibilityEvent.TYPE_VIEW_CLICKED:
158                case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
159                    refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId());
160                } break;
161
162                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
163                    synchronized (mLock) {
164                        final int windowId = event.getWindowId();
165                        final long sourceId = event.getSourceNodeId();
166                        if ((event.getContentChangeTypes()
167                                & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
168                            clearSubTreeLocked(windowId, sourceId);
169                        } else {
170                            refreshCachedNodeLocked(windowId, sourceId);
171                        }
172                    }
173                } break;
174
175                case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
176                    clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
177                } break;
178
179                case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
180                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
181                    clear();
182                } break;
183            }
184        }
185
186        if (CHECK_INTEGRITY) {
187            checkIntegrity();
188        }
189    }
190
191    private void refreshCachedNodeLocked(int windowId, long sourceId) {
192        if (DEBUG) {
193            Log.i(LOG_TAG, "Refreshing cached node.");
194        }
195
196        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
197        if (nodes == null) {
198            return;
199        }
200        AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
201        // If the source is not in the cache - nothing to do.
202        if (cachedInfo == null) {
203            return;
204        }
205        // The node changed so we will just refresh it right now.
206        if (mAccessibilityNodeRefresher.refreshNode(cachedInfo, true)) {
207            return;
208        }
209        // Weird, we could not refresh. Just evict the entire sub-tree.
210        clearSubTreeLocked(windowId, sourceId);
211    }
212
213    /**
214     * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
215     * window and the accessibility id of the node.
216     *
217     * @param windowId The id of the window hosting the node.
218     * @param accessibilityNodeId The info accessibility node id.
219     * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
220     */
221    public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
222        synchronized(mLock) {
223            LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
224            if (nodes == null) {
225                return null;
226            }
227            AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
228            if (info != null) {
229                // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
230                // will wipe the data of the cached info.
231                info = AccessibilityNodeInfo.obtain(info);
232            }
233            if (DEBUG) {
234                Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
235            }
236            return info;
237        }
238    }
239
240    public List<AccessibilityWindowInfo> getWindows() {
241        synchronized (mLock) {
242            if (!mIsAllWindowsCached) {
243                return null;
244            }
245
246            final int windowCount = mWindowCache.size();
247            if (windowCount > 0) {
248                // Careful to return the windows in a decreasing layer order.
249                SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
250                sortedWindows.clear();
251
252                for (int i = 0; i < windowCount; i++) {
253                    AccessibilityWindowInfo window = mWindowCache.valueAt(i);
254                    sortedWindows.put(window.getLayer(), window);
255                }
256
257                // It's possible in transient conditions for two windows to share the same
258                // layer, which results in sortedWindows being smaller than mWindowCache
259                final int sortedWindowCount = sortedWindows.size();
260                List<AccessibilityWindowInfo> windows = new ArrayList<>(sortedWindowCount);
261                for (int i = sortedWindowCount - 1; i >= 0; i--) {
262                    AccessibilityWindowInfo window = sortedWindows.valueAt(i);
263                    windows.add(AccessibilityWindowInfo.obtain(window));
264                    sortedWindows.removeAt(i);
265                }
266
267                return windows;
268            }
269            return null;
270        }
271    }
272
273    public AccessibilityWindowInfo getWindow(int windowId) {
274        synchronized (mLock) {
275            AccessibilityWindowInfo window = mWindowCache.get(windowId);
276            if (window != null) {
277               return AccessibilityWindowInfo.obtain(window);
278            }
279            return null;
280        }
281    }
282
283    /**
284     * Caches an {@link AccessibilityNodeInfo}.
285     *
286     * @param info The node to cache.
287     */
288    public void add(AccessibilityNodeInfo info) {
289        synchronized(mLock) {
290            if (DEBUG) {
291                Log.i(LOG_TAG, "add(" + info + ")");
292            }
293
294            final int windowId = info.getWindowId();
295            LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
296            if (nodes == null) {
297                nodes = new LongSparseArray<>();
298                mNodeCache.put(windowId, nodes);
299            }
300
301            final long sourceId = info.getSourceNodeId();
302            AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
303            if (oldInfo != null) {
304                // If the added node is in the cache we have to be careful if
305                // the new one represents a source state where some of the
306                // children have been removed to remove the descendants that
307                // are no longer present.
308                final LongArray newChildrenIds = info.getChildNodeIds();
309
310                final int oldChildCount = oldInfo.getChildCount();
311                for (int i = 0; i < oldChildCount; i++) {
312                    if (nodes.get(sourceId) == null) {
313                        // We've removed (and thus recycled) this node because it was its own
314                        // ancestor (the app gave us bad data), we can't continue using it.
315                        // Clear the cache for this window and give up on adding the node.
316                        clearNodesForWindowLocked(windowId);
317                        return;
318                    }
319                    final long oldChildId = oldInfo.getChildId(i);
320                    // If the child is no longer present, remove the sub-tree.
321                    if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
322                        clearSubTreeLocked(windowId, oldChildId);
323                    }
324                }
325
326                // Also be careful if the parent has changed since the new
327                // parent may be a predecessor of the old parent which will
328                // add cycles to the cache.
329                final long oldParentId = oldInfo.getParentNodeId();
330                if (info.getParentNodeId() != oldParentId) {
331                    clearSubTreeLocked(windowId, oldParentId);
332                } else {
333                    oldInfo.recycle();
334                }
335           }
336
337            // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
338            // will wipe the data of the cached info.
339            AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
340            nodes.put(sourceId, clone);
341            if (clone.isAccessibilityFocused()) {
342                mAccessibilityFocus = sourceId;
343            }
344            if (clone.isFocused()) {
345                mInputFocus = sourceId;
346            }
347        }
348    }
349
350    /**
351     * Clears the cache.
352     */
353    public void clear() {
354        synchronized(mLock) {
355            if (DEBUG) {
356                Log.i(LOG_TAG, "clear()");
357            }
358            clearWindowCache();
359            final int nodesForWindowCount = mNodeCache.size();
360            for (int i = 0; i < nodesForWindowCount; i++) {
361                final int windowId = mNodeCache.keyAt(i);
362                clearNodesForWindowLocked(windowId);
363            }
364
365            mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
366            mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
367        }
368    }
369
370    private void clearWindowCache() {
371        final int windowCount = mWindowCache.size();
372        for (int i = windowCount - 1; i >= 0; i--) {
373            AccessibilityWindowInfo window = mWindowCache.valueAt(i);
374            window.recycle();
375            mWindowCache.removeAt(i);
376        }
377        mIsAllWindowsCached = false;
378    }
379
380    /**
381     * Clears nodes for the window with the given id
382     */
383    private void clearNodesForWindowLocked(int windowId) {
384        if (DEBUG) {
385            Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
386        }
387        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
388        if (nodes == null) {
389            return;
390        }
391        // Recycle the nodes before clearing the cache.
392        final int nodeCount = nodes.size();
393        for (int i = nodeCount - 1; i >= 0; i--) {
394            AccessibilityNodeInfo info = nodes.valueAt(i);
395            nodes.removeAt(i);
396            info.recycle();
397        }
398        mNodeCache.remove(windowId);
399    }
400
401    /**
402     * Clears a subtree rooted at the node with the given id that is
403     * hosted in a given window.
404     *
405     * @param windowId The id of the hosting window.
406     * @param rootNodeId The root id.
407     */
408    private void clearSubTreeLocked(int windowId, long rootNodeId) {
409        if (DEBUG) {
410            Log.i(LOG_TAG, "Clearing cached subtree.");
411        }
412        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
413        if (nodes != null) {
414            clearSubTreeRecursiveLocked(nodes, rootNodeId);
415        }
416    }
417
418    /**
419     * Clears a subtree given a pointer to the root id and the nodes
420     * in the hosting window.
421     *
422     * @param nodes The nodes in the hosting window.
423     * @param rootNodeId The id of the root to evict.
424     */
425    private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
426            long rootNodeId) {
427        AccessibilityNodeInfo current = nodes.get(rootNodeId);
428        if (current == null) {
429            return;
430        }
431        nodes.remove(rootNodeId);
432        final int childCount = current.getChildCount();
433        for (int i = 0; i < childCount; i++) {
434            final long childNodeId = current.getChildId(i);
435            clearSubTreeRecursiveLocked(nodes, childNodeId);
436        }
437        current.recycle();
438    }
439
440    /**
441     * Check the integrity of the cache which is nodes from different windows
442     * are not mixed, there is a single active window, there is a single focused
443     * window, for every window there are no duplicates nodes, all nodes for a
444     * window are connected, for every window there is a single input focused
445     * node, and for every window there is a single accessibility focused node.
446     */
447    public void checkIntegrity() {
448        synchronized (mLock) {
449            // Get the root.
450            if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) {
451                return;
452            }
453
454            AccessibilityWindowInfo focusedWindow = null;
455            AccessibilityWindowInfo activeWindow = null;
456
457            final int windowCount = mWindowCache.size();
458            for (int i = 0; i < windowCount; i++) {
459                AccessibilityWindowInfo window = mWindowCache.valueAt(i);
460
461                // Check for one active window.
462                if (window.isActive()) {
463                    if (activeWindow != null) {
464                        Log.e(LOG_TAG, "Duplicate active window:" + window);
465                    } else {
466                        activeWindow = window;
467                    }
468                }
469
470                // Check for one focused window.
471                if (window.isFocused()) {
472                    if (focusedWindow != null) {
473                        Log.e(LOG_TAG, "Duplicate focused window:" + window);
474                    } else {
475                        focusedWindow = window;
476                    }
477                }
478            }
479
480            // Traverse the tree and do some checks.
481            AccessibilityNodeInfo accessFocus = null;
482            AccessibilityNodeInfo inputFocus = null;
483
484            final int nodesForWindowCount = mNodeCache.size();
485            for (int i = 0; i < nodesForWindowCount; i++) {
486                LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
487                if (nodes.size() <= 0) {
488                    continue;
489                }
490
491                ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
492                final int windowId = mNodeCache.keyAt(i);
493
494                final int nodeCount = nodes.size();
495                for (int j = 0; j < nodeCount; j++) {
496                    AccessibilityNodeInfo node = nodes.valueAt(j);
497
498                    // Check for duplicates
499                    if (!seen.add(node)) {
500                        Log.e(LOG_TAG, "Duplicate node: " + node
501                                + " in window:" + windowId);
502                        // Stop now as we potentially found a loop.
503                        continue;
504                    }
505
506                    // Check for one accessibility focus.
507                    if (node.isAccessibilityFocused()) {
508                        if (accessFocus != null) {
509                            Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
510                                    + " in window:" + windowId);
511                        } else {
512                            accessFocus = node;
513                        }
514                    }
515
516                    // Check for one input focus.
517                    if (node.isFocused()) {
518                        if (inputFocus != null) {
519                            Log.e(LOG_TAG, "Duplicate input focus: " + node
520                                    + " in window:" + windowId);
521                        } else {
522                            inputFocus = node;
523                        }
524                    }
525
526                    // The node should be a child of its parent if we have the parent.
527                    AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
528                    if (nodeParent != null) {
529                        boolean childOfItsParent = false;
530                        final int childCount = nodeParent.getChildCount();
531                        for (int k = 0; k < childCount; k++) {
532                            AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
533                            if (child == node) {
534                                childOfItsParent = true;
535                                break;
536                            }
537                        }
538                        if (!childOfItsParent) {
539                            Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
540                                    + nodeParent + " and child: " + node);
541                        }
542                    }
543
544                    // The node should be the parent of its child if we have the child.
545                    final int childCount = node.getChildCount();
546                    for (int k = 0; k < childCount; k++) {
547                        AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
548                        if (child != null) {
549                            AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
550                            if (parent != node) {
551                                Log.e(LOG_TAG, "Invalid child-parent relation between child: "
552                                        + node + " and parent: " + nodeParent);
553                            }
554                        }
555                    }
556                }
557            }
558        }
559    }
560
561    // Layer of indirection included to break dependency chain for testing
562    public static class AccessibilityNodeRefresher {
563        public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
564            return info.refresh(null, bypassCache);
565        }
566    }
567}
568