blob: 1fde2fa3ce36f2621071147963813af74805d913 [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) {
Svetoslav6254f482013-06-04 17:22:14 -070070 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
Svetoslav Ganovc406be92012-05-11 16:12:32 -070071 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
72 case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
73 final int windowId = event.getWindowId();
Svetoslav6254f482013-06-04 17:22:14 -070074 // If a new window, we clear the cache.
Svetoslav Ganovc406be92012-05-11 16:12:32 -070075 if (mWindowId != windowId) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070076 mWindowId = windowId;
77 clear();
78 }
79 } break;
Svetoslav Ganov42138042012-03-20 11:51:39 -070080 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
Svetoslav Ganovc406be92012-05-11 16:12:32 -070081 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
Svetoslavdb7da0e2013-04-22 18:34:02 -070082 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
Svetoslav Ganov42138042012-03-20 11:51:39 -070083 case AccessibilityEvent.TYPE_VIEW_SELECTED:
84 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
85 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
Svetoslav6254f482013-06-04 17:22:14 -070086 refreshCachedNode(event.getSourceNodeId());
87 } break;
88 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
89 clearSubTreeLocked(event.getSourceNodeId());
90 } break;
91 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
Svetoslav Ganovc406be92012-05-11 16:12:32 -070092 synchronized (mLock) {
93 final long sourceId = event.getSourceNodeId();
Svetoslav6254f482013-06-04 17:22:14 -070094 if (event.getContentChangeType()
95 == AccessibilityEvent.CONTENT_CHANGE_TYPE_NODE) {
96 refreshCachedNode(sourceId);
97 } else {
98 clearSubTreeLocked(sourceId);
Svetoslav Ganovc406be92012-05-11 16:12:32 -070099 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700100 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700101 } break;
102 }
Svetoslav6254f482013-06-04 17:22:14 -0700103 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700104 checkIntegrity();
105 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800106 }
107 }
108
Svetoslav6254f482013-06-04 17:22:14 -0700109 private void refreshCachedNode(long sourceId) {
110 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700111 Log.i(LOG_TAG, "Refreshing cached node.");
Svetoslav6254f482013-06-04 17:22:14 -0700112 }
113 synchronized (mLock) {
114 AccessibilityNodeInfo cachedInfo = mCacheImpl.get(sourceId);
115 // If the source is not in the cache - nothing to do.
116 if (cachedInfo == null) {
117 return;
118 }
119 // The node changed so we will just refresh it right now.
Svetoslav8d820ecf2013-07-15 17:12:41 -0700120 if (cachedInfo.refresh(true)) {
Svetoslav6254f482013-06-04 17:22:14 -0700121 return;
122 }
123 // Weird, we could not refresh. Just evict the entire sub-tree.
124 clearSubTreeLocked(sourceId);
125 }
126 }
127
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800128 /**
129 * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
130 *
131 * @param accessibilityNodeId The info accessibility node id.
132 * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
133 */
134 public AccessibilityNodeInfo get(long accessibilityNodeId) {
135 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800136 synchronized(mLock) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800137 AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId);
138 if (info != null) {
139 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
140 // will wipe the data of the cached info.
141 info = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800142 }
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800143 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700144 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800145 }
146 return info;
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800147 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800148 } else {
149 return null;
150 }
151 }
152
153 /**
154 * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
155 *
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800156 * @param info The {@link AccessibilityNodeInfo} to cache.
157 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700158 public void add(AccessibilityNodeInfo info) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800159 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800160 synchronized(mLock) {
161 if (DEBUG) {
Svetoslav8d820ecf2013-07-15 17:12:41 -0700162 Log.i(LOG_TAG, "add(" + info + ")");
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800163 }
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700164
165 final long sourceId = info.getSourceNodeId();
166 AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId);
167 if (oldInfo != null) {
168 // If the added node is in the cache we have to be careful if
169 // the new one represents a source state where some of the
170 // children have been removed to avoid having disconnected
171 // subtrees in the cache.
172 SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds();
173 SparseLongArray newChildrenIds = info.getChildNodeIds();
174 final int oldChildCount = oldChildrenIds.size();
175 for (int i = 0; i < oldChildCount; i++) {
176 final long oldChildId = oldChildrenIds.valueAt(i);
177 if (newChildrenIds.indexOfValue(oldChildId) < 0) {
178 clearSubTreeLocked(oldChildId);
179 }
180 }
181
182 // Also be careful if the parent has changed since the new
183 // parent may be a predecessor of the old parent which will
184 // make the cached tree cyclic.
185 final long oldParentId = oldInfo.getParentNodeId();
186 if (info.getParentNodeId() != oldParentId) {
187 clearSubTreeLocked(oldParentId);
188 }
189 }
190
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800191 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
192 // will wipe the data of the cached info.
193 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700194 mCacheImpl.put(sourceId, clone);
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800195 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800196 }
197 }
198
199 /**
200 * Clears the cache.
201 */
202 public void clear() {
203 if (ENABLED) {
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800204 synchronized(mLock) {
205 if (DEBUG) {
Svetoslav Ganovafd5fab2012-02-24 10:15:29 -0800206 Log.i(LOG_TAG, "clear()");
207 }
208 // Recycle the nodes before clearing the cache.
209 final int nodeCount = mCacheImpl.size();
210 for (int i = 0; i < nodeCount; i++) {
211 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
212 info.recycle();
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800213 }
214 mCacheImpl.clear();
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800215 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800216 }
217 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700218
219 /**
220 * Clears a subtree rooted at the node with the given id.
221 *
222 * @param rootNodeId The root id.
223 */
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700224 private void clearSubTreeLocked(long rootNodeId) {
Svetoslav6254f482013-06-04 17:22:14 -0700225 if (DEBUG) {
226 Log.i(LOG_TAG, "Clearing cached subtree.");
227 }
228 clearSubTreeRecursiveLocked(rootNodeId);
229 }
230
231 private void clearSubTreeRecursiveLocked(long rootNodeId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700232 AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId);
233 if (current == null) {
234 return;
235 }
236 mCacheImpl.remove(rootNodeId);
237 SparseLongArray childNodeIds = current.getChildNodeIds();
238 final int childCount = childNodeIds.size();
239 for (int i = 0; i < childCount; i++) {
240 final long childNodeId = childNodeIds.valueAt(i);
Svetoslav6254f482013-06-04 17:22:14 -0700241 clearSubTreeRecursiveLocked(childNodeId);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700242 }
243 }
244
245 /**
246 * Check the integrity of the cache which is it does not have nodes
247 * from more than one window, there are no duplicates, all nodes are
248 * connected, there is a single input focused node, and there is a
249 * single accessibility focused node.
250 */
251 private void checkIntegrity() {
252 synchronized (mLock) {
253 // Get the root.
254 if (mCacheImpl.size() <= 0) {
255 return;
256 }
257
258 // If the cache is a tree it does not matter from
259 // which node we start to search for the root.
260 AccessibilityNodeInfo root = mCacheImpl.valueAt(0);
261 AccessibilityNodeInfo parent = root;
262 while (parent != null) {
263 root = parent;
264 parent = mCacheImpl.get(parent.getParentNodeId());
265 }
266
267 // Traverse the tree and do some checks.
268 final int windowId = root.getWindowId();
269 AccessibilityNodeInfo accessFocus = null;
270 AccessibilityNodeInfo inputFocus = null;
271 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
272 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
273 fringe.add(root);
274
275 while (!fringe.isEmpty()) {
276 AccessibilityNodeInfo current = fringe.poll();
277 // Check for duplicates
278 if (!seen.add(current)) {
279 Log.e(LOG_TAG, "Duplicate node: " + current);
280 return;
281 }
282
283 // Check for one accessibility focus.
284 if (current.isAccessibilityFocused()) {
285 if (accessFocus != null) {
286 Log.e(LOG_TAG, "Duplicate accessibility focus:" + current);
287 } else {
288 accessFocus = current;
289 }
290 }
291
292 // Check for one input focus.
293 if (current.isFocused()) {
294 if (inputFocus != null) {
295 Log.e(LOG_TAG, "Duplicate input focus: " + current);
296 } else {
297 inputFocus = current;
298 }
299 }
300
301 SparseLongArray childIds = current.getChildNodeIds();
302 final int childCount = childIds.size();
303 for (int i = 0; i < childCount; i++) {
304 final long childId = childIds.valueAt(i);
305 AccessibilityNodeInfo child = mCacheImpl.get(childId);
306 if (child != null) {
307 fringe.add(child);
308 }
309 }
310 }
311
312 // Check for disconnected nodes or ones from another window.
Svetoslav6254f482013-06-04 17:22:14 -0700313 for (int i = 0; i < mCacheImpl.size(); i++) {
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700314 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
315 if (!seen.contains(info)) {
316 if (info.getWindowId() == windowId) {
Svetoslav6254f482013-06-04 17:22:14 -0700317 Log.e(LOG_TAG, "Disconneced node: " + info);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700318 } else {
319 Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:"
320 + windowId + " " + info);
321 }
322 }
323 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700324 }
325 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800326}