blob: 14954bea735aec927978fb0268fce041981b86b6 [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
42 private static final boolean DEBUG = false;
Svetoslav Ganov79311c42012-01-17 20:24:26 -080043
Svetoslav Ganovc406be92012-05-11 16:12:32 -070044 private static final boolean CHECK_INTEGRITY = true;
45
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) {
70 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070071 // New window so we clear the cache.
72 mWindowId = event.getWindowId();
Svetoslav Ganov42138042012-03-20 11:51:39 -070073 clear();
74 } break;
Svetoslav Ganovc406be92012-05-11 16:12:32 -070075 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
76 case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
77 final int windowId = event.getWindowId();
78 if (mWindowId != windowId) {
79 // New window so we clear the cache.
80 mWindowId = windowId;
81 clear();
82 }
83 } break;
Svetoslav Ganov42138042012-03-20 11:51:39 -070084 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
Svetoslav Ganovc406be92012-05-11 16:12:32 -070085 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
Svetoslav Ganov42138042012-03-20 11:51:39 -070086 case AccessibilityEvent.TYPE_VIEW_SELECTED:
87 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
88 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070089 // Since we prefetch the descendants of a node we
90 // just remove the entire subtree since when the node
91 // is fetched we will gets its descendant anyway.
92 synchronized (mLock) {
93 final long sourceId = event.getSourceNodeId();
94 clearSubTreeLocked(sourceId);
95 if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
96 clearSubtreeWithOldInputFocusLocked(sourceId);
97 }
98 if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
99 clearSubtreeWithOldAccessibilityFocusLocked(sourceId);
100 }
101 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700102 } break;
103 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
104 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700105 synchronized (mLock) {
106 final long accessibilityNodeId = event.getSourceNodeId();
107 clearSubTreeLocked(accessibilityNodeId);
108 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700109 } break;
110 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700111 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
112 checkIntegrity();
113 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800114 }
115 }
116
117 /**
118 * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
119 *
120 * @param accessibilityNodeId The info accessibility node id.
121 * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
122 */
123 public AccessibilityNodeInfo get(long accessibilityNodeId) {
124 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800125 synchronized(mLock) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800126 AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId);
127 if (info != null) {
128 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
129 // will wipe the data of the cached info.
130 info = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800131 }
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800132 if (DEBUG) {
133 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
134 }
135 return info;
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800136 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800137 } else {
138 return null;
139 }
140 }
141
142 /**
143 * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
144 *
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800145 * @param info The {@link AccessibilityNodeInfo} to cache.
146 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700147 public void add(AccessibilityNodeInfo info) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800148 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800149 synchronized(mLock) {
150 if (DEBUG) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700151 Log.i(LOG_TAG, "add(" + info + ")");
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800152 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700153
154 final long sourceId = info.getSourceNodeId();
155 AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId);
156 if (oldInfo != null) {
157 // If the added node is in the cache we have to be careful if
158 // the new one represents a source state where some of the
159 // children have been removed to avoid having disconnected
160 // subtrees in the cache.
161 SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds();
162 SparseLongArray newChildrenIds = info.getChildNodeIds();
163 final int oldChildCount = oldChildrenIds.size();
164 for (int i = 0; i < oldChildCount; i++) {
165 final long oldChildId = oldChildrenIds.valueAt(i);
166 if (newChildrenIds.indexOfValue(oldChildId) < 0) {
167 clearSubTreeLocked(oldChildId);
168 }
169 }
170
171 // Also be careful if the parent has changed since the new
172 // parent may be a predecessor of the old parent which will
173 // make the cached tree cyclic.
174 final long oldParentId = oldInfo.getParentNodeId();
175 if (info.getParentNodeId() != oldParentId) {
176 clearSubTreeLocked(oldParentId);
177 }
178 }
179
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800180 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
181 // will wipe the data of the cached info.
182 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700183 mCacheImpl.put(sourceId, clone);
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800184 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800185 }
186 }
187
188 /**
189 * Clears the cache.
190 */
191 public void clear() {
192 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800193 synchronized(mLock) {
194 if (DEBUG) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800195 Log.i(LOG_TAG, "clear()");
196 }
197 // Recycle the nodes before clearing the cache.
198 final int nodeCount = mCacheImpl.size();
199 for (int i = 0; i < nodeCount; i++) {
200 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
201 info.recycle();
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800202 }
203 mCacheImpl.clear();
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800204 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800205 }
206 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700207
208 /**
209 * Clears a subtree rooted at the node with the given id.
210 *
211 * @param rootNodeId The root id.
212 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700213 private void clearSubTreeLocked(long rootNodeId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700214 AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId);
215 if (current == null) {
216 return;
217 }
218 mCacheImpl.remove(rootNodeId);
219 SparseLongArray childNodeIds = current.getChildNodeIds();
220 final int childCount = childNodeIds.size();
221 for (int i = 0; i < childCount; i++) {
222 final long childNodeId = childNodeIds.valueAt(i);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700223 clearSubTreeLocked(childNodeId);
224 }
225 }
226
227 /**
228 * We are enforcing the invariant for a single input focus.
229 *
230 * @param currentInputFocusId The current input focused node.
231 */
232 private void clearSubtreeWithOldInputFocusLocked(long currentInputFocusId) {
233 final int cacheSize = mCacheImpl.size();
234 for (int i = 0; i < cacheSize; i++) {
235 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
236 final long infoSourceId = info.getSourceNodeId();
237 if (infoSourceId != currentInputFocusId && info.isFocused()) {
238 clearSubTreeLocked(infoSourceId);
239 return;
240 }
241 }
242 }
243
244 /**
245 * We are enforcing the invariant for a single accessibility focus.
246 *
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700247 * @param currentAccessibilityFocusId The current input focused node.
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700248 */
249 private void clearSubtreeWithOldAccessibilityFocusLocked(long currentAccessibilityFocusId) {
250 final int cacheSize = mCacheImpl.size();
251 for (int i = 0; i < cacheSize; i++) {
252 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
253 final long infoSourceId = info.getSourceNodeId();
254 if (infoSourceId != currentAccessibilityFocusId && info.isAccessibilityFocused()) {
255 clearSubTreeLocked(infoSourceId);
256 return;
257 }
258 }
259 }
260
261 /**
262 * Check the integrity of the cache which is it does not have nodes
263 * from more than one window, there are no duplicates, all nodes are
264 * connected, there is a single input focused node, and there is a
265 * single accessibility focused node.
266 */
267 private void checkIntegrity() {
268 synchronized (mLock) {
269 // Get the root.
270 if (mCacheImpl.size() <= 0) {
271 return;
272 }
273
274 // If the cache is a tree it does not matter from
275 // which node we start to search for the root.
276 AccessibilityNodeInfo root = mCacheImpl.valueAt(0);
277 AccessibilityNodeInfo parent = root;
278 while (parent != null) {
279 root = parent;
280 parent = mCacheImpl.get(parent.getParentNodeId());
281 }
282
283 // Traverse the tree and do some checks.
284 final int windowId = root.getWindowId();
285 AccessibilityNodeInfo accessFocus = null;
286 AccessibilityNodeInfo inputFocus = null;
287 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
288 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
289 fringe.add(root);
290
291 while (!fringe.isEmpty()) {
292 AccessibilityNodeInfo current = fringe.poll();
293 // Check for duplicates
294 if (!seen.add(current)) {
295 Log.e(LOG_TAG, "Duplicate node: " + current);
296 return;
297 }
298
299 // Check for one accessibility focus.
300 if (current.isAccessibilityFocused()) {
301 if (accessFocus != null) {
302 Log.e(LOG_TAG, "Duplicate accessibility focus:" + current);
303 } else {
304 accessFocus = current;
305 }
306 }
307
308 // Check for one input focus.
309 if (current.isFocused()) {
310 if (inputFocus != null) {
311 Log.e(LOG_TAG, "Duplicate input focus: " + current);
312 } else {
313 inputFocus = current;
314 }
315 }
316
317 SparseLongArray childIds = current.getChildNodeIds();
318 final int childCount = childIds.size();
319 for (int i = 0; i < childCount; i++) {
320 final long childId = childIds.valueAt(i);
321 AccessibilityNodeInfo child = mCacheImpl.get(childId);
322 if (child != null) {
323 fringe.add(child);
324 }
325 }
326 }
327
328 // Check for disconnected nodes or ones from another window.
329 final int cacheSize = mCacheImpl.size();
330 for (int i = 0; i < cacheSize; i++) {
331 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
332 if (!seen.contains(info)) {
333 if (info.getWindowId() == windowId) {
334 Log.e(LOG_TAG, "Disconneced node: ");
335 } else {
336 Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:"
337 + windowId + " " + info);
338 }
339 }
340 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700341 }
342 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800343}