blob: 28518aab8ab95786e29560e3f7cd18eb736ae204 [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:
Svetoslavdb7da0e2013-04-22 18:34:02 -070086 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
Svetoslav Ganov42138042012-03-20 11:51:39 -070087 case AccessibilityEvent.TYPE_VIEW_SELECTED:
88 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
89 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070090 // Since we prefetch the descendants of a node we
91 // just remove the entire subtree since when the node
92 // is fetched we will gets its descendant anyway.
93 synchronized (mLock) {
94 final long sourceId = event.getSourceNodeId();
95 clearSubTreeLocked(sourceId);
96 if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
97 clearSubtreeWithOldInputFocusLocked(sourceId);
98 }
99 if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
100 clearSubtreeWithOldAccessibilityFocusLocked(sourceId);
101 }
102 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700103 } break;
104 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
105 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700106 synchronized (mLock) {
107 final long accessibilityNodeId = event.getSourceNodeId();
108 clearSubTreeLocked(accessibilityNodeId);
109 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700110 } break;
111 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700112 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
113 checkIntegrity();
114 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800115 }
116 }
117
118 /**
119 * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
120 *
121 * @param accessibilityNodeId The info accessibility node id.
122 * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
123 */
124 public AccessibilityNodeInfo get(long accessibilityNodeId) {
125 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800126 synchronized(mLock) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800127 AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId);
128 if (info != null) {
129 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
130 // will wipe the data of the cached info.
131 info = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800132 }
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800133 if (DEBUG) {
134 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
135 }
136 return info;
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800137 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800138 } else {
139 return null;
140 }
141 }
142
143 /**
144 * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
145 *
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800146 * @param info The {@link AccessibilityNodeInfo} to cache.
147 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700148 public void add(AccessibilityNodeInfo info) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800149 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800150 synchronized(mLock) {
151 if (DEBUG) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700152 Log.i(LOG_TAG, "add(" + info + ")");
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800153 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700154
155 final long sourceId = info.getSourceNodeId();
156 AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId);
157 if (oldInfo != null) {
158 // If the added node is in the cache we have to be careful if
159 // the new one represents a source state where some of the
160 // children have been removed to avoid having disconnected
161 // subtrees in the cache.
162 SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds();
163 SparseLongArray newChildrenIds = info.getChildNodeIds();
164 final int oldChildCount = oldChildrenIds.size();
165 for (int i = 0; i < oldChildCount; i++) {
166 final long oldChildId = oldChildrenIds.valueAt(i);
167 if (newChildrenIds.indexOfValue(oldChildId) < 0) {
168 clearSubTreeLocked(oldChildId);
169 }
170 }
171
172 // Also be careful if the parent has changed since the new
173 // parent may be a predecessor of the old parent which will
174 // make the cached tree cyclic.
175 final long oldParentId = oldInfo.getParentNodeId();
176 if (info.getParentNodeId() != oldParentId) {
177 clearSubTreeLocked(oldParentId);
178 }
179 }
180
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800181 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
182 // will wipe the data of the cached info.
183 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700184 mCacheImpl.put(sourceId, clone);
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800185 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800186 }
187 }
188
189 /**
190 * Clears the cache.
191 */
192 public void clear() {
193 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800194 synchronized(mLock) {
195 if (DEBUG) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800196 Log.i(LOG_TAG, "clear()");
197 }
198 // Recycle the nodes before clearing the cache.
199 final int nodeCount = mCacheImpl.size();
200 for (int i = 0; i < nodeCount; i++) {
201 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
202 info.recycle();
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800203 }
204 mCacheImpl.clear();
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800205 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800206 }
207 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700208
209 /**
210 * Clears a subtree rooted at the node with the given id.
211 *
212 * @param rootNodeId The root id.
213 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700214 private void clearSubTreeLocked(long rootNodeId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700215 AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId);
216 if (current == null) {
217 return;
218 }
219 mCacheImpl.remove(rootNodeId);
220 SparseLongArray childNodeIds = current.getChildNodeIds();
221 final int childCount = childNodeIds.size();
222 for (int i = 0; i < childCount; i++) {
223 final long childNodeId = childNodeIds.valueAt(i);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700224 clearSubTreeLocked(childNodeId);
225 }
226 }
227
228 /**
229 * We are enforcing the invariant for a single input focus.
230 *
231 * @param currentInputFocusId The current input focused node.
232 */
233 private void clearSubtreeWithOldInputFocusLocked(long currentInputFocusId) {
234 final int cacheSize = mCacheImpl.size();
235 for (int i = 0; i < cacheSize; i++) {
236 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
237 final long infoSourceId = info.getSourceNodeId();
238 if (infoSourceId != currentInputFocusId && info.isFocused()) {
239 clearSubTreeLocked(infoSourceId);
240 return;
241 }
242 }
243 }
244
245 /**
246 * We are enforcing the invariant for a single accessibility focus.
247 *
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700248 * @param currentAccessibilityFocusId The current input focused node.
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700249 */
250 private void clearSubtreeWithOldAccessibilityFocusLocked(long currentAccessibilityFocusId) {
251 final int cacheSize = mCacheImpl.size();
252 for (int i = 0; i < cacheSize; i++) {
253 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
254 final long infoSourceId = info.getSourceNodeId();
255 if (infoSourceId != currentAccessibilityFocusId && info.isAccessibilityFocused()) {
256 clearSubTreeLocked(infoSourceId);
257 return;
258 }
259 }
260 }
261
262 /**
263 * Check the integrity of the cache which is it does not have nodes
264 * from more than one window, there are no duplicates, all nodes are
265 * connected, there is a single input focused node, and there is a
266 * single accessibility focused node.
267 */
268 private void checkIntegrity() {
269 synchronized (mLock) {
270 // Get the root.
271 if (mCacheImpl.size() <= 0) {
272 return;
273 }
274
275 // If the cache is a tree it does not matter from
276 // which node we start to search for the root.
277 AccessibilityNodeInfo root = mCacheImpl.valueAt(0);
278 AccessibilityNodeInfo parent = root;
279 while (parent != null) {
280 root = parent;
281 parent = mCacheImpl.get(parent.getParentNodeId());
282 }
283
284 // Traverse the tree and do some checks.
285 final int windowId = root.getWindowId();
286 AccessibilityNodeInfo accessFocus = null;
287 AccessibilityNodeInfo inputFocus = null;
288 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
289 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
290 fringe.add(root);
291
292 while (!fringe.isEmpty()) {
293 AccessibilityNodeInfo current = fringe.poll();
294 // Check for duplicates
295 if (!seen.add(current)) {
296 Log.e(LOG_TAG, "Duplicate node: " + current);
297 return;
298 }
299
300 // Check for one accessibility focus.
301 if (current.isAccessibilityFocused()) {
302 if (accessFocus != null) {
303 Log.e(LOG_TAG, "Duplicate accessibility focus:" + current);
304 } else {
305 accessFocus = current;
306 }
307 }
308
309 // Check for one input focus.
310 if (current.isFocused()) {
311 if (inputFocus != null) {
312 Log.e(LOG_TAG, "Duplicate input focus: " + current);
313 } else {
314 inputFocus = current;
315 }
316 }
317
318 SparseLongArray childIds = current.getChildNodeIds();
319 final int childCount = childIds.size();
320 for (int i = 0; i < childCount; i++) {
321 final long childId = childIds.valueAt(i);
322 AccessibilityNodeInfo child = mCacheImpl.get(childId);
323 if (child != null) {
324 fringe.add(child);
325 }
326 }
327 }
328
329 // Check for disconnected nodes or ones from another window.
330 final int cacheSize = mCacheImpl.size();
331 for (int i = 0; i < cacheSize; i++) {
332 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
333 if (!seen.contains(info)) {
334 if (info.getWindowId() == windowId) {
335 Log.e(LOG_TAG, "Disconneced node: ");
336 } else {
337 Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:"
338 + windowId + " " + info);
339 }
340 }
341 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700342 }
343 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800344}