1 /*
2  * Copyright (C) 2021 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 com.android.server.accessibility;
18 
19 
20 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_ANCESTORS;
21 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST;
22 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST;
23 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_HYBRID;
24 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS;
25 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_UNINTERRUPTIBLE;
26 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_SERVICE_REQUESTS_INCLUDE_NOT_IMPORTANT_VIEWS;
27 import static android.view.accessibility.AccessibilityNodeInfo.ROOT_NODE_ID;
28 
29 import static org.junit.Assert.assertEquals;
30 import static org.mockito.ArgumentMatchers.anyInt;
31 import static org.mockito.ArgumentMatchers.anyList;
32 import static org.mockito.ArgumentMatchers.eq;
33 import static org.mockito.Mockito.doAnswer;
34 import static org.mockito.Mockito.never;
35 import static org.mockito.Mockito.times;
36 import static org.mockito.Mockito.verify;
37 
38 import android.app.Instrumentation;
39 import android.content.Context;
40 import android.os.RemoteException;
41 import android.view.AccessibilityInteractionController;
42 import android.view.View;
43 import android.view.ViewRootImpl;
44 import android.view.WindowManager;
45 import android.view.accessibility.AccessibilityNodeIdManager;
46 import android.view.accessibility.AccessibilityNodeInfo;
47 import android.view.accessibility.AccessibilityNodeProvider;
48 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
49 import android.widget.FrameLayout;
50 import android.widget.TextView;
51 
52 import androidx.test.platform.app.InstrumentationRegistry;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import org.junit.After;
56 import org.junit.Before;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.mockito.ArgumentCaptor;
60 import org.mockito.Captor;
61 import org.mockito.Mock;
62 import org.mockito.MockitoAnnotations;
63 
64 import java.util.ArrayList;
65 import java.util.List;
66 
67 /**
68  * Tests that verify expected node and prefetched node results when finding a view by node id. We
69  * send some requests to the controller via View methods to control message timing.
70  */
71 @RunWith(AndroidJUnit4.class)
72 public class AccessibilityInteractionControllerNodeRequestsTest {
73     private AccessibilityInteractionController mAccessibilityInteractionController;
74     @Mock
75     private IAccessibilityInteractionConnectionCallback mMockClientCallback1;
76     @Mock
77     private IAccessibilityInteractionConnectionCallback mMockClientCallback2;
78 
79     @Captor
80     private ArgumentCaptor<AccessibilityNodeInfo> mFindInfoCaptor;
81     @Captor
82     private ArgumentCaptor<List<AccessibilityNodeInfo>> mFindInfosCaptor;
83     @Captor private ArgumentCaptor<List<AccessibilityNodeInfo>> mPrefetchInfoListCaptor;
84 
85     private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
86     private static final int MOCK_CLIENT_1_THREAD_AND_PROCESS_ID = 1;
87     private static final int MOCK_CLIENT_2_THREAD_AND_PROCESS_ID = 2;
88 
89     private static final String ROOT_FRAME_LAYOUT_DESCRIPTION = "rootFrameLayout";
90     private static final String TEXT_VIEW_1_DESCRIPTION = "textView1";
91     private static final String TEXT_VIEW_2_DESCRIPTION = "textView2";
92     private static final String CHILD_FRAME_DESCRIPTION = "childFrameLayout";
93     private static final String TEXT_VIEW_3_DESCRIPTION = "textView3";
94     private static final String TEXT_VIEW_4_DESCRIPTION = "textView4";
95     private static final String VIRTUAL_VIEW_1_DESCRIPTION = "virtual descendant 1";
96     private static final String VIRTUAL_VIEW_2_DESCRIPTION = "virtual descendant 2";
97     private static final String VIRTUAL_VIEW_3_DESCRIPTION = "virtual descendant 3";
98 
99     private TestFrameLayout mRootFrameLayout;
100     private TestFrameLayout mChildFrameLayout;
101     private TestTextView mTextView1;
102     private TestTextView mTextView2;
103     private TestTextView mTextView3;
104     private TestTextView mTextView4;
105 
106     private boolean mSendClient1RequestForTextAfterTextPrefetched;
107     private boolean mSendClient2RequestForTextAfterTextPrefetched;
108     private boolean mSendRequestForTextAndIncludeUnImportantViews;
109     private boolean mSendClient1RequestForRootAfterTextPrefetched;
110     private boolean mSendClient2RequestForTextAfterRootPrefetched;
111 
112     private int mMockClient1InteractionId;
113     private int mMockClient2InteractionId;
114 
115     @Before
setUp()116     public void setUp() throws Throwable {
117         MockitoAnnotations.initMocks(this);
118 
119         mInstrumentation.runOnMainSync(() -> {
120             final Context context = mInstrumentation.getTargetContext();
121             final ViewRootImpl viewRootImpl = new ViewRootImpl(context, context.getDisplay());
122 
123             mTextView1 = new TestTextView(context, 1, TEXT_VIEW_1_DESCRIPTION);
124             mTextView2 = new TestTextView(context, 2, TEXT_VIEW_2_DESCRIPTION);
125             mTextView3 = new TestTextView(context, 4, TEXT_VIEW_3_DESCRIPTION);
126             mTextView4 = new TestTextView(context, 5, TEXT_VIEW_4_DESCRIPTION);
127 
128             mChildFrameLayout = new TestFrameLayout(context, 3,
129                     CHILD_FRAME_DESCRIPTION, new ArrayList<>(List.of(mTextView4)));
130             mRootFrameLayout = new TestFrameLayout(context, 0, ROOT_FRAME_LAYOUT_DESCRIPTION,
131                     new ArrayList<>(
132                             List.of(mTextView1, mTextView2, mChildFrameLayout, mTextView3)));
133 
134             mRootFrameLayout.addView(mTextView1);
135             mRootFrameLayout.addView(mTextView2);
136             mChildFrameLayout.addView(mTextView4);
137             mRootFrameLayout.addView(mChildFrameLayout);
138             mRootFrameLayout.addView(mTextView3);
139 
140             // Layout
141             //                        mRootFrameLayout
142             //               /         |        |              \
143             //       mTextView1 mTextView2 mChildFrameLayout  mTextView3
144             //                                    |
145             //                                mTextView4
146 
147             // The controller retrieves views through this manager, and registration happens on
148             // when attached to a window, which we don't have. We can simply reference
149             // RootFrameLayout with ROOT_NODE_ID.
150             AccessibilityNodeIdManager.getInstance().registerViewWithId(
151                     mTextView1, mTextView1.getAccessibilityViewId());
152             AccessibilityNodeIdManager.getInstance().registerViewWithId(
153                     mTextView2, mTextView2.getAccessibilityViewId());
154             AccessibilityNodeIdManager.getInstance().registerViewWithId(
155                     mTextView3, mTextView3.getAccessibilityViewId());
156             AccessibilityNodeIdManager.getInstance().registerViewWithId(
157                     mChildFrameLayout, mChildFrameLayout.getAccessibilityViewId());
158             AccessibilityNodeIdManager.getInstance().registerViewWithId(
159                     mTextView4, mTextView4.getAccessibilityViewId());
160             try {
161                 viewRootImpl.setView(mRootFrameLayout, new WindowManager.LayoutParams(), null);
162 
163             } catch (WindowManager.BadTokenException e) {
164                 // activity isn't running, we will ignore BadTokenException.
165             }
166 
167             mAccessibilityInteractionController =
168                     new AccessibilityInteractionController(viewRootImpl);
169         });
170 
171     }
172 
173     @After
tearDown()174     public void tearDown() throws Throwable {
175         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
176                 mTextView1.getAccessibilityViewId());
177         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
178                 mTextView2.getAccessibilityViewId());
179         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
180                 mTextView3.getAccessibilityViewId());
181         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
182                 mTextView4.getAccessibilityViewId());
183         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
184                 mChildFrameLayout.getAccessibilityViewId());
185     }
186 
187     /**
188      * Tests a basic request for the root node with prefetch flag
189      * {@link AccessibilityNodeInfo#FLAG_PREFETCH_DESCENDANTS_HYBRID}
190      *
191      * @throws RemoteException
192      */
193     @Test
testFindRootView_withOneClient_shouldReturnRootNodeAndPrefetchDescendants()194     public void testFindRootView_withOneClient_shouldReturnRootNodeAndPrefetchDescendants()
195             throws RemoteException {
196         // Request for our RootFrameLayout.
197         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
198                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
199         mInstrumentation.waitForIdleSync();
200 
201         // Verify we get RootFrameLayout.
202         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
203                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
204         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
205         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
206 
207         verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
208                 mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
209         // The descendants are RootFrameLayout's 5 descendants.
210         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
211         assertEquals(5, prefetchedNodes.size());
212         assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
213         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
214         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
215         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
216         assertEquals(TEXT_VIEW_4_DESCRIPTION, prefetchedNodes.get(4).getContentDescription());
217     }
218 
219     /**
220      * Tests a basic request for TextView1's node with prefetch flag.
221      * {@link AccessibilityNodeInfo#FLAG_PREFETCH_SIBLINGS} and
222      * {@link AccessibilityNodeInfo#FLAG_PREFETCH_ANCESTORS}.
223      *
224      * @throws RemoteException
225      */
226     @Test
testFindTextView_withOneClient_shouldReturnNodeAndPrefetchedSiblingsAndParent()227     public void testFindTextView_withOneClient_shouldReturnNodeAndPrefetchedSiblingsAndParent()
228             throws RemoteException {
229         // Request for TextView1.
230         sendNodeRequestToController(AccessibilityNodeInfo.makeNodeId(
231                 mTextView1.getAccessibilityViewId(), AccessibilityNodeProvider.HOST_VIEW_ID),
232                 mMockClientCallback1, mMockClient1InteractionId,
233                 FLAG_PREFETCH_SIBLINGS | FLAG_PREFETCH_ANCESTORS);
234         mInstrumentation.waitForIdleSync();
235 
236         // Verify we get TextView1.
237         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
238                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
239         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
240         assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription());
241 
242         // Verify the prefetched sibling of TextView1 is TextView2, ChildFrameLayout, and TextView3.
243         // The predecessor is RootFrameLayout.
244         verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
245                 mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
246         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
247         assertEquals(4, prefetchedNodes.size());
248         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
249         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
250         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
251         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
252     }
253 
254     /**
255      * Tests a series of controller requests to prevent prefetching.
256      *     Request 1: Client 1 requests the root node
257      *     Request 2: When the root node is initialized in
258      *     {@link TestFrameLayout#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)},
259      *     Client 2 requests TestTextView1's node
260      *
261      * Request 2 on the queue prevents prefetching for Request 1.
262      *
263      * @throws RemoteException
264      */
265     @Test
testFindRootAndTextNodes_withTwoClients_shouldPreventClient1Prefetch()266     public void testFindRootAndTextNodes_withTwoClients_shouldPreventClient1Prefetch()
267             throws RemoteException {
268         mSendClient2RequestForTextAfterRootPrefetched = true;
269         mRootFrameLayout.setAccessibilityDelegate(new View.AccessibilityDelegate() {
270             @Override
271             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
272                 super.onInitializeAccessibilityNodeInfo(host, info);
273                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
274                         mTextView1.getAccessibilityViewId(),
275                         AccessibilityNodeProvider.HOST_VIEW_ID);
276 
277                 if (mSendClient2RequestForTextAfterRootPrefetched) {
278                     mSendClient2RequestForTextAfterRootPrefetched = false;
279 
280                     // Enqueue a request when this node is found from  client 2 for TextView1.
281                     sendNodeRequestToController(nodeId, mMockClientCallback2,
282                             mMockClient2InteractionId,
283                             FLAG_PREFETCH_SIBLINGS);
284                 }
285             }
286         });
287         // Client 1 request for RootFrameLayout.
288         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
289                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
290 
291         mInstrumentation.waitForIdleSync();
292 
293         // Verify client 1 gets RootFrameLayout.
294         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
295                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
296         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
297         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
298 
299         // The second request is put in the queue in the RootFrameLayout's onInitializeA11yNodeInfo,
300         // meaning prefetching is does not occur for first request.
301         verify(mMockClientCallback1, never())
302                 .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt());
303 
304         // Verify client 2 gets TextView1.
305         verify(mMockClientCallback2).setFindAccessibilityNodeInfoResult(
306                 mFindInfoCaptor.capture(), eq(mMockClient2InteractionId));
307         infoSentToService = mFindInfoCaptor.getValue();
308         assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription());
309 
310         // Verify the prefetched sibling of TextView1 is TextView2 and ChildFrameLayout
311         // (FLAG_PREFETCH_SIBLINGS). The parent, RootFrameLayout, is also retrieved. Since
312         // predecessors takes priority over siblings, RootFrameLayout is the first node in the list.
313         verify(mMockClientCallback2).setPrefetchAccessibilityNodeInfoResult(
314                 mPrefetchInfoListCaptor.capture(), eq(mMockClient2InteractionId));
315         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
316         assertEquals(4, prefetchedNodes.size());
317         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
318         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
319         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
320         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
321     }
322 
323     /**
324      * Tests a series of controller same-service requests to interrupt prefetching and satisfy a
325      * pending node request.
326      *     Request 1: Request the root node
327      *     Request 2: When TextTextView1's node is initialized as part of Request 1's prefetching,
328      *     request TestTextView1's node
329      *
330      * Request 1 prefetches TestTextView1's node, is interrupted by a pending request, and checks
331      * if its prefetched nodes satisfy any pending requests. It satisfies Request 2's request for
332      * TestTextView1's node. Request 2 is fulfilled, so it is removed from queue and does not
333      * prefetch. TestTextView1 is removed from Request's 1's prefetched results, meaning the list
334      * is empty.
335      *
336      * @throws RemoteException
337      */
338     @Test
testFindRootAndTextNode_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg()339     public void testFindRootAndTextNode_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg()
340             throws RemoteException {
341         mSendClient1RequestForTextAfterTextPrefetched = true;
342 
343         mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
344             @Override
345             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
346                 super.onInitializeAccessibilityNodeInfo(host, info);
347                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
348                         mTextView1.getAccessibilityViewId(),
349                         AccessibilityNodeProvider.HOST_VIEW_ID);
350 
351                 if (mSendClient1RequestForTextAfterTextPrefetched) {
352                     // Prevent a loop when processing this node's second request.
353                     mSendClient1RequestForTextAfterTextPrefetched = false;
354                     // TextView1 is prefetched here after the RootFrameLayout is found. Now enqueue
355                     // a same-client request for TextView1.
356                     sendNodeRequestToController(nodeId, mMockClientCallback1,
357                             ++mMockClient1InteractionId,
358                             FLAG_PREFETCH_SIBLINGS | FLAG_PREFETCH_ANCESTORS);
359 
360                 }
361             }
362         });
363         // Client 1 requests RootFrameLayout.
364         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
365                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
366 
367         // Flush out all messages.
368         mInstrumentation.waitForIdleSync();
369 
370         // When TextView1 is prefetched for RootFrameLayout, we put a message on the queue in
371         // TextView1's onInitializeA11yNodeInfo that requests for TextView1. The service thus gets
372         // two node results for FrameLayout and TextView1.
373         verify(mMockClientCallback1, times(2))
374                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
375 
376         List<AccessibilityNodeInfo> foundNodes = mFindInfoCaptor.getAllValues();
377         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, foundNodes.get(0).getContentDescription());
378         assertEquals(TEXT_VIEW_1_DESCRIPTION, foundNodes.get(1).getContentDescription());
379 
380         // The controller will look at RootFrameLayout's prefetched nodes and find matching nodes in
381         // pending requests. The prefetched TextView1 satisfied the second request. This is removed
382         // from the first request's prefetch list, which is now empty. The second request is removed
383         // from queue.
384         verify(mMockClientCallback1, never())
385                 .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
386                         eq(mMockClient1InteractionId - 1));
387     }
388 
389     /**
390      * Tests a series of controller same-service requests to interrupt prefetching and satisfy a
391      * pending node request.
392      *     Request 1: Request the root node
393      *     Request 2: When TextTextView1's node is initialized as part of Request 1's prefetching,
394      *     request the root node
395      *
396      * Request 1 prefetches TestTextView1's node, is interrupted by a pending request, and checks
397      * if its prefetched nodes satisfy any pending requests. It satisfies Request 2's request for
398      * the root node. Request 2 is fulfilled, so it is removed from queue and does not
399      * prefetch.
400      *
401      * @throws RemoteException
402      */
403     @Test
testFindRoot_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg()404     public void testFindRoot_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg()
405             throws RemoteException {
406         mSendClient1RequestForRootAfterTextPrefetched = true;
407 
408         mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
409             @Override
410             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
411                 super.onInitializeAccessibilityNodeInfo(host, info);
412                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
413                         mRootFrameLayout.getAccessibilityViewId(),
414                         AccessibilityNodeProvider.HOST_VIEW_ID);
415 
416                 if (mSendClient1RequestForRootAfterTextPrefetched) {
417                     // Prevent a loop when processing this node's second request.
418                     mSendClient1RequestForRootAfterTextPrefetched = false;
419                     // TextView1 is prefetched here after the FrameLayout is found. Now enqueue a
420                     // same-client request for FrameLayout.
421                     sendNodeRequestToController(nodeId, mMockClientCallback1,
422                             ++mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
423 
424                 }
425             }
426         });
427         // Client 1 requests RootFrameLayout.
428         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
429                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
430 
431         // Flush out all messages.
432         mInstrumentation.waitForIdleSync();
433 
434         // When TextView1 is prefetched for RootFrameLayout, we put a message on the queue in
435         // TextView1's onInitializeA11yNodeInfo that requests for TextView1. The service thus gets
436         // two node results for FrameLayout and TextView1.
437         verify(mMockClientCallback1, times(2))
438                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
439 
440         List<AccessibilityNodeInfo> foundNodes = mFindInfoCaptor.getAllValues();
441         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, foundNodes.get(0).getContentDescription());
442         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, foundNodes.get(1).getContentDescription());
443 
444         // The controller will look at RootFrameLayout's prefetched nodes and find matching nodes in
445         // pending requests.  The first requested node (FrameLayout) is also checked, and this
446         // satisfies the second request. The second request is removed from queue and prefetching
447         // for this request never occurs.
448         verify(mMockClientCallback1, times(1))
449                 .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
450                         eq(mMockClient1InteractionId - 1));
451         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
452         assertEquals(1, prefetchedNodes.size());
453         assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
454     }
455 
456     /**
457      * Like above, but tests a series of controller requests from different services to interrupt
458      * prefetching and satisfy a pending node request.
459      *
460      * @throws RemoteException
461      */
462     @Test
testFindRootAndTextNode_withTwoClients_shouldInterruptPrefetchAndSatisfyPendingMsg()463     public void testFindRootAndTextNode_withTwoClients_shouldInterruptPrefetchAndSatisfyPendingMsg()
464             throws RemoteException {
465         mSendClient2RequestForTextAfterTextPrefetched = true;
466         mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
467             @Override
468             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
469                 super.onInitializeAccessibilityNodeInfo(host, info);
470                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
471                         mTextView1.getAccessibilityViewId(),
472                         AccessibilityNodeProvider.HOST_VIEW_ID);
473 
474                 if (mSendClient2RequestForTextAfterTextPrefetched) {
475                     mSendClient2RequestForTextAfterTextPrefetched = false;
476                     // TextView1 is prefetched here. Now enqueue client 2's request for
477                     // TextView1.
478                     sendNodeRequestToController(nodeId, mMockClientCallback2,
479                             mMockClient2InteractionId, FLAG_PREFETCH_SIBLINGS);
480                 }
481             }
482         });
483         // Client 1 requests RootFrameLayout.
484         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
485                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
486 
487         mInstrumentation.waitForIdleSync();
488 
489         // Verify client 1 gets RootFrameLayout.
490         verify(mMockClientCallback1, times(1))
491                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
492         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION,
493                 mFindInfoCaptor.getValue().getContentDescription());
494 
495         // Verify client 1 doesn't have prefetched nodes.
496         verify(mMockClientCallback1, never())
497                 .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
498                         eq(mMockClient1InteractionId));
499 
500         // Verify client 2 gets TextView1.
501         verify(mMockClientCallback2, times(1))
502                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
503 
504         assertEquals(TEXT_VIEW_1_DESCRIPTION, mFindInfoCaptor.getValue().getContentDescription());
505 
506         // The second request was removed from queue and prefetching for this client request never
507         // occurred as it was satisfied.
508         verify(mMockClientCallback2, never())
509                 .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt());
510 
511     }
512 
513     @Test
testFindNodeById_withTwoDifferentPrefetchFlags_shouldNotSatisfyPendingRequest()514     public void testFindNodeById_withTwoDifferentPrefetchFlags_shouldNotSatisfyPendingRequest()
515             throws RemoteException {
516         mSendRequestForTextAndIncludeUnImportantViews = true;
517         mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
518             @Override
519             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
520                 super.onInitializeAccessibilityNodeInfo(host, info);
521                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
522                         mTextView1.getAccessibilityViewId(),
523                         AccessibilityNodeProvider.HOST_VIEW_ID);
524 
525                 if (mSendRequestForTextAndIncludeUnImportantViews) {
526                     mSendRequestForTextAndIncludeUnImportantViews = false;
527                     // TextView1 is prefetched here for client 1. Now enqueue a request from a
528                     // different client that holds different fetch flags for TextView1.
529                     sendNodeRequestToController(nodeId, mMockClientCallback2,
530                             mMockClient2InteractionId,
531                             FLAG_PREFETCH_SIBLINGS
532                                     | FLAG_SERVICE_REQUESTS_INCLUDE_NOT_IMPORTANT_VIEWS
533                                     | FLAG_PREFETCH_ANCESTORS);
534                 }
535             }
536         });
537 
538         // Mockito does not make copies of objects when called. It holds references, so
539         // the captor would point to client 2's results after all requests are processed. Verify
540         // prefetched node immediately.
541         doAnswer(invocation -> {
542             List<AccessibilityNodeInfo> prefetched = invocation.getArgument(0);
543             assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetched.get(0).getContentDescription());
544             return null;
545         }).when(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(anyList(),
546                 eq(mMockClient1InteractionId));
547 
548         // Client 1 requests RootFrameLayout.
549         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
550                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
551 
552         mInstrumentation.waitForIdleSync();
553 
554         // Verify client 1 gets RootFrameLayout.
555         verify(mMockClientCallback1, times(1))
556                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(),
557                         eq(mMockClient1InteractionId));
558 
559         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION,
560                 mFindInfoCaptor.getValue().getContentDescription());
561 
562         // Verify client 1 has prefetched results. The only prefetched node is TextView1
563         // (from above doAnswer).
564         verify(mMockClientCallback1, times(1))
565                 .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
566                         eq(mMockClient1InteractionId));
567 
568         // Verify client 2 gets TextView1.
569         verify(mMockClientCallback2, times(1))
570                 .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(),
571                         eq(mMockClient2InteractionId));
572         assertEquals(TEXT_VIEW_1_DESCRIPTION,
573                 mFindInfoCaptor.getValue().getContentDescription());
574         // Verify client 2 gets TextView1's siblings and its parent as prefetched nodes.
575         verify(mMockClientCallback2, times(1))
576                 .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
577                         eq(mMockClient2InteractionId));
578         List<AccessibilityNodeInfo> prefetchedNode = mPrefetchInfoListCaptor.getValue();
579         assertEquals(4, prefetchedNode.size());
580         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, prefetchedNode.get(0).getContentDescription());
581         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNode.get(1).getContentDescription());
582         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNode.get(2).getContentDescription());
583         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNode.get(3).getContentDescription());
584     }
585 
586     /**
587      * Tests a request for 4 nodes using depth first traversal.
588      *     Request 1: Request the root node.
589      *     Request 2: When TextView4 is prefetched, send a request for the root node. Depth first
590      *     traversal completes here.
591      * Out of the 5 descendants, the root frame's 3rd child (TextView3) should not be prefetched,
592      * since this was not reached by the df-traversal.
593      *
594      *   Layout
595      *                        mRootFrameLayout
596      *                /         |        |              \
597      *      mTextView1 mTextView2 mChildFrameLayout  *mTextView3*
598      *                                    |
599      *                                mTextView4
600      * @throws RemoteException
601      */
602     @Test
testFindRootView_depthFirstStrategy_shouldReturnRootNodeAndPrefetch4Descendants()603     public void testFindRootView_depthFirstStrategy_shouldReturnRootNodeAndPrefetch4Descendants()
604             throws RemoteException {
605         mTextView4.setAccessibilityDelegate(new View.AccessibilityDelegate(){
606             @Override
607             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
608                 super.onInitializeAccessibilityNodeInfo(host, info);
609                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
610                         mRootFrameLayout.getAccessibilityViewId(),
611                         AccessibilityNodeProvider.HOST_VIEW_ID);
612                 // This request is satisfied by first request.
613                 sendNodeRequestToController(nodeId, mMockClientCallback2,
614                         mMockClient2InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
615 
616             }
617         });
618         // Request for our RootFrameLayout.
619         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
620                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST);
621         mInstrumentation.waitForIdleSync();
622 
623         // Verify we get RootFrameLayout.
624         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
625                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
626         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
627         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
628 
629         verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
630                 mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
631         // Prefetch all the descendants besides TextView3.
632         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
633         assertEquals(4, prefetchedNodes.size());
634         assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
635         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
636         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
637         assertEquals(TEXT_VIEW_4_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
638     }
639 
640     /**
641      * Tests a request for 4 nodes using breadth first traversal.
642      *     Request 1: Request the root node
643      *     Request 2: When TextView3 is prefetched, send a request for the root node. Breadth first
644      *     traversal completes here.
645      * Out of the 5 descendants, the child frame's child (TextView4) should not be prefetched, since
646      * this was not reached by the bf-traversal.
647      *   Layout
648      *                        mRootFrameLayout
649      *                /         |        |              \
650      *      mTextView1 mTextView2 mChildFrameLayout  *mTextView3*
651      *                                    |
652      *                                *mTextView4*
653      * @throws RemoteException
654      */
655     @Test
testFindRootView_breadthFirstStrategy_shouldReturnRootNodeAndPrefetch4Descendants()656     public void testFindRootView_breadthFirstStrategy_shouldReturnRootNodeAndPrefetch4Descendants()
657             throws RemoteException {
658 
659         mTextView3.setAccessibilityDelegate(new View.AccessibilityDelegate(){
660             @Override
661             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
662                 super.onInitializeAccessibilityNodeInfo(host, info);
663                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
664                         mRootFrameLayout.getAccessibilityViewId(),
665                         AccessibilityNodeProvider.HOST_VIEW_ID);
666 
667                 // This request is satisfied by first request.
668                 sendNodeRequestToController(nodeId, mMockClientCallback2,
669                         mMockClient2InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
670             }
671         });
672         // Request for our RootFrameLayout.
673         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
674                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST);
675         mInstrumentation.waitForIdleSync();
676 
677         // Verify we get RootFrameLayout.
678         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
679                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
680         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
681         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
682 
683         verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
684                 mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
685         // Prefetch all the descendants besides TextView4.
686         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
687         assertEquals(4, prefetchedNodes.size());
688         assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
689         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
690         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
691         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
692     }
693 
694     /**
695      * Tests a request that should not have prefetching interrupted.
696      *     Request 1: Client 1 requests the root node
697      *     Request 2: When the root node is initialized in
698      *     {@link TestFrameLayout#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)},
699      *     Client 2 requests TextView1's node
700      *
701      * Request 1 is not interrupted during prefetch, and its prefetched node satisfies Request 2.
702      *
703      * @throws RemoteException
704      */
705     @Test
testFindRootAndTextNodes_withNoInterruptStrategy_shouldSatisfySecondRequest()706     public void testFindRootAndTextNodes_withNoInterruptStrategy_shouldSatisfySecondRequest()
707             throws RemoteException {
708         mRootFrameLayout.setAccessibilityDelegate(new View.AccessibilityDelegate(){
709             @Override
710             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
711                 super.onInitializeAccessibilityNodeInfo(host, info);
712                 final long nodeId = AccessibilityNodeInfo.makeNodeId(
713                         mTextView1.getAccessibilityViewId(),
714                         AccessibilityNodeProvider.HOST_VIEW_ID);
715 
716                 // TextView1 is prefetched here after the RootFrameLayout is found. Now enqueue a
717                 // same-client request for RootFrameLayout.
718                 sendNodeRequestToController(nodeId, mMockClientCallback2,
719                         mMockClient2InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID);
720                 }
721         });
722 
723         // Client 1 request for RootFrameLayout.
724         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
725                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_HYBRID
726                         | FLAG_PREFETCH_UNINTERRUPTIBLE);
727 
728         mInstrumentation.waitForIdleSync();
729 
730         // When the controller returns the nodes, it clears the sent list. Check immediately since
731         // the captor will be cleared.
732         doAnswer(invocation -> {
733             List<AccessibilityNodeInfo> nodes = invocation.getArgument(0);
734             assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, nodes.get(0).getContentDescription());
735             assertEquals(TEXT_VIEW_2_DESCRIPTION, nodes.get(1).getContentDescription());
736             assertEquals(CHILD_FRAME_DESCRIPTION, nodes.get(2).getContentDescription());
737             assertEquals(TEXT_VIEW_3_DESCRIPTION, nodes.get(3).getContentDescription());
738             assertEquals(TEXT_VIEW_4_DESCRIPTION, nodes.get(4).getContentDescription());
739             return null;
740         }).when(mMockClientCallback1).setFindAccessibilityNodeInfosResult(
741                 anyList(), eq(mMockClient1InteractionId));
742 
743         verify(mMockClientCallback1, never())
744                 .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt());
745 
746         // Verify client 2 gets TextView1.
747         verify(mMockClientCallback2).setFindAccessibilityNodeInfoResult(
748                 mFindInfoCaptor.capture(), eq(mMockClient2InteractionId));
749         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
750         assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription());
751     }
752 
753     /**
754      * Tests a request for root node where a virtual hierarchy is prefetched.
755      *
756      *   Layout
757      *                        mRootFrameLayout
758      *                /         |        |              \
759      *      mTextView1 mTextView2 mChildFrameLayout  *mTextView3*
760      *                                    |
761      *                                *mTextView4*
762      *                                  |         \
763      *                          virtual view 1   virtual view 2
764      *                                 |
765      *                             virtual view 3
766      * @throws RemoteException
767      */
768     @Test
testFindRootView_withVirtualView()769     public void testFindRootView_withVirtualView()
770             throws RemoteException {
771         mTextView4.setAccessibilityDelegate(new View.AccessibilityDelegate(){
772             @Override
773             public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
774                 return new AccessibilityNodeProvider() {
775                     @Override
776                     public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
777                         if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
778                             AccessibilityNodeInfo node = new AccessibilityNodeInfo(host);
779                             node.addChild(host, 1);
780                             node.addChild(host, 2);
781                             node.setContentDescription(TEXT_VIEW_4_DESCRIPTION);
782                             return node;
783                         } else if (virtualViewId == 1) {
784                             AccessibilityNodeInfo node = new AccessibilityNodeInfo(
785                                     host, virtualViewId);
786                             node.setParent(host);
787                             node.setContentDescription(VIRTUAL_VIEW_1_DESCRIPTION);
788                             node.addChild(host, 3);
789                             return node;
790                         } else if (virtualViewId == 2 || virtualViewId == 3) {
791                             AccessibilityNodeInfo node = new AccessibilityNodeInfo(
792                                     host, virtualViewId);
793                             node.setParent(host);
794                             node.setContentDescription(virtualViewId == 2
795                                     ? VIRTUAL_VIEW_2_DESCRIPTION
796                                     : VIRTUAL_VIEW_3_DESCRIPTION);
797                             return node;
798                         }
799                         return null;
800                     }
801                 };
802             }
803         });
804         // Request for our RootFrameLayout.
805         sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
806                 mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST);
807         mInstrumentation.waitForIdleSync();
808 
809         // Verify we get RootFrameLayout.
810         verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
811                 mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
812         AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
813         assertEquals(ROOT_FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
814 
815         verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
816                 mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
817 
818         List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
819         assertEquals(8, prefetchedNodes.size());
820         assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
821         assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
822         assertEquals(CHILD_FRAME_DESCRIPTION, prefetchedNodes.get(2).getContentDescription());
823         assertEquals(TEXT_VIEW_4_DESCRIPTION, prefetchedNodes.get(3).getContentDescription());
824 
825         assertEquals(VIRTUAL_VIEW_1_DESCRIPTION, prefetchedNodes.get(4).getContentDescription());
826         assertEquals(VIRTUAL_VIEW_3_DESCRIPTION, prefetchedNodes.get(5).getContentDescription());
827 
828         assertEquals(VIRTUAL_VIEW_2_DESCRIPTION, prefetchedNodes.get(6).getContentDescription());
829         assertEquals(TEXT_VIEW_3_DESCRIPTION, prefetchedNodes.get(7).getContentDescription());
830 
831 
832     }
833 
sendNodeRequestToController(long requestedNodeId, IAccessibilityInteractionConnectionCallback callback, int interactionId, int prefetchFlags)834     private void sendNodeRequestToController(long requestedNodeId,
835             IAccessibilityInteractionConnectionCallback callback, int interactionId,
836             int prefetchFlags) {
837         final int processAndThreadId = callback == mMockClientCallback1
838                 ? MOCK_CLIENT_1_THREAD_AND_PROCESS_ID
839                 : MOCK_CLIENT_2_THREAD_AND_PROCESS_ID;
840 
841         mAccessibilityInteractionController.findAccessibilityNodeInfoByAccessibilityIdClientThread(
842                 requestedNodeId,
843                 null, interactionId,
844                 callback, prefetchFlags,
845                 processAndThreadId,
846                 processAndThreadId, null, null, null);
847 
848     }
849 
850     private class TestFrameLayout extends FrameLayout {
851         private int mA11yId;
852         private String mContentDescription;
853         ArrayList<View> mChildren;
854 
TestFrameLayout(Context context, int a11yId, String contentDescription, ArrayList<View> children)855         TestFrameLayout(Context context, int a11yId, String contentDescription,
856                 ArrayList<View> children) {
857             super(context);
858             mA11yId = a11yId;
859             mContentDescription = contentDescription;
860             mChildren = children;
861         }
862 
863         @Override
getWindowVisibility()864         public int getWindowVisibility() {
865             // We aren't attached to a window so let's pretend
866             return VISIBLE;
867         }
868 
869         @Override
isShown()870         public boolean isShown() {
871             // Controller check
872             return true;
873         }
874 
875         @Override
getAccessibilityViewId()876         public int getAccessibilityViewId() {
877             // static id doesn't reset after tests so return the same one
878             return mA11yId;
879         }
880 
881         @Override
addChildrenForAccessibility(ArrayList<View> outChildren)882         public void addChildrenForAccessibility(ArrayList<View> outChildren) {
883             // ViewGroup#addChildrenForAccessbility sorting logic will switch these two
884             for (View view : mChildren) {
885                 outChildren.add(view);
886             }
887         }
888 
889         @Override
includeForAccessibility()890         public boolean includeForAccessibility() {
891             return true;
892         }
893 
894         @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)895         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
896             super.onInitializeAccessibilityNodeInfo(info);
897             info.setContentDescription(mContentDescription);
898         }
899     }
900 
901     private class TestTextView extends TextView {
902         private int mA11yId;
903         private String mContentDescription;
TestTextView(Context context, int a11yId, String contentDescription)904         TestTextView(Context context, int a11yId, String contentDescription) {
905             super(context);
906             mA11yId = a11yId;
907             mContentDescription = contentDescription;
908         }
909 
910         @Override
getWindowVisibility()911         public int getWindowVisibility() {
912             return VISIBLE;
913         }
914 
915         @Override
isShown()916         public boolean isShown() {
917             return true;
918         }
919 
920         @Override
getAccessibilityViewId()921         public int getAccessibilityViewId() {
922             return mA11yId;
923         }
924 
925         @Override
includeForAccessibility()926         public boolean includeForAccessibility() {
927             return true;
928         }
929 
930         @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)931         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
932             super.onInitializeAccessibilityNodeInfo(info);
933             info.setContentDescription(mContentDescription);
934         }
935     }
936 }
937