1 /*
2  * Copyright 2016 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 
17 package android.view.accessibility;
18 
19 import static junit.framework.Assert.assertEquals;
20 import static junit.framework.Assert.assertNotNull;
21 import static junit.framework.Assert.assertNull;
22 
23 import static org.junit.Assert.fail;
24 import static org.mockito.Matchers.anyBoolean;
25 import static org.mockito.Matchers.anyObject;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.mock;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.verify;
30 import static org.mockito.Mockito.when;
31 
32 import android.util.SparseArray;
33 import android.view.Display;
34 import android.view.View;
35 
36 import androidx.test.filters.LargeTest;
37 import androidx.test.runner.AndroidJUnit4;
38 
39 import com.google.common.base.Throwables;
40 
41 import org.junit.After;
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.mockito.invocation.InvocationOnMock;
46 import org.mockito.stubbing.Answer;
47 
48 import java.util.Arrays;
49 import java.util.List;
50 
51 @LargeTest
52 @RunWith(AndroidJUnit4.class)
53 public class AccessibilityCacheTest {
54     private static final int WINDOW_ID_1 = 0xBEEF;
55     private static final int WINDOW_ID_2 = 0xFACE;
56     private static final int WINDOW_ID_3 = 0xABCD;
57     private static final int WINDOW_ID_4 = 0xDCBA;
58     private static final int SINGLE_VIEW_ID = 0xCAFE;
59     private static final int OTHER_VIEW_ID = 0xCAB2;
60     private static final int PARENT_VIEW_ID = 0xFED4;
61     private static final int CHILD_VIEW_ID = 0xFEED;
62     private static final int OTHER_CHILD_VIEW_ID = 0xACE2;
63     private static final int MOCK_CONNECTION_ID = 1;
64     private static final int SECONDARY_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
65     private static final int DEFAULT_WINDOW_LAYER = 0;
66     private static final int SPECIFIC_WINDOW_LAYER = 5;
67 
68     AccessibilityCache mAccessibilityCache;
69     AccessibilityCache.AccessibilityNodeRefresher mAccessibilityNodeRefresher;
70 
71     @Before
setUp()72     public void setUp() {
73         mAccessibilityNodeRefresher = mock(AccessibilityCache.AccessibilityNodeRefresher.class);
74         when(mAccessibilityNodeRefresher.refreshNode(anyObject(), anyBoolean())).thenReturn(true);
75         mAccessibilityCache = new AccessibilityCache(mAccessibilityNodeRefresher);
76     }
77 
78     @After
tearDown()79     public void tearDown() {
80         // Make sure we're recycling all of our window and node infos.
81         mAccessibilityCache.clear();
82         AccessibilityInteractionClient.getInstance().clearCache();
83     }
84 
85     @Test
testEmptyCache_returnsNull()86     public void testEmptyCache_returnsNull() {
87         assertNull(mAccessibilityCache.getNode(0, 0));
88         assertNull(mAccessibilityCache.getWindowsOnAllDisplays());
89         assertNull(mAccessibilityCache.getWindow(0));
90     }
91 
92     @Test
testEmptyCache_clearDoesntCrash()93     public void testEmptyCache_clearDoesntCrash() {
94         mAccessibilityCache.clear();
95     }
96 
97     @Test
testEmptyCache_a11yEventsHaveNoEffect()98     public void testEmptyCache_a11yEventsHaveNoEffect() {
99         AccessibilityEvent event = AccessibilityEvent.obtain();
100         int[] a11yEventTypes = {
101                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
102                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
103                 AccessibilityEvent.TYPE_VIEW_FOCUSED,
104                 AccessibilityEvent.TYPE_VIEW_SELECTED,
105                 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED,
106                 AccessibilityEvent.TYPE_VIEW_CLICKED,
107                 AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED,
108                 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
109                 AccessibilityEvent.TYPE_VIEW_SCROLLED,
110                 AccessibilityEvent.TYPE_WINDOWS_CHANGED,
111                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED};
112         for (int i = 0; i < a11yEventTypes.length; i++) {
113             event.setEventType(a11yEventTypes[i]);
114             mAccessibilityCache.onAccessibilityEvent(event);
115         }
116     }
117 
118     @Test
addThenGetWindow_returnsEquivalentButNotSameWindow()119     public void addThenGetWindow_returnsEquivalentButNotSameWindow() {
120         AccessibilityWindowInfo windowInfo = null, copyOfInfo = null, windowFromCache = null;
121         try {
122             windowInfo = AccessibilityWindowInfo.obtain();
123             windowInfo.setId(WINDOW_ID_1);
124             windowInfo.setDisplayId(Display.DEFAULT_DISPLAY);
125             mAccessibilityCache.addWindow(windowInfo);
126             // Make a copy
127             copyOfInfo = AccessibilityWindowInfo.obtain(windowInfo);
128             windowInfo.setId(WINDOW_ID_2); // Simulate recycling and reusing the original info.
129             windowFromCache = mAccessibilityCache.getWindow(WINDOW_ID_1);
130             assertEquals(copyOfInfo, windowFromCache);
131         } finally {
132             windowFromCache.recycle();
133             windowInfo.recycle();
134             copyOfInfo.recycle();
135         }
136     }
137 
138     @Test
addWindowThenClear_noLongerInCache()139     public void addWindowThenClear_noLongerInCache() {
140         putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_1, Display.DEFAULT_DISPLAY,
141                 DEFAULT_WINDOW_LAYER);
142         mAccessibilityCache.clear();
143         assertNull(mAccessibilityCache.getWindow(WINDOW_ID_1));
144     }
145 
146     @Test
addWindowGetOtherId_returnsNull()147     public void addWindowGetOtherId_returnsNull() {
148         putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_1, Display.DEFAULT_DISPLAY,
149                 DEFAULT_WINDOW_LAYER);
150         assertNull(mAccessibilityCache.getWindow(WINDOW_ID_1 + 1));
151     }
152 
153     @Test
addWindowThenGetWindows_returnsNull()154     public void addWindowThenGetWindows_returnsNull() {
155         putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_1, Display.DEFAULT_DISPLAY,
156                 DEFAULT_WINDOW_LAYER);
157         assertNull(mAccessibilityCache.getWindowsOnAllDisplays());
158     }
159 
160     @Test
setWindowsThenGetWindows_returnsInDecreasingLayerOrder()161     public void setWindowsThenGetWindows_returnsInDecreasingLayerOrder() {
162         AccessibilityWindowInfo windowInfo1 = null;
163         AccessibilityWindowInfo windowInfo2 = null;
164         AccessibilityWindowInfo window1Out = null;
165         AccessibilityWindowInfo window2Out = null;
166         List<AccessibilityWindowInfo> windowsOut = null;
167         try {
168             windowInfo1 = obtainAccessibilityWindowInfo(WINDOW_ID_1, SPECIFIC_WINDOW_LAYER);
169             windowInfo2 = obtainAccessibilityWindowInfo(WINDOW_ID_2, windowInfo1.getLayer() + 1);
170             List<AccessibilityWindowInfo> windowsIn = Arrays.asList(windowInfo1, windowInfo2);
171             setWindowsByDisplay(Display.DEFAULT_DISPLAY, windowsIn);
172 
173             windowsOut = getWindowsByDisplay(Display.DEFAULT_DISPLAY);
174             window1Out = mAccessibilityCache.getWindow(WINDOW_ID_1);
175             window2Out = mAccessibilityCache.getWindow(WINDOW_ID_2);
176 
177             assertEquals(2, windowsOut.size());
178             assertEquals(windowInfo2, windowsOut.get(0));
179             assertEquals(windowInfo1, windowsOut.get(1));
180             assertEquals(windowInfo1, window1Out);
181             assertEquals(windowInfo2, window2Out);
182         } finally {
183             window1Out.recycle();
184             window2Out.recycle();
185             windowInfo1.recycle();
186             windowInfo2.recycle();
187             for (AccessibilityWindowInfo windowInfo : windowsOut) {
188                 windowInfo.recycle();
189             }
190         }
191     }
192 
193     @Test
setWindowsAndAddWindow_thenGetWindows_returnsInDecreasingLayerOrder()194     public void setWindowsAndAddWindow_thenGetWindows_returnsInDecreasingLayerOrder() {
195         AccessibilityWindowInfo windowInfo1 = null;
196         AccessibilityWindowInfo windowInfo2 = null;
197         AccessibilityWindowInfo window1Out = null;
198         AccessibilityWindowInfo window2Out = null;
199         AccessibilityWindowInfo window3Out = null;
200         List<AccessibilityWindowInfo> windowsOut = null;
201         try {
202             windowInfo1 = obtainAccessibilityWindowInfo(WINDOW_ID_1, SPECIFIC_WINDOW_LAYER);
203             windowInfo2 = obtainAccessibilityWindowInfo(WINDOW_ID_2, windowInfo1.getLayer() + 2);
204             List<AccessibilityWindowInfo> windowsIn = Arrays.asList(windowInfo1, windowInfo2);
205             setWindowsByDisplay(Display.DEFAULT_DISPLAY, windowsIn);
206 
207             putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_3, Display.DEFAULT_DISPLAY,
208                     windowInfo1.getLayer() + 1);
209 
210             windowsOut = getWindowsByDisplay(Display.DEFAULT_DISPLAY);
211             window1Out = mAccessibilityCache.getWindow(WINDOW_ID_1);
212             window2Out = mAccessibilityCache.getWindow(WINDOW_ID_2);
213             window3Out = mAccessibilityCache.getWindow(WINDOW_ID_3);
214 
215             assertEquals(3, windowsOut.size());
216             assertEquals(windowInfo2, windowsOut.get(0));
217             assertEquals(windowInfo1, windowsOut.get(2));
218             assertEquals(windowInfo1, window1Out);
219             assertEquals(windowInfo2, window2Out);
220             assertEquals(window3Out, windowsOut.get(1));
221         } finally {
222             window1Out.recycle();
223             window2Out.recycle();
224             window3Out.recycle();
225             windowInfo1.recycle();
226             windowInfo2.recycle();
227             for (AccessibilityWindowInfo windowInfo : windowsOut) {
228                 windowInfo.recycle();
229             }
230         }
231     }
232 
233     @Test
234     public void
setWindowsAtFirstDisplay_thenAddWindowAtSecondDisplay_returnWindowLayerOrderUnchange()235             setWindowsAtFirstDisplay_thenAddWindowAtSecondDisplay_returnWindowLayerOrderUnchange() {
236         AccessibilityWindowInfo windowInfo1 = null;
237         AccessibilityWindowInfo windowInfo2 = null;
238         AccessibilityWindowInfo window1Out = null;
239         AccessibilityWindowInfo window2Out = null;
240         List<AccessibilityWindowInfo> windowsOut = null;
241         try {
242             // Sets windows to default display.
243             windowInfo1 = obtainAccessibilityWindowInfo(WINDOW_ID_1, SPECIFIC_WINDOW_LAYER);
244             windowInfo2 = obtainAccessibilityWindowInfo(WINDOW_ID_2, windowInfo1.getLayer() + 2);
245             List<AccessibilityWindowInfo> windowsIn = Arrays.asList(windowInfo1, windowInfo2);
246             setWindowsByDisplay(Display.DEFAULT_DISPLAY, windowsIn);
247             // Adds one window to second display.
248             putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_3, SECONDARY_DISPLAY_ID,
249                     windowInfo1.getLayer() + 1);
250 
251             windowsOut = getWindowsByDisplay(Display.DEFAULT_DISPLAY);
252             window1Out = mAccessibilityCache.getWindow(WINDOW_ID_1);
253             window2Out = mAccessibilityCache.getWindow(WINDOW_ID_2);
254 
255             assertEquals(2, windowsOut.size());
256             assertEquals(windowInfo2, windowsOut.get(0));
257             assertEquals(windowInfo1, windowsOut.get(1));
258             assertEquals(windowInfo1, window1Out);
259             assertEquals(windowInfo2, window2Out);
260         } finally {
261             window1Out.recycle();
262             window2Out.recycle();
263             windowInfo1.recycle();
264             windowInfo2.recycle();
265             for (AccessibilityWindowInfo windowInfo : windowsOut) {
266                 windowInfo.recycle();
267             }
268         }
269     }
270 
271     @Test
setWindowsAtTwoDisplays_thenGetWindows_returnsInDecreasingLayerOrder()272     public void setWindowsAtTwoDisplays_thenGetWindows_returnsInDecreasingLayerOrder() {
273         AccessibilityWindowInfo windowInfo1 = null;
274         AccessibilityWindowInfo windowInfo2 = null;
275         AccessibilityWindowInfo window1Out = null;
276         AccessibilityWindowInfo window2Out = null;
277         AccessibilityWindowInfo windowInfo3 = null;
278         AccessibilityWindowInfo windowInfo4 = null;
279         AccessibilityWindowInfo window3Out = null;
280         AccessibilityWindowInfo window4Out = null;
281         List<AccessibilityWindowInfo> windowsOut1 = null;
282         List<AccessibilityWindowInfo> windowsOut2 = null;
283         try {
284             // Prepares all windows for default display.
285             windowInfo1 = obtainAccessibilityWindowInfo(WINDOW_ID_1, SPECIFIC_WINDOW_LAYER);
286             windowInfo2 = obtainAccessibilityWindowInfo(WINDOW_ID_2, windowInfo1.getLayer() + 1);
287             List<AccessibilityWindowInfo> windowsIn1 = Arrays.asList(windowInfo1, windowInfo2);
288             // Prepares all windows for second display.
289             windowInfo3 = obtainAccessibilityWindowInfo(WINDOW_ID_3, windowInfo1.getLayer() + 2);
290             windowInfo4 = obtainAccessibilityWindowInfo(WINDOW_ID_4, windowInfo1.getLayer() + 3);
291             List<AccessibilityWindowInfo> windowsIn2 = Arrays.asList(windowInfo3, windowInfo4);
292             // Sets all windows of all displays into A11y cache.
293             SparseArray<List<AccessibilityWindowInfo>> allWindows = new SparseArray<>();
294             allWindows.put(Display.DEFAULT_DISPLAY, windowsIn1);
295             allWindows.put(SECONDARY_DISPLAY_ID, windowsIn2);
296             mAccessibilityCache.setWindowsOnAllDisplays(allWindows);
297             // Gets windows at default display.
298             windowsOut1 = getWindowsByDisplay(Display.DEFAULT_DISPLAY);
299             window1Out = mAccessibilityCache.getWindow(WINDOW_ID_1);
300             window2Out = mAccessibilityCache.getWindow(WINDOW_ID_2);
301 
302             assertEquals(2, windowsOut1.size());
303             assertEquals(windowInfo2, windowsOut1.get(0));
304             assertEquals(windowInfo1, windowsOut1.get(1));
305             assertEquals(windowInfo1, window1Out);
306             assertEquals(windowInfo2, window2Out);
307             // Gets windows at seocnd display.
308             windowsOut2 = getWindowsByDisplay(SECONDARY_DISPLAY_ID);
309             window3Out = mAccessibilityCache.getWindow(WINDOW_ID_3);
310             window4Out = mAccessibilityCache.getWindow(WINDOW_ID_4);
311 
312             assertEquals(2, windowsOut2.size());
313             assertEquals(windowInfo4, windowsOut2.get(0));
314             assertEquals(windowInfo3, windowsOut2.get(1));
315             assertEquals(windowInfo3, window3Out);
316             assertEquals(windowInfo4, window4Out);
317         } finally {
318             window1Out.recycle();
319             window2Out.recycle();
320             windowInfo1.recycle();
321             windowInfo2.recycle();
322             window3Out.recycle();
323             window4Out.recycle();
324             windowInfo3.recycle();
325             windowInfo4.recycle();
326             for (AccessibilityWindowInfo windowInfo : windowsOut1) {
327                 windowInfo.recycle();
328             }
329             for (AccessibilityWindowInfo windowInfo : windowsOut2) {
330                 windowInfo.recycle();
331             }
332         }
333     }
334 
335     @Test
addWindowThenStateChangedEvent_noLongerInCache()336     public void addWindowThenStateChangedEvent_noLongerInCache() {
337         putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_1, Display.DEFAULT_DISPLAY,
338                 DEFAULT_WINDOW_LAYER);
339         mAccessibilityCache.onAccessibilityEvent(
340                 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED));
341         assertNull(mAccessibilityCache.getWindow(WINDOW_ID_1));
342     }
343 
344     @Test
addWindowThenWindowsChangedEvent_noLongerInCache()345     public void addWindowThenWindowsChangedEvent_noLongerInCache() {
346         putWindowWithWindowIdAndDisplayIdInCache(WINDOW_ID_1, Display.DEFAULT_DISPLAY,
347                 DEFAULT_WINDOW_LAYER);
348         mAccessibilityCache.onAccessibilityEvent(
349                 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOWS_CHANGED));
350         assertNull(mAccessibilityCache.getWindow(WINDOW_ID_1));
351     }
352 
353     @Test
addThenGetNode_returnsEquivalentNode()354     public void addThenGetNode_returnsEquivalentNode() {
355         AccessibilityNodeInfo nodeInfo, nodeCopy = null, nodeFromCache = null;
356         try {
357             nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
358             long id = nodeInfo.getSourceNodeId();
359             nodeCopy = AccessibilityNodeInfo.obtain(nodeInfo);
360             mAccessibilityCache.add(nodeInfo);
361             nodeInfo.recycle();
362             nodeFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, id);
363             assertEquals(nodeCopy, nodeFromCache);
364         } finally {
365             nodeFromCache.recycle();
366             nodeCopy.recycle();
367         }
368     }
369 
370     @Test
overwriteThenGetNode_returnsNewNode()371     public void overwriteThenGetNode_returnsNewNode() {
372         final CharSequence contentDescription1 = "foo";
373         final CharSequence contentDescription2 = "bar";
374         AccessibilityNodeInfo nodeInfo1 = null, nodeInfo2 = null, nodeFromCache = null;
375         try {
376             nodeInfo1 = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
377             nodeInfo1.setContentDescription(contentDescription1);
378             long id = nodeInfo1.getSourceNodeId();
379             nodeInfo2 = AccessibilityNodeInfo.obtain(nodeInfo1);
380             nodeInfo2.setContentDescription(contentDescription2);
381             mAccessibilityCache.add(nodeInfo1);
382             mAccessibilityCache.add(nodeInfo2);
383             nodeFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, id);
384             assertEquals(nodeInfo2, nodeFromCache);
385             assertEquals(contentDescription2, nodeFromCache.getContentDescription());
386         } finally {
387             nodeFromCache.recycle();
388             nodeInfo2.recycle();
389             nodeInfo1.recycle();
390         }
391     }
392 
393     @Test
nodesInDifferentWindowWithSameId_areKeptSeparate()394     public void nodesInDifferentWindowWithSameId_areKeptSeparate() {
395         final CharSequence contentDescription1 = "foo";
396         final CharSequence contentDescription2 = "bar";
397         AccessibilityNodeInfo nodeInfo1 = null, nodeInfo2 = null,
398                 node1FromCache = null, node2FromCache = null;
399         try {
400             nodeInfo1 = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
401             nodeInfo1.setContentDescription(contentDescription1);
402             long id = nodeInfo1.getSourceNodeId();
403             nodeInfo2 = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_2);
404             nodeInfo2.setContentDescription(contentDescription2);
405             assertEquals(id, nodeInfo2.getSourceNodeId());
406             mAccessibilityCache.add(nodeInfo1);
407             mAccessibilityCache.add(nodeInfo2);
408             node1FromCache = mAccessibilityCache.getNode(WINDOW_ID_1, id);
409             node2FromCache = mAccessibilityCache.getNode(WINDOW_ID_2, id);
410             assertEquals(nodeInfo1, node1FromCache);
411             assertEquals(nodeInfo2, node2FromCache);
412             assertEquals(nodeInfo1.getContentDescription(), node1FromCache.getContentDescription());
413             assertEquals(nodeInfo2.getContentDescription(), node2FromCache.getContentDescription());
414         } finally {
415             node1FromCache.recycle();
416             node2FromCache.recycle();
417             nodeInfo1.recycle();
418             nodeInfo2.recycle();
419         }
420     }
421 
422     @Test
addNodeThenClear_nodeIsRemoved()423     public void addNodeThenClear_nodeIsRemoved() {
424         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
425         long id = nodeInfo.getSourceNodeId();
426         mAccessibilityCache.add(nodeInfo);
427         nodeInfo.recycle();
428         mAccessibilityCache.clear();
429         assertNull(mAccessibilityCache.getNode(WINDOW_ID_1, id));
430     }
431 
432     @Test
windowStateChangeAndWindowsChangedEvents_clearsNode()433     public void windowStateChangeAndWindowsChangedEvents_clearsNode() {
434         assertEventTypeClearsNode(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
435         assertEventTypeClearsNode(AccessibilityEvent.TYPE_WINDOWS_CHANGED);
436     }
437 
438     @Test
windowsChangedWithWindowsChangeA11yFocusedEvent_dontClearCache()439     public void windowsChangedWithWindowsChangeA11yFocusedEvent_dontClearCache() {
440         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
441         mAccessibilityCache.add(nodeInfo);
442         AccessibilityEvent event = new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOWS_CHANGED);
443         event.setWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED);
444         mAccessibilityCache.onAccessibilityEvent(event);
445         AccessibilityNodeInfo cachedNode = mAccessibilityCache.getNode(WINDOW_ID_1,
446                 nodeInfo.getSourceNodeId());
447         try {
448             assertNotNull(cachedNode);
449         } finally {
450             nodeInfo.recycle();
451         }
452     }
453 
454     @Test
subTreeChangeEvent_clearsNodeAndChild()455     public void subTreeChangeEvent_clearsNodeAndChild() {
456         AccessibilityEvent event = AccessibilityEvent
457                 .obtain(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
458         event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
459         event.setSource(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
460 
461         try {
462             assertEventClearsParentAndChild(event);
463         } finally {
464             event.recycle();
465         }
466     }
467 
468     @Test
subTreeChangeEventFromUncachedNode_clearsNodeInCache()469     public void subTreeChangeEventFromUncachedNode_clearsNodeInCache() {
470         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(CHILD_VIEW_ID, WINDOW_ID_1);
471         long id = nodeInfo.getSourceNodeId();
472         mAccessibilityCache.add(nodeInfo);
473         nodeInfo.recycle();
474 
475         AccessibilityEvent event = AccessibilityEvent
476                 .obtain(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
477         event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
478         event.setSource(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
479 
480         mAccessibilityCache.onAccessibilityEvent(event);
481         AccessibilityNodeInfo shouldBeNull = mAccessibilityCache.getNode(WINDOW_ID_1, id);
482         if (shouldBeNull != null) {
483             shouldBeNull.recycle();
484         }
485         assertNull(shouldBeNull);
486     }
487 
488     @Test
scrollEvent_clearsNodeAndChild()489     public void scrollEvent_clearsNodeAndChild() {
490         AccessibilityEvent event = AccessibilityEvent
491                 .obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
492         event.setSource(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
493         try {
494             assertEventClearsParentAndChild(event);
495         } finally {
496             event.recycle();
497         }
498     }
499 
500     @Test
reparentNode_clearsOldParent()501     public void reparentNode_clearsOldParent() {
502         AccessibilityNodeInfo parentNodeInfo = getParentNode();
503         AccessibilityNodeInfo childNodeInfo = getChildNode();
504         long parentId = parentNodeInfo.getSourceNodeId();
505         mAccessibilityCache.add(parentNodeInfo);
506         mAccessibilityCache.add(childNodeInfo);
507 
508         childNodeInfo.setParent(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID + 1, WINDOW_ID_1));
509         mAccessibilityCache.add(childNodeInfo);
510 
511         AccessibilityNodeInfo parentFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, parentId);
512         try {
513             assertNull(parentFromCache);
514         } finally {
515             parentNodeInfo.recycle();
516             childNodeInfo.recycle();
517             if (parentFromCache != null) {
518                 parentFromCache.recycle();
519             }
520         }
521     }
522 
523     @Test
removeChildFromParent_clearsChild()524     public void removeChildFromParent_clearsChild() {
525         AccessibilityNodeInfo parentNodeInfo = getParentNode();
526         AccessibilityNodeInfo childNodeInfo = getChildNode();
527         long childId = childNodeInfo.getSourceNodeId();
528         mAccessibilityCache.add(parentNodeInfo);
529         mAccessibilityCache.add(childNodeInfo);
530 
531         AccessibilityNodeInfo parentNodeInfoWithNoChildren =
532                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
533         mAccessibilityCache.add(parentNodeInfoWithNoChildren);
534 
535         AccessibilityNodeInfo childFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, childId);
536         try {
537             assertNull(childFromCache);
538         } finally {
539             parentNodeInfoWithNoChildren.recycle();
540             parentNodeInfo.recycle();
541             childNodeInfo.recycle();
542             if (childFromCache != null) {
543                 childFromCache.recycle();
544             }
545         }
546     }
547 
548     @Test
nodeSourceOfA11yFocusEvent_getsRefreshed()549     public void nodeSourceOfA11yFocusEvent_getsRefreshed() {
550         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
551         nodeInfo.setAccessibilityFocused(false);
552         mAccessibilityCache.add(nodeInfo);
553         AccessibilityEvent event = AccessibilityEvent.obtain(
554                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
555         event.setSource(getMockViewWithA11yAndWindowIds(SINGLE_VIEW_ID, WINDOW_ID_1));
556         mAccessibilityCache.onAccessibilityEvent(event);
557         event.recycle();
558         try {
559             verify(mAccessibilityNodeRefresher).refreshNode(nodeInfo, true);
560         } finally {
561             nodeInfo.recycle();
562         }
563     }
564 
565     @Test
nodeWithA11yFocusWhenAnotherNodeGetsFocus_getsRemoved()566     public void nodeWithA11yFocusWhenAnotherNodeGetsFocus_getsRemoved() {
567         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
568         nodeInfo.setAccessibilityFocused(true);
569         mAccessibilityCache.add(nodeInfo);
570         AccessibilityEvent event = AccessibilityEvent.obtain(
571                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
572         event.setSource(getMockViewWithA11yAndWindowIds(OTHER_VIEW_ID, WINDOW_ID_1));
573         mAccessibilityCache.onAccessibilityEvent(event);
574         event.recycle();
575         try {
576             assertNull(mAccessibilityCache.getNode(WINDOW_ID_1, SINGLE_VIEW_ID));
577         } finally {
578             nodeInfo.recycle();
579         }
580     }
581 
582     @Test
nodeWithA11yFocusClearsIt_refreshes()583     public void nodeWithA11yFocusClearsIt_refreshes() {
584         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
585         nodeInfo.setAccessibilityFocused(true);
586         mAccessibilityCache.add(nodeInfo);
587         AccessibilityEvent event = AccessibilityEvent.obtain(
588                 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
589         event.setSource(getMockViewWithA11yAndWindowIds(SINGLE_VIEW_ID, WINDOW_ID_1));
590         mAccessibilityCache.onAccessibilityEvent(event);
591         event.recycle();
592         try {
593             verify(mAccessibilityNodeRefresher).refreshNode(nodeInfo, true);
594         } finally {
595             nodeInfo.recycle();
596         }
597     }
598 
599     @Test
nodeSourceOfInputFocusEvent_getsRefreshed()600     public void nodeSourceOfInputFocusEvent_getsRefreshed() {
601         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
602         nodeInfo.setFocused(false);
603         mAccessibilityCache.add(nodeInfo);
604         AccessibilityEvent event = AccessibilityEvent.obtain(
605                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
606         event.setSource(getMockViewWithA11yAndWindowIds(SINGLE_VIEW_ID, WINDOW_ID_1));
607         mAccessibilityCache.onAccessibilityEvent(event);
608         event.recycle();
609         try {
610             verify(mAccessibilityNodeRefresher).refreshNode(nodeInfo, true);
611         } finally {
612             nodeInfo.recycle();
613         }
614     }
615 
616     @Test
nodeWithInputFocusWhenAnotherNodeGetsFocus_getsRemoved()617     public void nodeWithInputFocusWhenAnotherNodeGetsFocus_getsRemoved() {
618         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
619         nodeInfo.setFocused(true);
620         mAccessibilityCache.add(nodeInfo);
621         AccessibilityEvent event = AccessibilityEvent.obtain(
622                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
623         event.setSource(getMockViewWithA11yAndWindowIds(OTHER_VIEW_ID, WINDOW_ID_1));
624         mAccessibilityCache.onAccessibilityEvent(event);
625         event.recycle();
626         try {
627             assertNull(mAccessibilityCache.getNode(WINDOW_ID_1, SINGLE_VIEW_ID));
628         } finally {
629             nodeInfo.recycle();
630         }
631     }
632 
633     @Test
nodeEventSaysWasSelected_getsRefreshed()634     public void nodeEventSaysWasSelected_getsRefreshed() {
635         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_VIEW_SELECTED,
636                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
637     }
638 
639     @Test
nodeEventSaysHadTextChanged_getsRefreshed()640     public void nodeEventSaysHadTextChanged_getsRefreshed() {
641         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED,
642                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
643     }
644 
645     @Test
nodeEventSaysWasClicked_getsRefreshed()646     public void nodeEventSaysWasClicked_getsRefreshed() {
647         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_VIEW_CLICKED,
648                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
649     }
650 
651     @Test
nodeEventSaysHadSelectionChange_getsRefreshed()652     public void nodeEventSaysHadSelectionChange_getsRefreshed() {
653         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED,
654                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
655     }
656 
657     @Test
nodeEventSaysHadTextContentChange_getsRefreshed()658     public void nodeEventSaysHadTextContentChange_getsRefreshed() {
659         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
660                 AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
661     }
662 
663     @Test
nodeEventSaysHadContentDescriptionChange_getsRefreshed()664     public void nodeEventSaysHadContentDescriptionChange_getsRefreshed() {
665         assertNodeIsRefreshedWithEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
666                 AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
667     }
668 
669     @Test
addNode_whenNodeBeingReplacedIsOwnGrandparentWithTwoChildren_doesntCrash()670     public void addNode_whenNodeBeingReplacedIsOwnGrandparentWithTwoChildren_doesntCrash() {
671         AccessibilityNodeInfo parentNodeInfo =
672                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
673         parentNodeInfo.addChild(getMockViewWithA11yAndWindowIds(CHILD_VIEW_ID, WINDOW_ID_1));
674         parentNodeInfo.addChild(getMockViewWithA11yAndWindowIds(OTHER_CHILD_VIEW_ID, WINDOW_ID_1));
675         AccessibilityNodeInfo childNodeInfo =
676                 getNodeWithA11yAndWindowId(CHILD_VIEW_ID, WINDOW_ID_1);
677         childNodeInfo.setParent(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
678         childNodeInfo.addChild(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
679 
680         AccessibilityNodeInfo replacementParentNodeInfo =
681                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
682         try {
683             mAccessibilityCache.add(parentNodeInfo);
684             mAccessibilityCache.add(childNodeInfo);
685             mAccessibilityCache.add(replacementParentNodeInfo);
686         } finally {
687             parentNodeInfo.recycle();
688             childNodeInfo.recycle();
689             replacementParentNodeInfo.recycle();
690         }
691     }
692 
693     @Test
addNode_whenNodeBeingReplacedIsOwnGrandparentWithOneChild_doesntCrash()694     public void addNode_whenNodeBeingReplacedIsOwnGrandparentWithOneChild_doesntCrash() {
695         AccessibilityNodeInfo parentNodeInfo =
696                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
697         parentNodeInfo.addChild(getMockViewWithA11yAndWindowIds(CHILD_VIEW_ID, WINDOW_ID_1));
698         AccessibilityNodeInfo childNodeInfo =
699                 getNodeWithA11yAndWindowId(CHILD_VIEW_ID, WINDOW_ID_1);
700         childNodeInfo.setParent(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
701         childNodeInfo.addChild(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
702 
703         AccessibilityNodeInfo replacementParentNodeInfo =
704                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
705         try {
706             mAccessibilityCache.add(parentNodeInfo);
707             mAccessibilityCache.add(childNodeInfo);
708             mAccessibilityCache.add(replacementParentNodeInfo);
709         } finally {
710             parentNodeInfo.recycle();
711             childNodeInfo.recycle();
712             replacementParentNodeInfo.recycle();
713         }
714     }
715 
716     @Test
testCacheCriticalEventList_doesntLackEvents()717     public void testCacheCriticalEventList_doesntLackEvents() {
718         for (int i = 0; i < 32; i++) {
719             int eventType = 1 << i;
720             if ((eventType & AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK) == 0) {
721                 try {
722                     assertEventTypeClearsNode(eventType, false);
723                     verify(mAccessibilityNodeRefresher, never())
724                             .refreshNode(anyObject(), anyBoolean());
725                 } catch (Throwable e) {
726                     throw new AssertionError(
727                             "Failed for eventType: " + AccessibilityEvent.eventTypeToString(
728                                     eventType),
729                             e);
730                 }
731             }
732         }
733     }
734 
735     @Test
addA11yFocusNodeBeforeFocusClearedEvent_previousA11yFocusNodeGetsRemoved()736     public void addA11yFocusNodeBeforeFocusClearedEvent_previousA11yFocusNodeGetsRemoved() {
737         AccessibilityNodeInfo nodeInfo1 = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
738         nodeInfo1.setAccessibilityFocused(true);
739         mAccessibilityCache.add(nodeInfo1);
740         AccessibilityNodeInfo nodeInfo2 = getNodeWithA11yAndWindowId(OTHER_VIEW_ID, WINDOW_ID_1);
741         nodeInfo2.setAccessibilityFocused(true);
742         mAccessibilityCache.add(nodeInfo2);
743         try {
744             assertNull(mAccessibilityCache.getNode(WINDOW_ID_1, SINGLE_VIEW_ID));
745         } finally {
746             nodeInfo1.recycle();
747             nodeInfo2.recycle();
748         }
749     }
750 
751     @Test
addSameParentNodeWithDifferentChildNode_whenOriginalChildHasChild_doesntCrash()752     public void addSameParentNodeWithDifferentChildNode_whenOriginalChildHasChild_doesntCrash() {
753         AccessibilityNodeInfo parentNodeInfo = getParentNode();
754         AccessibilityNodeInfo childNodeInfo = getChildNode();
755         childNodeInfo.addChild(getMockViewWithA11yAndWindowIds(CHILD_VIEW_ID + 1, WINDOW_ID_1));
756 
757         AccessibilityNodeInfo replacementParentNodeInfo =
758                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
759         replacementParentNodeInfo.addChild(
760                 getMockViewWithA11yAndWindowIds(OTHER_CHILD_VIEW_ID, WINDOW_ID_1));
761         try {
762             mAccessibilityCache.add(parentNodeInfo);
763             mAccessibilityCache.add(childNodeInfo);
764             mAccessibilityCache.add(replacementParentNodeInfo);
765         } catch (IllegalStateException e) {
766             fail("recycle A11yNodeInfo twice" + Throwables.getStackTraceAsString(e));
767         } finally {
768             parentNodeInfo.recycle();
769             childNodeInfo.recycle();
770             replacementParentNodeInfo.recycle();
771         }
772     }
773 
assertNodeIsRefreshedWithEventType(int eventType, int contentChangeTypes)774     private void assertNodeIsRefreshedWithEventType(int eventType, int contentChangeTypes) {
775         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(SINGLE_VIEW_ID, WINDOW_ID_1);
776         mAccessibilityCache.add(nodeInfo);
777         AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
778         event.setSource(getMockViewWithA11yAndWindowIds(SINGLE_VIEW_ID, WINDOW_ID_1));
779         event.setContentChangeTypes(contentChangeTypes);
780         mAccessibilityCache.onAccessibilityEvent(event);
781         event.recycle();
782         try {
783             verify(mAccessibilityNodeRefresher).refreshNode(nodeInfo, true);
784         } finally {
785             nodeInfo.recycle();
786         }
787     }
788 
obtainAccessibilityWindowInfo(int windowId, int layer)789     private AccessibilityWindowInfo obtainAccessibilityWindowInfo(int windowId, int layer) {
790         AccessibilityWindowInfo windowInfo = AccessibilityWindowInfo.obtain();
791         windowInfo.setId(windowId);
792         windowInfo.setLayer(layer);
793         return windowInfo;
794     }
795 
putWindowWithWindowIdAndDisplayIdInCache(int windowId, int displayId, int layer)796     private void putWindowWithWindowIdAndDisplayIdInCache(int windowId, int displayId, int layer) {
797         AccessibilityWindowInfo windowInfo = obtainAccessibilityWindowInfo(windowId, layer);
798         windowInfo.setDisplayId(displayId);
799         mAccessibilityCache.addWindow(windowInfo);
800         windowInfo.recycle();
801     }
802 
getNodeWithA11yAndWindowId(int a11yId, int windowId)803     private AccessibilityNodeInfo getNodeWithA11yAndWindowId(int a11yId, int windowId) {
804         AccessibilityNodeInfo node =
805                 AccessibilityNodeInfo.obtain(getMockViewWithA11yAndWindowIds(a11yId, windowId));
806         node.setConnectionId(MOCK_CONNECTION_ID);
807         return node;
808     }
809 
getMockViewWithA11yAndWindowIds(int a11yId, int windowId)810     private View getMockViewWithA11yAndWindowIds(int a11yId, int windowId) {
811         View mockView = mock(View.class);
812         when(mockView.getAccessibilityViewId()).thenReturn(a11yId);
813         when(mockView.getAccessibilityWindowId()).thenReturn(windowId);
814         doAnswer(new Answer<AccessibilityNodeInfo>() {
815             public AccessibilityNodeInfo answer(InvocationOnMock invocation) {
816                 return AccessibilityNodeInfo.obtain((View) invocation.getMock());
817             }
818         }).when(mockView).createAccessibilityNodeInfo();
819         return mockView;
820     }
821 
assertEventTypeClearsNode(int eventType)822     private void assertEventTypeClearsNode(int eventType) {
823         assertEventTypeClearsNode(eventType, true);
824     }
825 
assertEventTypeClearsNode(int eventType, boolean clears)826     private void assertEventTypeClearsNode(int eventType, boolean clears) {
827         final int nodeId = 0xBEEF;
828         AccessibilityNodeInfo nodeInfo = getNodeWithA11yAndWindowId(nodeId, WINDOW_ID_1);
829         long id = nodeInfo.getSourceNodeId();
830         mAccessibilityCache.add(nodeInfo);
831         nodeInfo.recycle();
832         mAccessibilityCache.onAccessibilityEvent(AccessibilityEvent.obtain(eventType));
833         AccessibilityNodeInfo cachedNode = mAccessibilityCache.getNode(WINDOW_ID_1, id);
834         try {
835             if (clears) {
836                 assertNull(cachedNode);
837             } else {
838                 assertNotNull(cachedNode);
839             }
840         } finally {
841             if (cachedNode != null) {
842                 cachedNode.recycle();
843             }
844         }
845     }
846 
getParentNode()847     private AccessibilityNodeInfo getParentNode() {
848         AccessibilityNodeInfo parentNodeInfo =
849                 getNodeWithA11yAndWindowId(PARENT_VIEW_ID, WINDOW_ID_1);
850         parentNodeInfo.addChild(getMockViewWithA11yAndWindowIds(CHILD_VIEW_ID, WINDOW_ID_1));
851         return parentNodeInfo;
852     }
853 
getChildNode()854     private AccessibilityNodeInfo getChildNode() {
855         AccessibilityNodeInfo childNodeInfo =
856                 getNodeWithA11yAndWindowId(CHILD_VIEW_ID, WINDOW_ID_1);
857         childNodeInfo.setParent(getMockViewWithA11yAndWindowIds(PARENT_VIEW_ID, WINDOW_ID_1));
858         return childNodeInfo;
859     }
860 
assertEventClearsParentAndChild(AccessibilityEvent event)861     private void assertEventClearsParentAndChild(AccessibilityEvent event) {
862         AccessibilityNodeInfo parentNodeInfo = getParentNode();
863         AccessibilityNodeInfo childNodeInfo = getChildNode();
864         long parentId = parentNodeInfo.getSourceNodeId();
865         long childId = childNodeInfo.getSourceNodeId();
866         mAccessibilityCache.add(parentNodeInfo);
867         mAccessibilityCache.add(childNodeInfo);
868 
869         mAccessibilityCache.onAccessibilityEvent(event);
870         parentNodeInfo.recycle();
871         childNodeInfo.recycle();
872 
873         AccessibilityNodeInfo parentFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, parentId);
874         AccessibilityNodeInfo childFromCache = mAccessibilityCache.getNode(WINDOW_ID_1, childId);
875         try {
876             assertNull(parentFromCache);
877             assertNull(childFromCache);
878         } finally {
879             if (parentFromCache != null) {
880                 parentFromCache.recycle();
881             }
882             if (childFromCache != null) {
883                 childFromCache.recycle();
884             }
885         }
886     }
887 
setWindowsByDisplay(int displayId, List<AccessibilityWindowInfo> windows)888     private void setWindowsByDisplay(int displayId, List<AccessibilityWindowInfo> windows) {
889         SparseArray<List<AccessibilityWindowInfo>> allWindows = new SparseArray<>();
890         allWindows.put(displayId, windows);
891         mAccessibilityCache.setWindowsOnAllDisplays(allWindows);
892     }
893 
getWindowsByDisplay(int displayId)894     private List<AccessibilityWindowInfo> getWindowsByDisplay(int displayId) {
895         final SparseArray<List<AccessibilityWindowInfo>> allWindows =
896                 mAccessibilityCache.getWindowsOnAllDisplays();
897 
898         if (allWindows != null && allWindows.size() > 0) {
899             return allWindows.get(displayId);
900         }
901         return null;
902     }
903 }
904