blob: da5a1cd67922bb26f692b60b36b24f220467e5a6 [file] [log] [blame]
Svetoslav8e3feb12014-02-24 13:46:47 -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
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.
Phil Weaverb010b122016-08-17 17:47:48 -070032 * @hide
Svetoslav8e3feb12014-02-24 13:46:47 -080033 */
Phil Weaverc140fdc2017-11-09 15:24:17 -080034public class AccessibilityCache {
Svetoslav8e3feb12014-02-24 13:46:47 -080035
36 private static final String LOG_TAG = "AccessibilityCache";
37
38 private static final boolean DEBUG = false;
39
Jeff Sharkey5ab02432017-06-27 11:01:36 -060040 private static final boolean CHECK_INTEGRITY = Build.IS_ENG;
Svetoslav8e3feb12014-02-24 13:46:47 -080041
Eugene Suslaeb1375c2016-12-20 16:32:59 -080042 /**
43 * {@link AccessibilityEvent} types that are critical for the cache to stay up to date
44 *
45 * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to
46 * make sure that the events are delivered to cache regardless of
47 * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes}
48 */
49 public static final int CACHE_CRITICAL_EVENTS_MASK =
50 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
51 | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
52 | AccessibilityEvent.TYPE_VIEW_FOCUSED
53 | AccessibilityEvent.TYPE_VIEW_SELECTED
54 | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
55 | AccessibilityEvent.TYPE_VIEW_CLICKED
56 | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
57 | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
58 | AccessibilityEvent.TYPE_VIEW_SCROLLED
59 | AccessibilityEvent.TYPE_WINDOWS_CHANGED
60 | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
61
Svetoslav8e3feb12014-02-24 13:46:47 -080062 private final Object mLock = new Object();
63
Phil Weaverb010b122016-08-17 17:47:48 -070064 private final AccessibilityNodeRefresher mAccessibilityNodeRefresher;
65
Svetoslav27ad2e92015-02-12 17:54:15 -080066 private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
67 private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
68
Maxim Bogatovc3281202015-10-21 12:38:01 -070069 private boolean mIsAllWindowsCached;
70
Svetoslav8e3feb12014-02-24 13:46:47 -080071 private final SparseArray<AccessibilityWindowInfo> mWindowCache =
Svetoslavf7c42c52014-07-24 12:19:22 -070072 new SparseArray<>();
Svetoslav8e3feb12014-02-24 13:46:47 -080073
74 private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
Svetoslavf7c42c52014-07-24 12:19:22 -070075 new SparseArray<>();
Svetoslav8e3feb12014-02-24 13:46:47 -080076
77 private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
Svetoslavf7c42c52014-07-24 12:19:22 -070078 new SparseArray<>();
Svetoslav8e3feb12014-02-24 13:46:47 -080079
Phil Weaverb010b122016-08-17 17:47:48 -070080 public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
81 mAccessibilityNodeRefresher = nodeRefresher;
82 }
83
Maxim Bogatovc3281202015-10-21 12:38:01 -070084 public void setWindows(List<AccessibilityWindowInfo> windows) {
85 synchronized (mLock) {
86 if (DEBUG) {
87 Log.i(LOG_TAG, "Set windows");
88 }
89 clearWindowCache();
90 if (windows == null) {
91 return;
92 }
93 final int windowCount = windows.size();
94 for (int i = 0; i < windowCount; i++) {
95 final AccessibilityWindowInfo window = windows.get(i);
96 addWindow(window);
97 }
98 mIsAllWindowsCached = true;
99 }
100 }
101
Svetoslav8e3feb12014-02-24 13:46:47 -0800102 public void addWindow(AccessibilityWindowInfo window) {
103 synchronized (mLock) {
104 if (DEBUG) {
105 Log.i(LOG_TAG, "Caching window: " + window.getId());
106 }
Svetoslav04cab1b2014-08-25 18:35:57 -0700107 final int windowId = window.getId();
108 AccessibilityWindowInfo oldWindow = mWindowCache.get(windowId);
109 if (oldWindow != null) {
110 oldWindow.recycle();
111 }
112 mWindowCache.put(windowId, AccessibilityWindowInfo.obtain(window));
Svetoslav8e3feb12014-02-24 13:46:47 -0800113 }
114 }
115
Svetoslav8e3feb12014-02-24 13:46:47 -0800116 /**
117 * Notifies the cache that the something in the UI changed. As a result
118 * the cache will either refresh some nodes or evict some nodes.
119 *
Eugene Suslaeb1375c2016-12-20 16:32:59 -0800120 * Note: any event that ends up affecting the cache should also be present in
121 * {@link #CACHE_CRITICAL_EVENTS_MASK}
122 *
Svetoslav8e3feb12014-02-24 13:46:47 -0800123 * @param event An event.
124 */
125 public void onAccessibilityEvent(AccessibilityEvent event) {
126 synchronized (mLock) {
127 final int eventType = event.getEventType();
128 switch (eventType) {
Svetoslav27ad2e92015-02-12 17:54:15 -0800129 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
130 if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
131 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
132 }
133 mAccessibilityFocus = event.getSourceNodeId();
134 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
135 } break;
136
137 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
138 if (mAccessibilityFocus == event.getSourceNodeId()) {
139 refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
140 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
141 }
142 } break;
143
144 case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
145 if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
146 refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
147 }
148 mInputFocus = event.getSourceNodeId();
149 refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
150 } break;
151
Svetoslav8e3feb12014-02-24 13:46:47 -0800152 case AccessibilityEvent.TYPE_VIEW_SELECTED:
153 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
Svet Ganov03221002014-12-05 16:52:27 -0800154 case AccessibilityEvent.TYPE_VIEW_CLICKED:
Svetoslav8e3feb12014-02-24 13:46:47 -0800155 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
156 refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId());
157 } break;
158
159 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
160 synchronized (mLock) {
161 final int windowId = event.getWindowId();
162 final long sourceId = event.getSourceNodeId();
163 if ((event.getContentChangeTypes()
164 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
165 clearSubTreeLocked(windowId, sourceId);
166 } else {
167 refreshCachedNodeLocked(windowId, sourceId);
168 }
169 }
170 } break;
171
172 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
173 clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
174 } break;
Svetoslava4725ef2014-07-24 14:17:05 -0700175
Svetoslav13bd7712014-09-19 18:18:04 -0700176 case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
177 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
178 clear();
Svetoslava4725ef2014-07-24 14:17:05 -0700179 } break;
Svetoslav8e3feb12014-02-24 13:46:47 -0800180 }
181 }
182
183 if (CHECK_INTEGRITY) {
184 checkIntegrity();
185 }
186 }
187
188 private void refreshCachedNodeLocked(int windowId, long sourceId) {
189 if (DEBUG) {
190 Log.i(LOG_TAG, "Refreshing cached node.");
191 }
192
193 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
194 if (nodes == null) {
195 return;
196 }
197 AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
198 // If the source is not in the cache - nothing to do.
199 if (cachedInfo == null) {
200 return;
201 }
202 // The node changed so we will just refresh it right now.
Phil Weaverb010b122016-08-17 17:47:48 -0700203 if (mAccessibilityNodeRefresher.refreshNode(cachedInfo, true)) {
Svetoslav8e3feb12014-02-24 13:46:47 -0800204 return;
205 }
206 // Weird, we could not refresh. Just evict the entire sub-tree.
207 clearSubTreeLocked(windowId, sourceId);
208 }
209
210 /**
211 * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
212 * window and the accessibility id of the node.
213 *
214 * @param windowId The id of the window hosting the node.
215 * @param accessibilityNodeId The info accessibility node id.
216 * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
217 */
218 public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
219 synchronized(mLock) {
220 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
221 if (nodes == null) {
222 return null;
223 }
224 AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
225 if (info != null) {
226 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
227 // will wipe the data of the cached info.
228 info = AccessibilityNodeInfo.obtain(info);
229 }
230 if (DEBUG) {
231 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
232 }
233 return info;
234 }
235 }
236
237 public List<AccessibilityWindowInfo> getWindows() {
238 synchronized (mLock) {
Maxim Bogatovc3281202015-10-21 12:38:01 -0700239 if (!mIsAllWindowsCached) {
240 return null;
241 }
242
Svetoslav8e3feb12014-02-24 13:46:47 -0800243 final int windowCount = mWindowCache.size();
244 if (windowCount > 0) {
245 // Careful to return the windows in a decreasing layer order.
246 SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
247 sortedWindows.clear();
248
249 for (int i = 0; i < windowCount; i++) {
250 AccessibilityWindowInfo window = mWindowCache.valueAt(i);
251 sortedWindows.put(window.getLayer(), window);
252 }
253
Phil Weaveradaafb22016-05-19 10:32:52 -0700254 // It's possible in transient conditions for two windows to share the same
255 // layer, which results in sortedWindows being smaller than mWindowCache
256 final int sortedWindowCount = sortedWindows.size();
257 List<AccessibilityWindowInfo> windows = new ArrayList<>(sortedWindowCount);
258 for (int i = sortedWindowCount - 1; i >= 0; i--) {
Svetoslav8e3feb12014-02-24 13:46:47 -0800259 AccessibilityWindowInfo window = sortedWindows.valueAt(i);
260 windows.add(AccessibilityWindowInfo.obtain(window));
Svetoslav04cab1b2014-08-25 18:35:57 -0700261 sortedWindows.removeAt(i);
Svetoslav8e3feb12014-02-24 13:46:47 -0800262 }
263
Svetoslav8e3feb12014-02-24 13:46:47 -0800264 return windows;
265 }
266 return null;
267 }
268 }
269
270 public AccessibilityWindowInfo getWindow(int windowId) {
271 synchronized (mLock) {
272 AccessibilityWindowInfo window = mWindowCache.get(windowId);
273 if (window != null) {
274 return AccessibilityWindowInfo.obtain(window);
275 }
276 return null;
277 }
278 }
279
280 /**
281 * Caches an {@link AccessibilityNodeInfo}.
282 *
283 * @param info The node to cache.
284 */
285 public void add(AccessibilityNodeInfo info) {
286 synchronized(mLock) {
287 if (DEBUG) {
288 Log.i(LOG_TAG, "add(" + info + ")");
289 }
290
291 final int windowId = info.getWindowId();
292 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
293 if (nodes == null) {
Svetoslavf7c42c52014-07-24 12:19:22 -0700294 nodes = new LongSparseArray<>();
Svetoslav8e3feb12014-02-24 13:46:47 -0800295 mNodeCache.put(windowId, nodes);
296 }
297
298 final long sourceId = info.getSourceNodeId();
299 AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
300 if (oldInfo != null) {
301 // If the added node is in the cache we have to be careful if
302 // the new one represents a source state where some of the
303 // children have been removed to remove the descendants that
304 // are no longer present.
305 final LongArray newChildrenIds = info.getChildNodeIds();
Svetoslav8e3feb12014-02-24 13:46:47 -0800306
Svetoslavf7c42c52014-07-24 12:19:22 -0700307 final int oldChildCount = oldInfo.getChildCount();
308 for (int i = 0; i < oldChildCount; i++) {
Phil Weaver61a1fab2017-05-03 13:55:51 -0700309 if (nodes.get(sourceId) == null) {
310 // We've removed (and thus recycled) this node because it was its own
311 // ancestor (the app gave us bad data), we can't continue using it.
312 // Clear the cache for this window and give up on adding the node.
313 clearNodesForWindowLocked(windowId);
314 return;
315 }
Svetoslavf7c42c52014-07-24 12:19:22 -0700316 final long oldChildId = oldInfo.getChildId(i);
317 // If the child is no longer present, remove the sub-tree.
318 if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
319 clearSubTreeLocked(windowId, oldChildId);
Svetoslav8e3feb12014-02-24 13:46:47 -0800320 }
Svetoslav8e3feb12014-02-24 13:46:47 -0800321 }
322
323 // Also be careful if the parent has changed since the new
324 // parent may be a predecessor of the old parent which will
Phil Weaverb010b122016-08-17 17:47:48 -0700325 // add cycles to the cache.
Svetoslav8e3feb12014-02-24 13:46:47 -0800326 final long oldParentId = oldInfo.getParentNodeId();
327 if (info.getParentNodeId() != oldParentId) {
328 clearSubTreeLocked(windowId, oldParentId);
Phil Weaverc140fdc2017-11-09 15:24:17 -0800329 } else {
330 oldInfo.recycle();
Svetoslav8e3feb12014-02-24 13:46:47 -0800331 }
332 }
333
334 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
335 // will wipe the data of the cached info.
336 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
337 nodes.put(sourceId, clone);
Phil Weaverb010b122016-08-17 17:47:48 -0700338 if (clone.isAccessibilityFocused()) {
339 mAccessibilityFocus = sourceId;
340 }
341 if (clone.isFocused()) {
342 mInputFocus = sourceId;
343 }
Svetoslav8e3feb12014-02-24 13:46:47 -0800344 }
345 }
346
347 /**
348 * Clears the cache.
349 */
350 public void clear() {
351 synchronized(mLock) {
352 if (DEBUG) {
353 Log.i(LOG_TAG, "clear()");
354 }
Maxim Bogatovc3281202015-10-21 12:38:01 -0700355 clearWindowCache();
Svetoslav8e3feb12014-02-24 13:46:47 -0800356 final int nodesForWindowCount = mNodeCache.size();
357 for (int i = 0; i < nodesForWindowCount; i++) {
358 final int windowId = mNodeCache.keyAt(i);
359 clearNodesForWindowLocked(windowId);
360 }
Svetoslav27ad2e92015-02-12 17:54:15 -0800361
362 mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
363 mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
Svetoslav8e3feb12014-02-24 13:46:47 -0800364 }
365 }
366
Maxim Bogatovc3281202015-10-21 12:38:01 -0700367 private void clearWindowCache() {
368 final int windowCount = mWindowCache.size();
369 for (int i = windowCount - 1; i >= 0; i--) {
370 AccessibilityWindowInfo window = mWindowCache.valueAt(i);
371 window.recycle();
372 mWindowCache.removeAt(i);
373 }
374 mIsAllWindowsCached = false;
375 }
376
Eugene Susla74c6cba2016-12-28 14:42:54 -0800377 /**
378 * Clears nodes for the window with the given id
379 */
Svetoslav8e3feb12014-02-24 13:46:47 -0800380 private void clearNodesForWindowLocked(int windowId) {
381 if (DEBUG) {
Svetoslav13bd7712014-09-19 18:18:04 -0700382 Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
Svetoslav8e3feb12014-02-24 13:46:47 -0800383 }
384 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
385 if (nodes == null) {
386 return;
387 }
388 // Recycle the nodes before clearing the cache.
389 final int nodeCount = nodes.size();
390 for (int i = nodeCount - 1; i >= 0; i--) {
391 AccessibilityNodeInfo info = nodes.valueAt(i);
392 nodes.removeAt(i);
393 info.recycle();
394 }
395 mNodeCache.remove(windowId);
396 }
397
398 /**
399 * Clears a subtree rooted at the node with the given id that is
400 * hosted in a given window.
401 *
402 * @param windowId The id of the hosting window.
403 * @param rootNodeId The root id.
404 */
405 private void clearSubTreeLocked(int windowId, long rootNodeId) {
406 if (DEBUG) {
407 Log.i(LOG_TAG, "Clearing cached subtree.");
408 }
409 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
410 if (nodes != null) {
411 clearSubTreeRecursiveLocked(nodes, rootNodeId);
412 }
413 }
414
415 /**
416 * Clears a subtree given a pointer to the root id and the nodes
417 * in the hosting window.
418 *
419 * @param nodes The nodes in the hosting window.
420 * @param rootNodeId The id of the root to evict.
421 */
422 private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
423 long rootNodeId) {
424 AccessibilityNodeInfo current = nodes.get(rootNodeId);
425 if (current == null) {
426 return;
427 }
428 nodes.remove(rootNodeId);
429 final int childCount = current.getChildCount();
430 for (int i = 0; i < childCount; i++) {
431 final long childNodeId = current.getChildId(i);
432 clearSubTreeRecursiveLocked(nodes, childNodeId);
433 }
Phil Weaverb010b122016-08-17 17:47:48 -0700434 current.recycle();
Svetoslav8e3feb12014-02-24 13:46:47 -0800435 }
436
437 /**
438 * Check the integrity of the cache which is nodes from different windows
439 * are not mixed, there is a single active window, there is a single focused
440 * window, for every window there are no duplicates nodes, all nodes for a
441 * window are connected, for every window there is a single input focused
442 * node, and for every window there is a single accessibility focused node.
443 */
444 public void checkIntegrity() {
445 synchronized (mLock) {
446 // Get the root.
447 if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) {
448 return;
449 }
450
451 AccessibilityWindowInfo focusedWindow = null;
452 AccessibilityWindowInfo activeWindow = null;
453
454 final int windowCount = mWindowCache.size();
455 for (int i = 0; i < windowCount; i++) {
456 AccessibilityWindowInfo window = mWindowCache.valueAt(i);
457
458 // Check for one active window.
459 if (window.isActive()) {
460 if (activeWindow != null) {
461 Log.e(LOG_TAG, "Duplicate active window:" + window);
462 } else {
463 activeWindow = window;
464 }
465 }
466
467 // Check for one focused window.
468 if (window.isFocused()) {
469 if (focusedWindow != null) {
470 Log.e(LOG_TAG, "Duplicate focused window:" + window);
471 } else {
472 focusedWindow = window;
473 }
474 }
475 }
476
477 // Traverse the tree and do some checks.
478 AccessibilityNodeInfo accessFocus = null;
479 AccessibilityNodeInfo inputFocus = null;
480
481 final int nodesForWindowCount = mNodeCache.size();
482 for (int i = 0; i < nodesForWindowCount; i++) {
483 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
484 if (nodes.size() <= 0) {
485 continue;
486 }
487
Svetoslavf7c42c52014-07-24 12:19:22 -0700488 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
Svetoslav8e3feb12014-02-24 13:46:47 -0800489 final int windowId = mNodeCache.keyAt(i);
490
491 final int nodeCount = nodes.size();
492 for (int j = 0; j < nodeCount; j++) {
493 AccessibilityNodeInfo node = nodes.valueAt(j);
494
495 // Check for duplicates
496 if (!seen.add(node)) {
497 Log.e(LOG_TAG, "Duplicate node: " + node
498 + " in window:" + windowId);
Svetoslavf7c42c52014-07-24 12:19:22 -0700499 // Stop now as we potentially found a loop.
500 continue;
Svetoslav8e3feb12014-02-24 13:46:47 -0800501 }
502
503 // Check for one accessibility focus.
504 if (node.isAccessibilityFocused()) {
505 if (accessFocus != null) {
506 Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
507 + " in window:" + windowId);
508 } else {
509 accessFocus = node;
510 }
511 }
512
513 // Check for one input focus.
514 if (node.isFocused()) {
515 if (inputFocus != null) {
516 Log.e(LOG_TAG, "Duplicate input focus: " + node
517 + " in window:" + windowId);
518 } else {
519 inputFocus = node;
520 }
521 }
522
523 // The node should be a child of its parent if we have the parent.
524 AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
525 if (nodeParent != null) {
526 boolean childOfItsParent = false;
527 final int childCount = nodeParent.getChildCount();
528 for (int k = 0; k < childCount; k++) {
529 AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
530 if (child == node) {
531 childOfItsParent = true;
532 break;
533 }
534 }
535 if (!childOfItsParent) {
Svetoslav13bd7712014-09-19 18:18:04 -0700536 Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
Svetoslav8e3feb12014-02-24 13:46:47 -0800537 + nodeParent + " and child: " + node);
538 }
539 }
540
541 // The node should be the parent of its child if we have the child.
542 final int childCount = node.getChildCount();
543 for (int k = 0; k < childCount; k++) {
544 AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
545 if (child != null) {
546 AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
547 if (parent != node) {
Svetoslav13bd7712014-09-19 18:18:04 -0700548 Log.e(LOG_TAG, "Invalid child-parent relation between child: "
Svetoslav8e3feb12014-02-24 13:46:47 -0800549 + node + " and parent: " + nodeParent);
550 }
551 }
552 }
553 }
554 }
555 }
556 }
Phil Weaverb010b122016-08-17 17:47:48 -0700557
558 // Layer of indirection included to break dependency chain for testing
559 public static class AccessibilityNodeRefresher {
560 public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
Phil Weaverc2e28932016-12-08 12:29:25 -0800561 return info.refresh(null, bypassCache);
Phil Weaverb010b122016-08-17 17:47:48 -0700562 }
563 }
Svetoslav8e3feb12014-02-24 13:46:47 -0800564}