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 java.util.ArrayList;
27import java.util.List;
28
29/**
30 * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos.
31 * It is updated when windows change or nodes change.
32 */
33final class AccessibilityCache {
34
35    private static final String LOG_TAG = "AccessibilityCache";
36
37    private static final boolean DEBUG = false;
38
39    private static final boolean CHECK_INTEGRITY = "eng".equals(Build.TYPE);
40
41    private final Object mLock = new Object();
42
43    private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
44    private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
45
46    private final SparseArray<AccessibilityWindowInfo> mWindowCache =
47            new SparseArray<>();
48
49    private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
50            new SparseArray<>();
51
52    private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
53            new SparseArray<>();
54
55    public void addWindow(AccessibilityWindowInfo window) {
56        synchronized (mLock) {
57            if (DEBUG) {
58                Log.i(LOG_TAG, "Caching window: " + window.getId());
59            }
60            final int windowId = window.getId();
61            AccessibilityWindowInfo oldWindow = mWindowCache.get(windowId);
62            if (oldWindow != null) {
63                oldWindow.recycle();
64            }
65            mWindowCache.put(windowId, AccessibilityWindowInfo.obtain(window));
66        }
67    }
68
69    /**
70     * Notifies the cache that the something in the UI changed. As a result
71     * the cache will either refresh some nodes or evict some nodes.
72     *
73     * @param event An event.
74     */
75    public void onAccessibilityEvent(AccessibilityEvent event) {
76        synchronized (mLock) {
77            final int eventType = event.getEventType();
78            switch (eventType) {
79                case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
80                    if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
81                        refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
82                    }
83                    mAccessibilityFocus = event.getSourceNodeId();
84                    refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
85                } break;
86
87                case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
88                    if (mAccessibilityFocus == event.getSourceNodeId()) {
89                        refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
90                        mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
91                    }
92                } break;
93
94                case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
95                    if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
96                        refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
97                    }
98                    mInputFocus = event.getSourceNodeId();
99                    refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
100                } break;
101
102                case AccessibilityEvent.TYPE_VIEW_SELECTED:
103                case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
104                case AccessibilityEvent.TYPE_VIEW_CLICKED:
105                case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
106                    refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId());
107                } break;
108
109                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
110                    synchronized (mLock) {
111                        final int windowId = event.getWindowId();
112                        final long sourceId = event.getSourceNodeId();
113                        if ((event.getContentChangeTypes()
114                                & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
115                            clearSubTreeLocked(windowId, sourceId);
116                        } else {
117                            refreshCachedNodeLocked(windowId, sourceId);
118                        }
119                    }
120                } break;
121
122                case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
123                    clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
124                } break;
125
126                case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
127                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
128                    clear();
129                } break;
130            }
131        }
132
133        if (CHECK_INTEGRITY) {
134            checkIntegrity();
135        }
136    }
137
138    private void refreshCachedNodeLocked(int windowId, long sourceId) {
139        if (DEBUG) {
140            Log.i(LOG_TAG, "Refreshing cached node.");
141        }
142
143        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
144        if (nodes == null) {
145            return;
146        }
147        AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
148        // If the source is not in the cache - nothing to do.
149        if (cachedInfo == null) {
150            return;
151        }
152        // The node changed so we will just refresh it right now.
153        if (cachedInfo.refresh(true)) {
154            return;
155        }
156        // Weird, we could not refresh. Just evict the entire sub-tree.
157        clearSubTreeLocked(windowId, sourceId);
158    }
159
160    /**
161     * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
162     * window and the accessibility id of the node.
163     *
164     * @param windowId The id of the window hosting the node.
165     * @param accessibilityNodeId The info accessibility node id.
166     * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
167     */
168    public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
169        synchronized(mLock) {
170            LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
171            if (nodes == null) {
172                return null;
173            }
174            AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
175            if (info != null) {
176                // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
177                // will wipe the data of the cached info.
178                info = AccessibilityNodeInfo.obtain(info);
179            }
180            if (DEBUG) {
181                Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
182            }
183            return info;
184        }
185    }
186
187    public List<AccessibilityWindowInfo> getWindows() {
188        synchronized (mLock) {
189            final int windowCount = mWindowCache.size();
190            if (windowCount > 0) {
191                // Careful to return the windows in a decreasing layer order.
192                SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
193                sortedWindows.clear();
194
195                for (int i = 0; i < windowCount; i++) {
196                    AccessibilityWindowInfo window = mWindowCache.valueAt(i);
197                    sortedWindows.put(window.getLayer(), window);
198                }
199
200                List<AccessibilityWindowInfo> windows = new ArrayList<>(windowCount);
201                for (int i = windowCount - 1; i >= 0; i--) {
202                    AccessibilityWindowInfo window = sortedWindows.valueAt(i);
203                    windows.add(AccessibilityWindowInfo.obtain(window));
204                    sortedWindows.removeAt(i);
205                }
206
207                return windows;
208            }
209            return null;
210        }
211    }
212
213    public AccessibilityWindowInfo getWindow(int windowId) {
214        synchronized (mLock) {
215            AccessibilityWindowInfo window = mWindowCache.get(windowId);
216            if (window != null) {
217               return AccessibilityWindowInfo.obtain(window);
218            }
219            return null;
220        }
221    }
222
223    /**
224     * Caches an {@link AccessibilityNodeInfo}.
225     *
226     * @param info The node to cache.
227     */
228    public void add(AccessibilityNodeInfo info) {
229        synchronized(mLock) {
230            if (DEBUG) {
231                Log.i(LOG_TAG, "add(" + info + ")");
232            }
233
234            final int windowId = info.getWindowId();
235            LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
236            if (nodes == null) {
237                nodes = new LongSparseArray<>();
238                mNodeCache.put(windowId, nodes);
239            }
240
241            final long sourceId = info.getSourceNodeId();
242            AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
243            if (oldInfo != null) {
244                // If the added node is in the cache we have to be careful if
245                // the new one represents a source state where some of the
246                // children have been removed to remove the descendants that
247                // are no longer present.
248                final LongArray newChildrenIds = info.getChildNodeIds();
249
250                final int oldChildCount = oldInfo.getChildCount();
251                for (int i = 0; i < oldChildCount; i++) {
252                    final long oldChildId = oldInfo.getChildId(i);
253                    // If the child is no longer present, remove the sub-tree.
254                    if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
255                        clearSubTreeLocked(windowId, oldChildId);
256                    }
257                }
258
259                // Also be careful if the parent has changed since the new
260                // parent may be a predecessor of the old parent which will
261                // add cyclse to the cache.
262                final long oldParentId = oldInfo.getParentNodeId();
263                if (info.getParentNodeId() != oldParentId) {
264                    clearSubTreeLocked(windowId, oldParentId);
265                }
266           }
267
268            // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
269            // will wipe the data of the cached info.
270            AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
271            nodes.put(sourceId, clone);
272        }
273    }
274
275    /**
276     * Clears the cache.
277     */
278    public void clear() {
279        synchronized(mLock) {
280            if (DEBUG) {
281                Log.i(LOG_TAG, "clear()");
282            }
283            final int windowCount = mWindowCache.size();
284            for (int i = windowCount - 1; i >= 0; i--) {
285                AccessibilityWindowInfo window = mWindowCache.valueAt(i);
286                window.recycle();
287                mWindowCache.removeAt(i);
288            }
289            final int nodesForWindowCount = mNodeCache.size();
290            for (int i = 0; i < nodesForWindowCount; i++) {
291                final int windowId = mNodeCache.keyAt(i);
292                clearNodesForWindowLocked(windowId);
293            }
294
295            mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
296            mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
297        }
298    }
299
300    private void clearNodesForWindowLocked(int windowId) {
301        if (DEBUG) {
302            Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
303        }
304        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
305        if (nodes == null) {
306            return;
307        }
308        // Recycle the nodes before clearing the cache.
309        final int nodeCount = nodes.size();
310        for (int i = nodeCount - 1; i >= 0; i--) {
311            AccessibilityNodeInfo info = nodes.valueAt(i);
312            nodes.removeAt(i);
313            info.recycle();
314        }
315        mNodeCache.remove(windowId);
316    }
317
318    /**
319     * Clears a subtree rooted at the node with the given id that is
320     * hosted in a given window.
321     *
322     * @param windowId The id of the hosting window.
323     * @param rootNodeId The root id.
324     */
325    private void clearSubTreeLocked(int windowId, long rootNodeId) {
326        if (DEBUG) {
327            Log.i(LOG_TAG, "Clearing cached subtree.");
328        }
329        LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
330        if (nodes != null) {
331            clearSubTreeRecursiveLocked(nodes, rootNodeId);
332        }
333    }
334
335    /**
336     * Clears a subtree given a pointer to the root id and the nodes
337     * in the hosting window.
338     *
339     * @param nodes The nodes in the hosting window.
340     * @param rootNodeId The id of the root to evict.
341     */
342    private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
343            long rootNodeId) {
344        AccessibilityNodeInfo current = nodes.get(rootNodeId);
345        if (current == null) {
346            return;
347        }
348        nodes.remove(rootNodeId);
349        final int childCount = current.getChildCount();
350        for (int i = 0; i < childCount; i++) {
351            final long childNodeId = current.getChildId(i);
352            clearSubTreeRecursiveLocked(nodes, childNodeId);
353        }
354    }
355
356    /**
357     * Check the integrity of the cache which is nodes from different windows
358     * are not mixed, there is a single active window, there is a single focused
359     * window, for every window there are no duplicates nodes, all nodes for a
360     * window are connected, for every window there is a single input focused
361     * node, and for every window there is a single accessibility focused node.
362     */
363    public void checkIntegrity() {
364        synchronized (mLock) {
365            // Get the root.
366            if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) {
367                return;
368            }
369
370            AccessibilityWindowInfo focusedWindow = null;
371            AccessibilityWindowInfo activeWindow = null;
372
373            final int windowCount = mWindowCache.size();
374            for (int i = 0; i < windowCount; i++) {
375                AccessibilityWindowInfo window = mWindowCache.valueAt(i);
376
377                // Check for one active window.
378                if (window.isActive()) {
379                    if (activeWindow != null) {
380                        Log.e(LOG_TAG, "Duplicate active window:" + window);
381                    } else {
382                        activeWindow = window;
383                    }
384                }
385
386                // Check for one focused window.
387                if (window.isFocused()) {
388                    if (focusedWindow != null) {
389                        Log.e(LOG_TAG, "Duplicate focused window:" + window);
390                    } else {
391                        focusedWindow = window;
392                    }
393                }
394            }
395
396            // Traverse the tree and do some checks.
397            AccessibilityNodeInfo accessFocus = null;
398            AccessibilityNodeInfo inputFocus = null;
399
400            final int nodesForWindowCount = mNodeCache.size();
401            for (int i = 0; i < nodesForWindowCount; i++) {
402                LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
403                if (nodes.size() <= 0) {
404                    continue;
405                }
406
407                ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
408                final int windowId = mNodeCache.keyAt(i);
409
410                final int nodeCount = nodes.size();
411                for (int j = 0; j < nodeCount; j++) {
412                    AccessibilityNodeInfo node = nodes.valueAt(j);
413
414                    // Check for duplicates
415                    if (!seen.add(node)) {
416                        Log.e(LOG_TAG, "Duplicate node: " + node
417                                + " in window:" + windowId);
418                        // Stop now as we potentially found a loop.
419                        continue;
420                    }
421
422                    // Check for one accessibility focus.
423                    if (node.isAccessibilityFocused()) {
424                        if (accessFocus != null) {
425                            Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
426                                    + " in window:" + windowId);
427                        } else {
428                            accessFocus = node;
429                        }
430                    }
431
432                    // Check for one input focus.
433                    if (node.isFocused()) {
434                        if (inputFocus != null) {
435                            Log.e(LOG_TAG, "Duplicate input focus: " + node
436                                    + " in window:" + windowId);
437                        } else {
438                            inputFocus = node;
439                        }
440                    }
441
442                    // The node should be a child of its parent if we have the parent.
443                    AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
444                    if (nodeParent != null) {
445                        boolean childOfItsParent = false;
446                        final int childCount = nodeParent.getChildCount();
447                        for (int k = 0; k < childCount; k++) {
448                            AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
449                            if (child == node) {
450                                childOfItsParent = true;
451                                break;
452                            }
453                        }
454                        if (!childOfItsParent) {
455                            Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
456                                    + nodeParent + " and child: " + node);
457                        }
458                    }
459
460                    // The node should be the parent of its child if we have the child.
461                    final int childCount = node.getChildCount();
462                    for (int k = 0; k < childCount; k++) {
463                        AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
464                        if (child != null) {
465                            AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
466                            if (parent != node) {
467                                Log.e(LOG_TAG, "Invalid child-parent relation between child: "
468                                        + node + " and parent: " + nodeParent);
469                            }
470                        }
471                    }
472                }
473            }
474        }
475    }
476}
477