blob: a9473a8df63cb8a992150914c601cf53f975e70d [file] [log] [blame]
Svetoslav Ganov79311c42012-01-17 20:24:26 -08001/*
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
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -080017package android.view.accessibility;
Svetoslav Ganov79311c42012-01-17 20:24:26 -080018
Svetoslav Ganovc406be92012-05-11 16:12:32 -070019import android.os.Build;
Svetoslav Ganov0d04e242012-02-21 13:46:36 -080020import android.util.Log;
Svetoslav Ganov79311c42012-01-17 20:24:26 -080021import android.util.LongSparseArray;
Svetoslav Ganov42138042012-03-20 11:51:39 -070022import android.util.SparseLongArray;
Svetoslav Ganov79311c42012-01-17 20:24:26 -080023
Svetoslav Ganovc406be92012-05-11 16:12:32 -070024import java.util.HashSet;
25import java.util.LinkedList;
26import java.util.Queue;
27
Svetoslav Ganov79311c42012-01-17 20:24:26 -080028/**
29 * Simple cache for AccessibilityNodeInfos. The cache is mapping an
30 * accessibility id to an info. The cache allows storing of
31 * <code>null</code> values. It also tracks accessibility events
32 * and invalidates accordingly.
33 *
34 * @hide
35 */
36public class AccessibilityNodeInfoCache {
37
Svetoslav Ganov0d04e242012-02-21 13:46:36 -080038 private static final String LOG_TAG = AccessibilityNodeInfoCache.class.getSimpleName();
39
40 private static final boolean ENABLED = true;
41
Svetoslav8d820ecf2013-07-15 17:12:41 -070042 private static final boolean DEBUG = false;
Svetoslav Ganov79311c42012-01-17 20:24:26 -080043
Svetoslav6254f482013-06-04 17:22:14 -070044 private static final boolean CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD = true;
Svetoslav Ganovc406be92012-05-11 16:12:32 -070045
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -080046 private final Object mLock = new Object();
Svetoslav Ganov79311c42012-01-17 20:24:26 -080047
48 private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl;
49
Svetoslav Ganovc406be92012-05-11 16:12:32 -070050 private int mWindowId;
51
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -080052 public AccessibilityNodeInfoCache() {
Svetoslav Ganov79311c42012-01-17 20:24:26 -080053 if (ENABLED) {
54 mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>();
55 } else {
56 mCacheImpl = null;
57 }
58 }
59
60 /**
61 * The cache keeps track of {@link AccessibilityEvent}s and invalidates
62 * cached nodes as appropriate.
63 *
64 * @param event An event.
65 */
66 public void onAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganov42138042012-03-20 11:51:39 -070067 if (ENABLED) {
68 final int eventType = event.getEventType();
69 switch (eventType) {
Alan Viverette3d1c5a722013-10-09 17:10:21 -070070 case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
Svetoslav6254f482013-06-04 17:22:14 -070071 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
Svetoslav Ganovc406be92012-05-11 16:12:32 -070072 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
73 case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
Alan Viverette3d1c5a722013-10-09 17:10:21 -070074 // If the active window changes, clear the cache.
Svetoslav Ganovc406be92012-05-11 16:12:32 -070075 final int windowId = event.getWindowId();
76 if (mWindowId != windowId) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070077 mWindowId = windowId;
78 clear();
79 }
80 } break;
Svetoslav Ganov42138042012-03-20 11:51:39 -070081 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
Svetoslav Ganovc406be92012-05-11 16:12:32 -070082 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
Svetoslavdb7da0e2013-04-22 18:34:02 -070083 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
Svetoslav Ganov42138042012-03-20 11:51:39 -070084 case AccessibilityEvent.TYPE_VIEW_SELECTED:
85 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
86 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
Svetoslav6254f482013-06-04 17:22:14 -070087 refreshCachedNode(event.getSourceNodeId());
88 } break;
89 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
Svetoslav3edcd8c2013-10-08 18:31:54 -070090 synchronized (mLock) {
91 clearSubTreeLocked(event.getSourceNodeId());
92 }
Svetoslav6254f482013-06-04 17:22:14 -070093 } break;
94 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070095 synchronized (mLock) {
96 final long sourceId = event.getSourceNodeId();
Alan Viverette77e9a282013-09-12 17:16:09 -070097 if ((event.getContentChangeTypes()
98 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
Svetoslav6254f482013-06-04 17:22:14 -070099 clearSubTreeLocked(sourceId);
Alan Viverette77e9a282013-09-12 17:16:09 -0700100 } else {
101 refreshCachedNode(sourceId);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700102 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700103 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700104 } break;
105 }
Svetoslav651dd4e2013-09-12 14:37:47 -0700106 if (CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD && Build.IS_DEBUGGABLE) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700107 checkIntegrity();
108 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800109 }
110 }
111
Svetoslav6254f482013-06-04 17:22:14 -0700112 private void refreshCachedNode(long sourceId) {
113 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700114 Log.i(LOG_TAG, "Refreshing cached node.");
Svetoslav6254f482013-06-04 17:22:14 -0700115 }
116 synchronized (mLock) {
117 AccessibilityNodeInfo cachedInfo = mCacheImpl.get(sourceId);
118 // If the source is not in the cache - nothing to do.
119 if (cachedInfo == null) {
120 return;
121 }
122 // The node changed so we will just refresh it right now.
Svetoslav8d820ecf2013-07-15 17:12:41 -0700123 if (cachedInfo.refresh(true)) {
Svetoslav6254f482013-06-04 17:22:14 -0700124 return;
125 }
126 // Weird, we could not refresh. Just evict the entire sub-tree.
127 clearSubTreeLocked(sourceId);
128 }
129 }
130
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800131 /**
132 * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
133 *
134 * @param accessibilityNodeId The info accessibility node id.
135 * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
136 */
137 public AccessibilityNodeInfo get(long accessibilityNodeId) {
138 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800139 synchronized(mLock) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800140 AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId);
141 if (info != null) {
142 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
143 // will wipe the data of the cached info.
144 info = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800145 }
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800146 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700147 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800148 }
149 return info;
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800150 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800151 } else {
152 return null;
153 }
154 }
155
156 /**
157 * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
158 *
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800159 * @param info The {@link AccessibilityNodeInfo} to cache.
160 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700161 public void add(AccessibilityNodeInfo info) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800162 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800163 synchronized(mLock) {
164 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700165 Log.i(LOG_TAG, "add(" + info + ")");
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800166 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700167
168 final long sourceId = info.getSourceNodeId();
169 AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId);
170 if (oldInfo != null) {
171 // If the added node is in the cache we have to be careful if
172 // the new one represents a source state where some of the
173 // children have been removed to avoid having disconnected
174 // subtrees in the cache.
175 SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds();
176 SparseLongArray newChildrenIds = info.getChildNodeIds();
177 final int oldChildCount = oldChildrenIds.size();
178 for (int i = 0; i < oldChildCount; i++) {
179 final long oldChildId = oldChildrenIds.valueAt(i);
180 if (newChildrenIds.indexOfValue(oldChildId) < 0) {
181 clearSubTreeLocked(oldChildId);
182 }
183 }
184
185 // Also be careful if the parent has changed since the new
186 // parent may be a predecessor of the old parent which will
187 // make the cached tree cyclic.
188 final long oldParentId = oldInfo.getParentNodeId();
189 if (info.getParentNodeId() != oldParentId) {
190 clearSubTreeLocked(oldParentId);
191 }
192 }
193
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800194 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
195 // will wipe the data of the cached info.
196 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700197 mCacheImpl.put(sourceId, clone);
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800198 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800199 }
200 }
201
202 /**
203 * Clears the cache.
204 */
205 public void clear() {
206 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800207 synchronized(mLock) {
208 if (DEBUG) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800209 Log.i(LOG_TAG, "clear()");
210 }
211 // Recycle the nodes before clearing the cache.
212 final int nodeCount = mCacheImpl.size();
213 for (int i = 0; i < nodeCount; i++) {
214 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
215 info.recycle();
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800216 }
217 mCacheImpl.clear();
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800218 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800219 }
220 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700221
222 /**
223 * Clears a subtree rooted at the node with the given id.
224 *
225 * @param rootNodeId The root id.
226 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700227 private void clearSubTreeLocked(long rootNodeId) {
Svetoslav6254f482013-06-04 17:22:14 -0700228 if (DEBUG) {
229 Log.i(LOG_TAG, "Clearing cached subtree.");
230 }
231 clearSubTreeRecursiveLocked(rootNodeId);
232 }
233
234 private void clearSubTreeRecursiveLocked(long rootNodeId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700235 AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId);
236 if (current == null) {
237 return;
238 }
239 mCacheImpl.remove(rootNodeId);
240 SparseLongArray childNodeIds = current.getChildNodeIds();
241 final int childCount = childNodeIds.size();
242 for (int i = 0; i < childCount; i++) {
243 final long childNodeId = childNodeIds.valueAt(i);
Svetoslav6254f482013-06-04 17:22:14 -0700244 clearSubTreeRecursiveLocked(childNodeId);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700245 }
246 }
247
248 /**
249 * Check the integrity of the cache which is it does not have nodes
250 * from more than one window, there are no duplicates, all nodes are
251 * connected, there is a single input focused node, and there is a
252 * single accessibility focused node.
253 */
254 private void checkIntegrity() {
255 synchronized (mLock) {
256 // Get the root.
257 if (mCacheImpl.size() <= 0) {
258 return;
259 }
260
261 // If the cache is a tree it does not matter from
262 // which node we start to search for the root.
263 AccessibilityNodeInfo root = mCacheImpl.valueAt(0);
264 AccessibilityNodeInfo parent = root;
265 while (parent != null) {
266 root = parent;
267 parent = mCacheImpl.get(parent.getParentNodeId());
268 }
269
270 // Traverse the tree and do some checks.
271 final int windowId = root.getWindowId();
272 AccessibilityNodeInfo accessFocus = null;
273 AccessibilityNodeInfo inputFocus = null;
274 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
275 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
276 fringe.add(root);
277
278 while (!fringe.isEmpty()) {
279 AccessibilityNodeInfo current = fringe.poll();
280 // Check for duplicates
281 if (!seen.add(current)) {
282 Log.e(LOG_TAG, "Duplicate node: " + current);
283 return;
284 }
285
286 // Check for one accessibility focus.
287 if (current.isAccessibilityFocused()) {
288 if (accessFocus != null) {
289 Log.e(LOG_TAG, "Duplicate accessibility focus:" + current);
290 } else {
291 accessFocus = current;
292 }
293 }
294
295 // Check for one input focus.
296 if (current.isFocused()) {
297 if (inputFocus != null) {
298 Log.e(LOG_TAG, "Duplicate input focus: " + current);
299 } else {
300 inputFocus = current;
301 }
302 }
303
304 SparseLongArray childIds = current.getChildNodeIds();
305 final int childCount = childIds.size();
306 for (int i = 0; i < childCount; i++) {
307 final long childId = childIds.valueAt(i);
308 AccessibilityNodeInfo child = mCacheImpl.get(childId);
309 if (child != null) {
310 fringe.add(child);
311 }
312 }
313 }
314
315 // Check for disconnected nodes or ones from another window.
Svetoslav6254f482013-06-04 17:22:14 -0700316 for (int i = 0; i < mCacheImpl.size(); i++) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700317 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
318 if (!seen.contains(info)) {
319 if (info.getWindowId() == windowId) {
Svetoslav6254f482013-06-04 17:22:14 -0700320 Log.e(LOG_TAG, "Disconneced node: " + info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700321 } else {
322 Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:"
323 + windowId + " " + info);
324 }
325 }
326 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700327 }
328 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800329}