• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.accessibilityservice.cts;
18 
19 import static android.Manifest.permission.POST_NOTIFICATIONS;
20 import static android.accessibility.cts.common.InstrumentedAccessibilityService.TIMEOUT_SERVICE_ENABLE;
21 import static android.accessibility.cts.common.InstrumentedAccessibilityService.enableService;
22 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
23 import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
24 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventType;
25 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithAction;
26 import static android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils.filterForEventTypeWithResource;
27 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.findWindowByTitle;
28 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.getActivityTitle;
29 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
30 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
31 import static android.accessibilityservice.cts.utils.RunOnMainUtils.getOnMain;
32 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
33 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
34 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
35 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP;
36 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_IN_DIRECTION;
37 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP;
38 
39 import static com.google.common.truth.Truth.assertThat;
40 
41 import static org.junit.Assert.assertEquals;
42 import static org.junit.Assert.assertFalse;
43 import static org.junit.Assert.assertNotNull;
44 import static org.junit.Assert.assertThrows;
45 import static org.junit.Assert.assertTrue;
46 import static org.junit.Assert.fail;
47 import static org.mockito.ArgumentMatchers.any;
48 import static org.mockito.ArgumentMatchers.argThat;
49 import static org.mockito.ArgumentMatchers.eq;
50 import static org.mockito.Mockito.inOrder;
51 import static org.mockito.Mockito.mock;
52 import static org.mockito.Mockito.timeout;
53 import static org.mockito.Mockito.verify;
54 
55 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
56 import android.accessibility.cts.common.InstrumentedAccessibilityService;
57 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
58 import android.accessibility.cts.common.ShellCommandBuilder;
59 import android.accessibilityservice.AccessibilityServiceInfo;
60 import android.accessibilityservice.MagnificationConfig;
61 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity;
62 import android.app.Activity;
63 import android.app.AlertDialog;
64 import android.app.Instrumentation;
65 import android.app.Notification;
66 import android.app.NotificationChannel;
67 import android.app.NotificationManager;
68 import android.app.PendingIntent;
69 import android.app.Service;
70 import android.app.UiAutomation;
71 import android.appwidget.AppWidgetHost;
72 import android.appwidget.AppWidgetManager;
73 import android.appwidget.AppWidgetProviderInfo;
74 import android.content.ComponentName;
75 import android.content.Context;
76 import android.content.Intent;
77 import android.content.pm.PackageManager;
78 import android.content.res.Configuration;
79 import android.content.res.Resources;
80 import android.graphics.Rect;
81 import android.graphics.Region;
82 import android.os.Parcel;
83 import android.os.Process;
84 import android.os.SystemClock;
85 import android.platform.test.annotations.AppModeFull;
86 import android.platform.test.annotations.AsbSecurityTest;
87 import android.platform.test.annotations.FlakyTest;
88 import android.platform.test.annotations.Presubmit;
89 import android.provider.Settings;
90 import android.text.TextUtils;
91 import android.util.Log;
92 import android.view.InputDevice;
93 import android.view.MotionEvent;
94 import android.view.TouchDelegate;
95 import android.view.View;
96 import android.view.Window;
97 import android.view.WindowManager;
98 import android.view.accessibility.AccessibilityEvent;
99 import android.view.accessibility.AccessibilityManager;
100 import android.view.accessibility.AccessibilityNodeInfo;
101 import android.view.accessibility.AccessibilityWindowInfo;
102 import android.widget.Button;
103 import android.widget.EditText;
104 import android.widget.ListView;
105 
106 import androidx.test.InstrumentationRegistry;
107 import androidx.test.filters.MediumTest;
108 import androidx.test.rule.ActivityTestRule;
109 import androidx.test.runner.AndroidJUnit4;
110 
111 import com.android.compatibility.common.util.ApiTest;
112 import com.android.compatibility.common.util.CddTest;
113 import com.android.compatibility.common.util.CtsMouseUtil;
114 import com.android.compatibility.common.util.ShellUtils;
115 import com.android.compatibility.common.util.TestUtils;
116 import com.android.sts.common.util.StsExtraBusinessLogicTestCase;
117 
118 import org.junit.After;
119 import org.junit.AfterClass;
120 import org.junit.Before;
121 import org.junit.BeforeClass;
122 import org.junit.Rule;
123 import org.junit.Test;
124 import org.junit.rules.RuleChain;
125 import org.junit.runner.RunWith;
126 
127 import java.time.Duration;
128 import java.util.ArrayDeque;
129 import java.util.Deque;
130 import java.util.Iterator;
131 import java.util.List;
132 import java.util.concurrent.TimeoutException;
133 import java.util.concurrent.atomic.AtomicBoolean;
134 import java.util.concurrent.atomic.AtomicInteger;
135 import java.util.concurrent.atomic.AtomicReference;
136 
137 /**
138  * This class performs end-to-end testing of the accessibility feature by
139  * creating an {@link Activity} and poking around so {@link AccessibilityEvent}s
140  * are generated and their correct dispatch verified.
141  */
142 @RunWith(AndroidJUnit4.class)
143 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
144 @Presubmit
145 public class AccessibilityEndToEndTest extends StsExtraBusinessLogicTestCase {
146 
147     private static final String LOG_TAG = "AccessibilityEndToEndTest";
148 
149     private static final String GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND =
150             "appwidget grantbind --package android.accessibilityservice.cts --user ";
151 
152     private static final String REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND =
153             "appwidget revokebind --package android.accessibilityservice.cts --user ";
154 
155     private static final String APP_WIDGET_PROVIDER_PACKAGE = "foo.bar.baz";
156 
157     private static final int TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS = 1000;
158 
159     private static Instrumentation sInstrumentation;
160     private static UiAutomation sUiAutomation;
161 
162     private AccessibilityEndToEndActivity mActivity;
163 
164     private ActivityTestRule<AccessibilityEndToEndActivity> mActivityRule =
165             new ActivityTestRule<>(AccessibilityEndToEndActivity.class, false, false);
166 
167     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
168             new AccessibilityDumpOnFailureRule();
169 
170     private final InstrumentedAccessibilityServiceTestRule<
171             StubMotionInterceptingAccessibilityService>
172             mMotionInterceptingServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
173             StubMotionInterceptingAccessibilityService.class, false);
174 
175     @Rule
176     public final RuleChain mRuleChain = RuleChain
177             .outerRule(mActivityRule)
178             .around(mMotionInterceptingServiceRule)
179             .around(mDumpOnFailureRule);
180 
181     @BeforeClass
oneTimeSetup()182     public static void oneTimeSetup() throws Exception {
183         sInstrumentation = InstrumentationRegistry.getInstrumentation();
184         sUiAutomation = sInstrumentation.getUiAutomation();
185     }
186 
187     @AfterClass
postTestTearDown()188     public static void postTestTearDown() {
189         sUiAutomation.destroy();
190     }
191 
192     @Before
setUp()193     public void setUp() throws Exception {
194         sUiAutomation.adoptShellPermissionIdentity(POST_NOTIFICATIONS);
195         mActivity = launchActivityAndWaitForItToBeOnscreen(
196                 sInstrumentation, sUiAutomation, mActivityRule);
197     }
198 
199     @After
tearDown()200     public void tearDown() throws Exception {
201         sUiAutomation.dropShellPermissionIdentity();
202     }
203 
204     @MediumTest
205     @Test
206     @ApiTest(apis = {"android.view.View#setSelected",
207             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewSelectedAccessibilityEvent()208     public void testTypeViewSelectedAccessibilityEvent() throws Throwable {
209         // create and populate the expected event
210         final AccessibilityEvent expected = AccessibilityEvent.obtain();
211         expected.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
212         expected.setClassName(ListView.class.getName());
213         expected.setPackageName(mActivity.getPackageName());
214         expected.setDisplayId(mActivity.getDisplayId());
215         expected.getText().add(mActivity.getString(R.string.second_list_item));
216         expected.setItemCount(2);
217         expected.setCurrentItemIndex(1);
218         expected.setEnabled(true);
219         expected.setScrollable(false);
220         expected.setFromIndex(0);
221         expected.setToIndex(1);
222 
223         final ListView listView = (ListView) mActivity.findViewById(R.id.listview);
224 
225         AccessibilityEvent awaitedEvent =
226             sUiAutomation.executeAndWaitForEvent(
227                 new Runnable() {
228             @Override
229             public void run() {
230                 // trigger the event
231                 mActivity.runOnUiThread(new Runnable() {
232                     @Override
233                     public void run() {
234                         listView.setSelection(1);
235                     }
236                 });
237             }},
238             new UiAutomation.AccessibilityEventFilter() {
239                 // check the received event
240                 @Override
241                 public boolean accept(AccessibilityEvent event) {
242                     return equalsAccessiblityEvent(event, expected);
243                 }
244             },
245                     DEFAULT_TIMEOUT_MS);
246         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
247     }
248 
249     @MediumTest
250     @Test
251     @ApiTest(apis = {"android.view.View#performClick",
252             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewClickedAccessibilityEvent()253     public void testTypeViewClickedAccessibilityEvent() throws Throwable {
254         // create and populate the expected event
255         final AccessibilityEvent expected = AccessibilityEvent.obtain();
256         expected.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED);
257         expected.setClassName(Button.class.getName());
258         expected.setPackageName(mActivity.getPackageName());
259         expected.setDisplayId(mActivity.getDisplayId());
260         expected.getText().add(mActivity.getString(R.string.button_title));
261         expected.setEnabled(true);
262 
263         final Button button = (Button) mActivity.findViewById(R.id.button);
264 
265         AccessibilityEvent awaitedEvent =
266             sUiAutomation.executeAndWaitForEvent(
267                 new Runnable() {
268             @Override
269             public void run() {
270                 // trigger the event
271                 mActivity.runOnUiThread(new Runnable() {
272                     @Override
273                     public void run() {
274                         button.performClick();
275                     }
276                 });
277             }},
278             new UiAutomation.AccessibilityEventFilter() {
279                 // check the received event
280                 @Override
281                 public boolean accept(AccessibilityEvent event) {
282                     return equalsAccessiblityEvent(event, expected);
283                 }
284             },
285                     DEFAULT_TIMEOUT_MS);
286         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
287     }
288 
289     @MediumTest
290     @Test
291     @ApiTest(apis = {"android.view.View#performLongClick",
292             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewLongClickedAccessibilityEvent()293     public void testTypeViewLongClickedAccessibilityEvent() throws Throwable {
294         // create and populate the expected event
295         final AccessibilityEvent expected = AccessibilityEvent.obtain();
296         expected.setEventType(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
297         expected.setClassName(Button.class.getName());
298         expected.setPackageName(mActivity.getPackageName());
299         expected.setDisplayId(mActivity.getDisplayId());
300         expected.getText().add(mActivity.getString(R.string.button_title));
301         expected.setEnabled(true);
302 
303         final Button button = (Button) mActivity.findViewById(R.id.button);
304 
305         AccessibilityEvent awaitedEvent =
306             sUiAutomation.executeAndWaitForEvent(
307                 new Runnable() {
308             @Override
309             public void run() {
310                 // trigger the event
311                 mActivity.runOnUiThread(new Runnable() {
312                     @Override
313                     public void run() {
314                         button.performLongClick();
315                     }
316                 });
317             }},
318             new UiAutomation.AccessibilityEventFilter() {
319                 // check the received event
320                 @Override
321                 public boolean accept(AccessibilityEvent event) {
322                     return equalsAccessiblityEvent(event, expected);
323                 }
324             },
325                     DEFAULT_TIMEOUT_MS);
326         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
327     }
328 
329     @MediumTest
330     @Test
331     @ApiTest(apis = {"android.view.View#requestFocus",
332             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewFocusedAccessibilityEvent()333     public void testTypeViewFocusedAccessibilityEvent() throws Throwable {
334         // create and populate the expected event
335         final AccessibilityEvent expected = AccessibilityEvent.obtain();
336         expected.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
337         expected.setClassName(Button.class.getName());
338         expected.setPackageName(mActivity.getPackageName());
339         expected.setDisplayId(mActivity.getDisplayId());
340         expected.getText().add(mActivity.getString(R.string.button_title));
341         expected.setItemCount(6);
342         expected.setCurrentItemIndex(4);
343         expected.setEnabled(true);
344 
345         final Button button = (Button) mActivity.findViewById(R.id.buttonWithTooltip);
346 
347         AccessibilityEvent awaitedEvent =
348             sUiAutomation.executeAndWaitForEvent(
349                     () -> mActivity.runOnUiThread(button::requestFocus),
350                     (event) -> equalsAccessiblityEvent(event, expected),
351                     DEFAULT_TIMEOUT_MS);
352         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
353     }
354 
355     @MediumTest
356     @Test
357     @ApiTest(apis = {"android.text.Editable#replace",
358             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeViewTextChangedAccessibilityEvent()359     public void testTypeViewTextChangedAccessibilityEvent() throws Throwable {
360         // focus the edit text
361         final EditText editText = (EditText) mActivity.findViewById(R.id.edittext);
362 
363         AccessibilityEvent awaitedFocusEvent =
364             sUiAutomation.executeAndWaitForEvent(
365                 new Runnable() {
366             @Override
367             public void run() {
368                 // trigger the event
369                 mActivity.runOnUiThread(new Runnable() {
370                     @Override
371                     public void run() {
372                         editText.requestFocus();
373                     }
374                 });
375             }},
376             new UiAutomation.AccessibilityEventFilter() {
377                 // check the received event
378                 @Override
379                 public boolean accept(AccessibilityEvent event) {
380                     return event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED;
381                 }
382             },
383                     DEFAULT_TIMEOUT_MS);
384         assertNotNull("Did not receive expected focuss event.", awaitedFocusEvent);
385 
386         final String beforeText = mActivity.getString(R.string.text_input_blah);
387         final String newText = mActivity.getString(R.string.text_input_blah_blah);
388         final String afterText = beforeText.substring(0, 3) + newText;
389 
390         // create and populate the expected event
391         final AccessibilityEvent expected = AccessibilityEvent.obtain();
392         expected.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
393         expected.setClassName(EditText.class.getName());
394         expected.setPackageName(mActivity.getPackageName());
395         expected.setDisplayId(mActivity.getDisplayId());
396         expected.getText().add(afterText);
397         expected.setBeforeText(beforeText);
398         expected.setFromIndex(3);
399         expected.setAddedCount(9);
400         expected.setRemovedCount(1);
401         expected.setEnabled(true);
402 
403         AccessibilityEvent awaitedTextChangeEvent =
404             sUiAutomation.executeAndWaitForEvent(
405                 new Runnable() {
406             @Override
407             public void run() {
408                 // trigger the event
409                 mActivity.runOnUiThread(new Runnable() {
410                     @Override
411                     public void run() {
412                         editText.getEditableText().replace(3, 4, newText);
413                     }
414                 });
415             }},
416             new UiAutomation.AccessibilityEventFilter() {
417                 // check the received event
418                 @Override
419                 public boolean accept(AccessibilityEvent event) {
420                     return equalsAccessiblityEvent(event, expected);
421                 }
422             },
423                     DEFAULT_TIMEOUT_MS);
424         assertNotNull("Did not receive expected event: " + expected, awaitedTextChangeEvent);
425     }
426 
427     @MediumTest
428     @Test
429     @ApiTest(apis = {"android.view.ViewManager#addView",
430             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeWindowStateChangedAccessibilityEvent()431     public void testTypeWindowStateChangedAccessibilityEvent() throws Throwable {
432         // create and populate the expected event
433         final AccessibilityEvent expected = AccessibilityEvent.obtain();
434         expected.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
435         expected.setClassName(AlertDialog.class.getName());
436         expected.setPackageName(mActivity.getPackageName());
437         expected.setDisplayId(mActivity.getDisplayId());
438         expected.getText().add(mActivity.getString(R.string.alert_title));
439         expected.getText().add(mActivity.getString(R.string.alert_message));
440         expected.setEnabled(true);
441 
442         AccessibilityEvent awaitedEvent =
443             sUiAutomation.executeAndWaitForEvent(
444                 new Runnable() {
445             @Override
446             public void run() {
447                 // trigger the event
448                 mActivity.runOnUiThread(new Runnable() {
449                     @Override
450                     public void run() {
451                         (new AlertDialog.Builder(mActivity).setTitle(R.string.alert_title)
452                                 .setMessage(R.string.alert_message)).create().show();
453                     }
454                 });
455             }},
456             new UiAutomation.AccessibilityEventFilter() {
457                 // check the received event
458                 @Override
459                 public boolean accept(AccessibilityEvent event) {
460                     return equalsAccessiblityEvent(event, expected);
461                 }
462             },
463                     DEFAULT_TIMEOUT_MS);
464         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
465     }
466 
467     @MediumTest
468     @Test
469     @ApiTest(apis = {"android.app.Activity#finish",
470             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeWindowsChangedAccessibilityEvent()471     public void testTypeWindowsChangedAccessibilityEvent() throws Throwable {
472         // create and populate the expected event
473         final AccessibilityEvent expected = AccessibilityEvent.obtain();
474         expected.setEventType(AccessibilityEvent.TYPE_WINDOWS_CHANGED);
475         expected.setDisplayId(mActivity.getDisplayId());
476 
477         // check the received event
478         AccessibilityEvent awaitedEvent =
479             sUiAutomation.executeAndWaitForEvent(
480                     () -> mActivity.runOnUiThread(() -> mActivity.finish()),
481                     event -> event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED
482                             && equalsAccessiblityEvent(event, expected),
483                     DEFAULT_TIMEOUT_MS);
484         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
485     }
486 
487     @MediumTest
488     @AppModeFull
489     @SuppressWarnings("deprecation")
490     @Test
491     @ApiTest(apis = {"android.app.NotificationManager#notify",
492             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTypeNotificationStateChangedAccessibilityEvent()493     public void testTypeNotificationStateChangedAccessibilityEvent() throws Throwable {
494         // No notification UI on televisions.
495         if ((mActivity.getResources().getConfiguration().uiMode
496                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
497             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
498                     " - No notification UI on televisions.");
499             return;
500         }
501         PackageManager pm = sInstrumentation.getTargetContext().getPackageManager();
502         if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
503             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
504                     " - Watches have different notification system.");
505             return;
506         }
507         if (pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
508             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
509                     " - Automotive handle notifications differently.");
510             return;
511         }
512 
513         String message = mActivity.getString(R.string.notification_message);
514 
515         final NotificationManager notificationManager =
516                 (NotificationManager) mActivity.getSystemService(Service.NOTIFICATION_SERVICE);
517         final NotificationChannel channel =
518                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
519         try {
520             // create the notification to send
521             channel.enableVibration(true);
522             channel.enableLights(true);
523             channel.setBypassDnd(true);
524             notificationManager.createNotificationChannel(channel);
525             final int notificationId = 1;
526             final Notification notification =
527                     new Notification.Builder(mActivity, channel.getId())
528                             .setSmallIcon(android.R.drawable.stat_notify_call_mute)
529                             .setContentIntent(PendingIntent.getActivity(mActivity, 0,
530                                     new Intent(),
531             PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE))
532                             .setTicker(message)
533                             .setContentTitle("")
534                             .setContentText("")
535                             .setPriority(Notification.PRIORITY_MAX)
536                             // Mark the notification as "interruptive" by specifying a vibration
537                             // pattern. This ensures it's announced properly on watch-type devices.
538                             .setVibrate(new long[]{})
539                             .build();
540 
541             // create and populate the expected event
542             final AccessibilityEvent expected = AccessibilityEvent.obtain();
543             expected.setEventType(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
544             expected.setClassName(Notification.class.getName());
545             expected.setPackageName(mActivity.getPackageName());
546             expected.getText().add(message);
547             expected.setParcelableData(notification);
548 
549             AccessibilityEvent awaitedEvent =
550                     sUiAutomation.executeAndWaitForEvent(
551                             new Runnable() {
552                                 @Override
553                                 public void run() {
554                                     // trigger the event
555                                     mActivity.runOnUiThread(new Runnable() {
556                                         @Override
557                                         public void run() {
558                                             // trigger the event
559                                             notificationManager
560                                                     .notify(notificationId, notification);
561                                             mActivity.finish();
562                                         }
563                                     });
564                                 }
565                             },
566                             new UiAutomation.AccessibilityEventFilter() {
567                                 // check the received event
568                                 @Override
569                                 public boolean accept(AccessibilityEvent event) {
570                                     return equalsAccessiblityEvent(event, expected);
571                                 }
572                             },
573                             DEFAULT_TIMEOUT_MS);
574             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
575         } finally {
576             notificationManager.deleteNotificationChannel(channel.getId());
577         }
578     }
579 
580     @MediumTest
581     @Test
582     @ApiTest(apis = {"android.view.accessibility.AccessibilityManager#interrupt"})
testInterrupt_notifiesService()583     public void testInterrupt_notifiesService() {
584         sInstrumentation
585                 .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
586         InstrumentedAccessibilityService service =
587                 enableService(InstrumentedAccessibilityService.class);
588 
589         try {
590             assertFalse(service.wasOnInterruptCalled());
591 
592             mActivity.runOnUiThread(() -> {
593                 AccessibilityManager accessibilityManager = (AccessibilityManager) mActivity
594                         .getSystemService(Service.ACCESSIBILITY_SERVICE);
595                 accessibilityManager.interrupt();
596             });
597 
598             Object waitObject = service.getInterruptWaitObject();
599             synchronized (waitObject) {
600                 if (!service.wasOnInterruptCalled()) {
601                     try {
602                         waitObject.wait(DEFAULT_TIMEOUT_MS);
603                     } catch (InterruptedException e) {
604                         // Do nothing
605                     }
606                 }
607             }
608             assertTrue(service.wasOnInterruptCalled());
609         } finally {
610             service.disableSelfAndRemove();
611         }
612     }
613 
614     @MediumTest
615     @Test
616     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getPackageName"})
testPackageNameCannotBeFaked()617     public void testPackageNameCannotBeFaked() {
618         mActivity.runOnUiThread(() -> {
619             // Set the activity to report fake package for events and nodes
620             mActivity.setReportedPackageName("foo.bar.baz");
621 
622             // Make sure node package cannot be faked
623             AccessibilityNodeInfo root = sUiAutomation
624                     .getRootInActiveWindow();
625             assertPackageName(root, mActivity.getPackageName());
626         });
627 
628         // Make sure event package cannot be faked
629         try {
630             sUiAutomation.executeAndWaitForEvent(() ->
631                 sInstrumentation.runOnMainSync(() ->
632                     mActivity.findViewById(R.id.button).requestFocus())
633                 , (AccessibilityEvent event) ->
634                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
635                             && event.getPackageName().equals(mActivity.getPackageName())
636                 , DEFAULT_TIMEOUT_MS);
637         } catch (TimeoutException e) {
638             fail("Events from fake package should be fixed to use the correct package");
639         }
640     }
641 
642     @AppModeFull
643     @MediumTest
644     @Test
645     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getPackageName"})
testPackageNameCannotBeFakedAppWidget()646     public void testPackageNameCannotBeFakedAppWidget() throws Exception {
647         if (!hasAppWidgets()) {
648             return;
649         }
650 
651         sInstrumentation.runOnMainSync(() -> {
652             // Set the activity to report fake package for events and nodes
653             mActivity.setReportedPackageName(APP_WIDGET_PROVIDER_PACKAGE);
654 
655             // Make sure we cannot report nodes as if from the widget package
656             AccessibilityNodeInfo root = sUiAutomation
657                     .getRootInActiveWindow();
658             assertPackageName(root, mActivity.getPackageName());
659         });
660 
661         // Make sure we cannot send events as if from the widget package
662         try {
663             sUiAutomation.executeAndWaitForEvent(() ->
664                 sInstrumentation.runOnMainSync(() ->
665                     mActivity.findViewById(R.id.button).requestFocus())
666                 , (AccessibilityEvent event) ->
667                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
668                             && event.getPackageName().equals(mActivity.getPackageName())
669                 , DEFAULT_TIMEOUT_MS);
670         } catch (TimeoutException e) {
671             fail("Should not be able to send events from a widget package if no widget hosted");
672         }
673 
674         // Create a host and start listening.
675         final AppWidgetHost host = new AppWidgetHost(sInstrumentation.getTargetContext(), 0);
676         host.deleteHost();
677         host.startListening();
678 
679         // Well, app do not have this permission unless explicitly granted
680         // by the user. Now we will pretend for the user and grant it.
681         grantBindAppWidgetPermission();
682 
683         // Allocate an app widget id to bind.
684         final int appWidgetId = host.allocateAppWidgetId();
685         try {
686             // Grab a provider we defined to be bound.
687             final AppWidgetProviderInfo provider = getAppWidgetProviderInfo();
688 
689             // Bind the widget.
690             final boolean widgetBound = getAppWidgetManager().bindAppWidgetIdIfAllowed(
691                     appWidgetId, provider.getProfile(), provider.provider, null);
692             assertTrue(widgetBound);
693 
694             // Make sure the app can use the package of a widget it hosts
695             sInstrumentation.runOnMainSync(() -> {
696                 // Make sure we can report nodes as if from the widget package
697                 AccessibilityNodeInfo root = sUiAutomation
698                         .getRootInActiveWindow();
699                 assertPackageName(root, APP_WIDGET_PROVIDER_PACKAGE);
700             });
701 
702             // Make sure we can send events as if from the widget package
703             try {
704                 sUiAutomation.executeAndWaitForEvent(() ->
705                     sInstrumentation.runOnMainSync(() ->
706                         mActivity.findViewById(R.id.button).performClick())
707                     , (AccessibilityEvent event) ->
708                             event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED
709                                     && event.getPackageName().equals(APP_WIDGET_PROVIDER_PACKAGE)
710                     , DEFAULT_TIMEOUT_MS);
711             } catch (TimeoutException e) {
712                 fail("Should be able to send events from a widget package if widget hosted");
713             }
714         } finally {
715             // Clean up.
716             host.deleteAppWidgetId(appWidgetId);
717             host.deleteHost();
718             revokeBindAppWidgetPermission();
719         }
720     }
721 
722     @MediumTest
723     @Test
724     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#isHeading"})
testViewHeadingReportedToAccessibility()725     public void testViewHeadingReportedToAccessibility() throws Exception {
726         final EditText editText = (EditText) getOnMain(sInstrumentation,
727                 () -> mActivity.findViewById(R.id.edittext));
728         // Make sure the edittext was populated properly from xml
729         final boolean editTextIsHeading = getOnMain(sInstrumentation,
730                 editText::isAccessibilityHeading);
731         assertTrue("isAccessibilityHeading not populated properly from xml", editTextIsHeading);
732 
733         final AccessibilityNodeInfo editTextNode = sUiAutomation.getRootInActiveWindow()
734                 .findAccessibilityNodeInfosByViewId(
735                         "android.accessibilityservice.cts:id/edittext")
736                 .get(0);
737         assertTrue("isAccessibilityHeading not reported to accessibility",
738                 editTextNode.isHeading());
739 
740         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() ->
741                         editText.setAccessibilityHeading(false)),
742                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
743                 DEFAULT_TIMEOUT_MS);
744         editTextNode.refresh();
745         assertFalse("isAccessibilityHeading not reported to accessibility after update",
746                 editTextNode.isHeading());
747     }
748 
749     @MediumTest
750     @Test
751     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTooltipText"})
testTooltipTextReportedToAccessibility()752     public void testTooltipTextReportedToAccessibility() {
753         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
754                 .findAccessibilityNodeInfosByViewId(
755                         "android.accessibilityservice.cts:id/buttonWithTooltip")
756                 .get(0);
757         assertEquals("Tooltip text not reported to accessibility",
758                 sInstrumentation.getContext().getString(R.string.button_tooltip),
759                 buttonNode.getTooltipText());
760     }
761 
762     @MediumTest
763     @Test
testAccessibilityActionRetained()764     public void testAccessibilityActionRetained() throws Exception {
765         final AccessibilityNodeInfo sentInfo = new AccessibilityNodeInfo(new View(getContext()));
766         sentInfo.addAction(ACTION_SCROLL_IN_DIRECTION);
767         final Parcel parcel = Parcel.obtain();
768         sentInfo.writeToParcelNoRecycle(parcel, 0);
769         parcel.setDataPosition(0);
770         AccessibilityNodeInfo receivedInfo = AccessibilityNodeInfo.CREATOR.createFromParcel(parcel);
771 
772         assertThat(receivedInfo.getActionList()).contains(ACTION_SCROLL_IN_DIRECTION);
773 
774         parcel.recycle();
775     }
776 
777     @MediumTest
778     @Test
779     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getActionList"})
testTooltipTextActionsReportedToAccessibility()780     public void testTooltipTextActionsReportedToAccessibility() throws Exception {
781         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
782                 .findAccessibilityNodeInfosByViewId(
783                         "android.accessibilityservice.cts:id/buttonWithTooltip")
784                 .get(0);
785         assertFalse(hasTooltipShowing(R.id.buttonWithTooltip));
786         assertThat(buttonNode.getActionList()).contains(ACTION_SHOW_TOOLTIP);
787         assertThat(buttonNode.getActionList()).doesNotContain(ACTION_HIDE_TOOLTIP);
788         sUiAutomation.executeAndWaitForEvent(
789                 () -> buttonNode.performAction(ACTION_SHOW_TOOLTIP.getId()),
790                 filterForEventTypeWithAction(
791                         AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
792                         ACTION_SHOW_TOOLTIP.getId()),
793                 DEFAULT_TIMEOUT_MS);
794 
795         // The button should now be showing the tooltip, so it should have the option to hide it.
796         buttonNode.refresh();
797         assertThat(buttonNode.getActionList()).contains(ACTION_HIDE_TOOLTIP);
798         assertThat(buttonNode.getActionList()).doesNotContain(ACTION_SHOW_TOOLTIP);
799         assertTrue(hasTooltipShowing(R.id.buttonWithTooltip));
800     }
801 
802     @MediumTest
803     @Test
804     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTraversalBefore"})
testTraversalBeforeReportedToAccessibility()805     public void testTraversalBeforeReportedToAccessibility() throws Exception {
806         final AccessibilityNodeInfo buttonNode = sUiAutomation.getRootInActiveWindow()
807                 .findAccessibilityNodeInfosByViewId(
808                         "android.accessibilityservice.cts:id/buttonWithTooltip")
809                 .get(0);
810         final AccessibilityNodeInfo beforeNode = buttonNode.getTraversalBefore();
811         assertThat(beforeNode).isNotNull();
812         assertThat(beforeNode.getViewIdResourceName()).isEqualTo(
813                 "android.accessibilityservice.cts:id/edittext");
814 
815         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
816                 () -> mActivity.findViewById(R.id.buttonWithTooltip)
817                         .setAccessibilityTraversalBefore(View.NO_ID)),
818                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
819                 DEFAULT_TIMEOUT_MS);
820 
821         buttonNode.refresh();
822         assertThat(buttonNode.getTraversalBefore()).isNull();
823     }
824 
825     @MediumTest
826     @Test
827     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTraversalAfter"})
testTraversalAfterReportedToAccessibility()828     public void testTraversalAfterReportedToAccessibility() throws Exception {
829         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
830                 .findAccessibilityNodeInfosByViewId(
831                         "android.accessibilityservice.cts:id/edittext")
832                 .get(0);
833         final AccessibilityNodeInfo afterNode = editNode.getTraversalAfter();
834         assertThat(afterNode).isNotNull();
835         assertThat(afterNode.getViewIdResourceName()).isEqualTo(
836                 "android.accessibilityservice.cts:id/buttonWithTooltip");
837 
838         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(
839                 () -> mActivity.findViewById(R.id.edittext)
840                         .setAccessibilityTraversalAfter(View.NO_ID)),
841                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
842                 DEFAULT_TIMEOUT_MS);
843 
844         editNode.refresh();
845         assertThat(editNode.getTraversalAfter()).isNull();
846     }
847 
848     @MediumTest
849     @Test
850     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getLabelFor"})
testLabelForReportedToAccessibility()851     public void testLabelForReportedToAccessibility() throws Exception {
852         sUiAutomation.executeAndWaitForEvent(() -> sInstrumentation.runOnMainSync(() -> mActivity
853                 .findViewById(R.id.edittext).setLabelFor(R.id.buttonWithTooltip)),
854                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
855                 DEFAULT_TIMEOUT_MS);
856         // TODO: b/78022650: This code should move above the executeAndWait event. It's here because
857         // the a11y cache doesn't get notified when labelFor changes, so the node with the
858         // labledBy isn't updated.
859         final AccessibilityNodeInfo editNode = sUiAutomation.getRootInActiveWindow()
860                 .findAccessibilityNodeInfosByViewId(
861                         "android.accessibilityservice.cts:id/edittext")
862                 .get(0);
863         editNode.refresh();
864         final AccessibilityNodeInfo labelForNode = editNode.getLabelFor();
865         assertThat(labelForNode).isNotNull();
866         // Labeled node should indicate that it is labeled by the other one
867         assertThat(labelForNode.getLabeledBy()).isEqualTo(editNode);
868     }
869 
870     @MediumTest
871     @Test
872     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#performAction"})
testA11yActionTriggerMotionEventActionOutside()873     public void testA11yActionTriggerMotionEventActionOutside() throws Exception {
874         final View.OnTouchListener listener = mock(View.OnTouchListener.class);
875         final AccessibilityNodeInfo button = sUiAutomation.getRootInActiveWindow()
876                 .findAccessibilityNodeInfosByViewId(
877                         "android.accessibilityservice.cts:id/button")
878                 .get(0);
879         final String title = sInstrumentation.getContext().getString(R.string.alert_title);
880 
881         // Add a dialog that is watching outside touch
882         sUiAutomation.executeAndWaitForEvent(
883                 () -> sInstrumentation.runOnMainSync(() -> {
884                             final AlertDialog dialog = new AlertDialog.Builder(mActivity)
885                                     .setTitle(R.string.alert_title)
886                                     .setMessage(R.string.alert_message)
887                                     .create();
888                             final Window window = dialog.getWindow();
889                             window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
890                                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
891                             window.getDecorView().setOnTouchListener(listener);
892                             window.setTitle(title);
893                             dialog.show();
894                     }),
895                 (event) -> {
896                     // Ensure the dialog is shown over the activity
897                     final AccessibilityWindowInfo dialog = findWindowByTitle(
898                             sUiAutomation, title);
899                     final AccessibilityWindowInfo activity = findWindowByTitle(
900                             sUiAutomation, getActivityTitle(sInstrumentation, mActivity));
901                     return (dialog != null && activity != null)
902                             && (dialog.getLayer() > activity.getLayer());
903                 }, DEFAULT_TIMEOUT_MS);
904 
905         // Perform an action and wait for an event
906         sUiAutomation.executeAndWaitForEvent(
907                 () -> button.performAction(AccessibilityNodeInfo.ACTION_CLICK),
908                 filterForEventTypeWithAction(
909                         AccessibilityEvent.TYPE_VIEW_CLICKED, AccessibilityNodeInfo.ACTION_CLICK),
910                 DEFAULT_TIMEOUT_MS);
911 
912         // Make sure the MotionEvent.ACTION_OUTSIDE is received.
913         verify(listener, timeout(DEFAULT_TIMEOUT_MS).atLeastOnce()).onTouch(any(View.class),
914                 argThat(event -> event.getActionMasked() == MotionEvent.ACTION_OUTSIDE));
915     }
916 
917     @MediumTest
918     @Test
919     @ApiTest(apis = {"android.view.accessibility.AccessibilityNodeInfo#getTouchDelegateInfo"})
testTouchDelegateInfoReportedToAccessibility()920     public void testTouchDelegateInfoReportedToAccessibility() {
921         final Button button = getOnMain(sInstrumentation, () -> mActivity.findViewById(
922                 R.id.button));
923         final View parent = (View) button.getParent();
924         final Rect rect = new Rect();
925         button.getHitRect(rect);
926         parent.setTouchDelegate(new TouchDelegate(rect, button));
927 
928         final AccessibilityNodeInfo nodeInfo = sUiAutomation.getRootInActiveWindow()
929                 .findAccessibilityNodeInfosByViewId(
930                         "android.accessibilityservice.cts:id/buttonLayout")
931                 .get(0);
932         AccessibilityNodeInfo.TouchDelegateInfo targetMapInfo =
933                 nodeInfo.getTouchDelegateInfo();
934         assertNotNull("Did not receive TouchDelegate target map", targetMapInfo);
935         assertEquals("Incorrect target map size", 1, targetMapInfo.getRegionCount());
936         assertEquals("Incorrect target map region", new Region(rect),
937                 targetMapInfo.getRegionAt(0));
938         final AccessibilityNodeInfo node = targetMapInfo.getTargetForRegion(
939                 targetMapInfo.getRegionAt(0));
940         assertEquals("Incorrect target map view",
941                 "android.accessibilityservice.cts:id/button",
942                 node.getViewIdResourceName());
943         node.recycle();
944     }
945 
946     @MediumTest
947     @Test
948     @ApiTest(apis = {"android.view.View#onHoverEvent",
949             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()950     public void testTouchDelegateWithEbtBetweenView_ReHoverDelegate_FocusTargetAgain()
951             throws Throwable {
952         mActivity.waitForEnterAnimationComplete();
953 
954         final Resources resources = sInstrumentation.getTargetContext().getResources();
955         final String buttonResourceName = resources.getResourceName(R.id.button);
956         final Button button = mActivity.findViewById(R.id.button);
957         final int[] buttonLocation = new int[2];
958         button.getLocationOnScreen(buttonLocation);
959         final int buttonX = button.getWidth() / 2;
960         final int buttonY = button.getHeight() / 2;
961         final int hoverY = buttonLocation[1] + buttonY;
962         final Button buttonWithTooltip = mActivity.findViewById(R.id.buttonWithTooltip);
963         final int[] buttonWithTooltipLocation = new int[2];
964         buttonWithTooltip.getLocationOnScreen(buttonWithTooltipLocation);
965         final int touchableSize = resources.getDimensionPixelSize(
966                 R.dimen.button_touchable_width_increment_amount);
967         final int hoverRight = buttonWithTooltipLocation[0] + touchableSize / 2;
968         final int hoverLeft = buttonLocation[0] + button.getWidth() + touchableSize / 2;
969         final int hoverMiddle = (hoverLeft + hoverRight) / 2;
970         final View.OnHoverListener listener = CtsMouseUtil.installHoverListener(button, false);
971         enableTouchExploration(true);
972 
973         try {
974             // common downTime for touch explorer injected events
975             final long downTime = SystemClock.uptimeMillis();
976             // hover through delegate, parent, 2nd view, parent and delegate again
977             sUiAutomation.executeAndWaitForEvent(
978                     () -> injectHoverEvent(downTime, false, hoverLeft, hoverY),
979                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
980                             buttonResourceName), DEFAULT_TIMEOUT_MS);
981             assertTrue(button.isHovered());
982             sUiAutomation.executeAndWaitForEvent(
983                     () -> {
984                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
985                         injectHoverEvent(downTime, true, hoverRight, hoverY);
986                         injectHoverEvent(downTime, true, hoverMiddle, hoverY);
987                         injectHoverEvent(downTime, true, hoverLeft, hoverY);
988                     },
989                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
990                             buttonResourceName), DEFAULT_TIMEOUT_MS);
991             // delegate target has a11y focus again
992             assertTrue(button.isHovered());
993 
994             CtsMouseUtil.clearHoverListener(button);
995             View.OnHoverListener verifier = inOrder(listener).verify(listener);
996             verifier.onHover(eq(button),
997                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
998             verifier.onHover(eq(button),
999                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
1000             verifier.onHover(eq(button),
1001                     matchHover(MotionEvent.ACTION_HOVER_MOVE, hoverMiddle, buttonY));
1002             verifier.onHover(eq(button),
1003                     matchHover(MotionEvent.ACTION_HOVER_EXIT, buttonX, buttonY));
1004             verifier.onHover(eq(button),
1005                     matchHover(MotionEvent.ACTION_HOVER_ENTER, buttonX, buttonY));
1006             verifier.onHover(eq(button),
1007                     matchHover(MotionEvent.ACTION_HOVER_MOVE, buttonX, buttonY));
1008         } catch (TimeoutException e) {
1009             fail("Accessibility events should be received as expected " + e.getMessage());
1010         } finally {
1011             enableTouchExploration(false);
1012         }
1013     }
1014 
1015     @MediumTest
1016     @Test
1017     @ApiTest(apis = {"android.view.View#onHoverEvent",
1018             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
1019     @FlakyTest
testTouchDelegateCoverParentWithEbt_HoverChildAndBack_FocusTargetAgain()1020     public void testTouchDelegateCoverParentWithEbt_HoverChildAndBack_FocusTargetAgain()
1021             throws Throwable {
1022         mActivity.waitForEnterAnimationComplete();
1023 
1024         final Resources resources = sInstrumentation.getTargetContext().getResources();
1025         final int touchableSize = resources.getDimensionPixelSize(
1026                 R.dimen.button_touchable_width_increment_amount);
1027         final String targetResourceName = resources.getResourceName(R.id.buttonDelegated);
1028         final View textView = mActivity.findViewById(R.id.delegateText);
1029         final Button target = mActivity.findViewById(R.id.buttonDelegated);
1030         int[] location = new int[2];
1031         textView.getLocationOnScreen(location);
1032         final int textX = location[0] + touchableSize/2;
1033         final int textY = location[1] + textView.getHeight() / 2;
1034         final int delegateX = location[0] - touchableSize/2;
1035         final int targetX = target.getWidth() / 2;
1036         final int targetY = target.getHeight() / 2;
1037         final View.OnHoverListener listener = CtsMouseUtil.installHoverListener(target, false);
1038         enableTouchExploration(true);
1039 
1040         try {
1041             final long downTime = SystemClock.uptimeMillis();
1042             // Like switch bar, it has a text view, a button and a delegate covers parent layout.
1043             // hover the delegate, text and delegate again.
1044             sUiAutomation.executeAndWaitForEvent(
1045                     () -> injectHoverEvent(downTime, false, delegateX, textY),
1046                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1047                            targetResourceName), DEFAULT_TIMEOUT_MS);
1048             assertTrue(target.isHovered());
1049             sUiAutomation.executeAndWaitForEvent(
1050                     () -> injectHoverEvent(downTime, true, textX, textY),
1051                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT,
1052                            targetResourceName), DEFAULT_TIMEOUT_MS);
1053             sUiAutomation.executeAndWaitForEvent(
1054                     () -> injectHoverEvent(downTime, true, delegateX, textY),
1055                     filterForEventTypeWithResource(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1056                            targetResourceName), DEFAULT_TIMEOUT_MS);
1057             assertTrue(target.isHovered());
1058 
1059             CtsMouseUtil.clearHoverListener(target);
1060             View.OnHoverListener verifier = inOrder(listener).verify(listener);
1061             verifier.onHover(eq(target),
1062                     matchHover(MotionEvent.ACTION_HOVER_ENTER, targetX, targetY));
1063             verifier.onHover(eq(target),
1064                     matchHover(MotionEvent.ACTION_HOVER_MOVE, targetX, targetY));
1065             verifier.onHover(eq(target),
1066                     matchHover(MotionEvent.ACTION_HOVER_MOVE, textX, textY));
1067             verifier.onHover(eq(target),
1068                     matchHover(MotionEvent.ACTION_HOVER_EXIT, targetX, targetY));
1069             verifier.onHover(eq(target),
1070                     matchHover(MotionEvent.ACTION_HOVER_ENTER, targetX, targetY));
1071             verifier.onHover(eq(target),
1072                     matchHover(MotionEvent.ACTION_HOVER_MOVE, targetX, targetY));
1073         } catch (TimeoutException e) {
1074             fail("Accessibility events should be received as expected " + e.getMessage());
1075         } finally {
1076             enableTouchExploration(false);
1077         }
1078     }
1079 
1080     @Test
1081     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_nodeMatchesViewProperty()1082     public void testAccessibilityDataSensitive_nodeMatchesViewProperty() {
1083         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(true);
1084         try {
1085             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1086 
1087             final AccessibilityNodeInfo nonAdsNode = root.findAccessibilityNodeInfosByViewId(
1088                     mActivity.getResources().getResourceName(R.id.containerView)).get(0);
1089             final AccessibilityNodeInfo adsNode = root.findAccessibilityNodeInfosByViewId(
1090                     mActivity.getResources().getResourceName(R.id.adsView)).get(0);
1091 
1092             assertThat(nonAdsNode.isAccessibilityDataSensitive()).isFalse();
1093             assertThat(adsNode.isAccessibilityDataSensitive()).isTrue();
1094         } finally {
1095             service.disableSelfAndRemove();
1096         }
1097     }
1098 
1099     @Test
1100     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_visibleToAccessibilityTool()1101     public void testAccessibilityDataSensitive_visibleToAccessibilityTool() throws Throwable {
1102         // Relevant view structure:
1103         //   containerView (LinearLayout, accessibilityDataSensitive=auto)
1104         //     adsView (LinearLayout, accessibilityDataSensitive=true)
1105         //       innerContainerView (LinearLayout, accessibilityDataSensitive=auto)
1106         //         innerView (Button, accessibilityDataSensitive=auto)
1107         // Only adsView sets accessibilityDataSensitive=true in the layout XML.
1108         // Inner views should inherit true from their (grand)parent view.
1109         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(true);
1110         try {
1111             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1112 
1113             final String containerViewName = mActivity.getResources().getResourceName(
1114                     R.id.containerView);
1115 
1116             final String adsViewName = mActivity.getResources().getResourceName(R.id.adsView);
1117             final String adsViewText = mActivity.findViewById(
1118                     R.id.adsView).getContentDescription().toString();
1119 
1120             final String innerContainerViewName = mActivity.getResources().getResourceName(
1121                     R.id.innerContainerView);
1122             final String innerContainerViewText =
1123                     mActivity.findViewById(
1124                             R.id.innerContainerView).getContentDescription().toString();
1125 
1126             final String innerViewName = mActivity.getResources().getResourceName(R.id.innerView);
1127             final String innerViewText = mActivity.findViewById(
1128                     R.id.innerView).getContentDescription().toString();
1129 
1130             // Search for the Views' nodes using various techniques:
1131 
1132             // ByViewId
1133             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName)).hasSize(1);
1134             assertThat(root.findAccessibilityNodeInfosByViewId(innerContainerViewName)).hasSize(1);
1135             assertThat(root.findAccessibilityNodeInfosByViewId(innerViewName)).hasSize(1);
1136             // ByText
1137             assertThat(root.findAccessibilityNodeInfosByText(adsViewText)).hasSize(1);
1138             assertThat(root.findAccessibilityNodeInfosByText(innerContainerViewText)).hasSize(1);
1139             assertThat(root.findAccessibilityNodeInfosByText(innerViewText)).hasSize(1);
1140             // Event propagation and findFocus
1141             service.setEventFilter(
1142                     filterForEventTypeWithResource(TYPE_VIEW_ACCESSIBILITY_FOCUSED, adsViewName));
1143             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName).get(0)
1144                     .performAction(ACTION_ACCESSIBILITY_FOCUS)).isTrue();
1145             service.waitOnEvent(DEFAULT_TIMEOUT_MS,
1146                     "Expected TYPE_VIEW_ACCESSIBILITY_FOCUSED event");
1147             assertThat(service.findFocus(
1148                     AccessibilityNodeInfo.FOCUS_ACCESSIBILITY).getContentDescription()).isEqualTo(
1149                     adsViewText);
1150             // Parent view's getChild()
1151             final AccessibilityNodeInfo parent = root.findAccessibilityNodeInfosByViewId(
1152                     containerViewName).get(0);
1153             assertThat(parent.getChildCount()).isEqualTo(1);
1154             assertThat(parent.getChild(0)).isNotNull();
1155         } finally {
1156             service.disableSelfAndRemove();
1157         }
1158     }
1159 
1160     @Test
1161     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_canObserveHoverEvent()1162     public void testAccessibilityDataSensitive_canObserveHoverEvent() {
1163         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(true);
1164         try {
1165             final long time = SystemClock.uptimeMillis();
1166             final View view = mActivity.findViewById(R.id.innerView);
1167             final int[] viewLocation = new int[2];
1168             view.getLocationOnScreen(viewLocation);
1169             final int x = viewLocation[0] + view.getWidth() / 2;
1170             final int y = viewLocation[1] + view.getHeight() / 2;
1171 
1172             service.setEventFilter(
1173                     filterForEventTypeWithResource(
1174                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
1175                             sInstrumentation.getTargetContext().getResources()
1176                                     .getResourceName(R.id.innerView)));
1177             injectHoverEvent(time, true, x, y);
1178             service.waitOnEvent(DEFAULT_TIMEOUT_MS, "Expected TYPE_VIEW_HOVER_ENTER event");
1179         } finally {
1180             service.disableSelfAndRemove();
1181         }
1182     }
1183 
1184     @Test
1185     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_checkAdsProperty_topDown()1186     public void testAccessibilityDataSensitive_checkAdsProperty_topDown() {
1187         // Accessing the View#isAccessibilityDataSensitive() property causes both the View & its
1188         // parent hierarchy to cache their values.
1189         // Assert that the property is as expected when starting from the top-most view.
1190         assertThat(mActivity.findViewById(R.id.containerView).isAccessibilityDataSensitive())
1191                 .isFalse();
1192         assertThat(mActivity.findViewById(R.id.adsView).isAccessibilityDataSensitive()).isTrue();
1193         assertThat(mActivity.findViewById(R.id.innerContainerView).isAccessibilityDataSensitive())
1194                 .isTrue();
1195         assertThat(mActivity.findViewById(R.id.innerView).isAccessibilityDataSensitive()).isTrue();
1196     }
1197 
1198     @Test
1199     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_checkAdsProperty_bottomUp()1200     public void testAccessibilityDataSensitive_checkAdsProperty_bottomUp() {
1201         // Accessing the View#isAccessibilityDataSensitive() property causes both the View & its
1202         // parent hierarchy to cache their values.
1203         // Assert that the property is as expected when starting from the bottom-most view.
1204         assertThat(mActivity.findViewById(R.id.innerView).isAccessibilityDataSensitive()).isTrue();
1205         assertThat(mActivity.findViewById(R.id.innerContainerView).isAccessibilityDataSensitive())
1206                 .isTrue();
1207         assertThat(mActivity.findViewById(R.id.adsView).isAccessibilityDataSensitive()).isTrue();
1208         assertThat(mActivity.findViewById(R.id.containerView).isAccessibilityDataSensitive())
1209                 .isFalse();
1210     }
1211 
1212     @Test
1213     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1214             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId",
1215             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByText",
1216             "android.view.accessibility.AccessibilityNodeInfo#getChild"})
testAccessibilityDataSensitive_hiddenFromSearches()1217     public void testAccessibilityDataSensitive_hiddenFromSearches() {
1218         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1219         try {
1220             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1221             final String adsViewName = mActivity.getResources().getResourceName(R.id.adsView);
1222             final String adsViewText = mActivity.getString(R.string.ads_desc);
1223 
1224             assertThat(root.findAccessibilityNodeInfosByViewId(adsViewName)).isEmpty();
1225             assertThat(root.findAccessibilityNodeInfosByText(adsViewText)).isEmpty();
1226             Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
1227             deque.add(root);
1228             while (!deque.isEmpty()) {
1229                 AccessibilityNodeInfo node = deque.removeFirst();
1230                 assertThat(node.getContentDescription()).isNotEqualTo(adsViewText);
1231                 for (int i = node.getChildCount() - 1; i >= 0; i--) {
1232                     deque.addLast(node.getChild(i));
1233                 }
1234             }
1235         } finally {
1236             service.disableSelfAndRemove();
1237         }
1238     }
1239 
1240     @Test
1241     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1242             "android.accessibilityservice.AccessibilityService#findFocus"})
testAccessibilityDataSensitive_hiddenFromFindFocus()1243     public void testAccessibilityDataSensitive_hiddenFromFindFocus() {
1244         StubEventCapturingAccessibilityService toolService = null;
1245         InstrumentedAccessibilityService nonToolService = null;
1246         try {
1247             toolService = getServiceForA11yToolTests(true);
1248             nonToolService = getServiceForA11yToolTests(false);
1249 
1250             // Set up initial focus on the ADS view.
1251             toolService.setEventFilter(filterForEventType(TYPE_VIEW_ACCESSIBILITY_FOCUSED));
1252             assertThat(mActivity.findViewById(R.id.adsView).performAccessibilityAction(
1253                     ACTION_ACCESSIBILITY_FOCUS, null)).isTrue();
1254             toolService.waitOnEvent(DEFAULT_TIMEOUT_MS,
1255                     "Expected TYPE_VIEW_ACCESSIBILITY_FOCUSED event");
1256 
1257             assertThat(toolService.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY))
1258                     .isNotNull();
1259             assertThat(nonToolService.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY))
1260                     .isNull();
1261         } finally {
1262             if (toolService != null) {
1263                 toolService.disableSelfAndRemove();
1264             }
1265             if (nonToolService != null) {
1266                 nonToolService.disableSelfAndRemove();
1267             }
1268         }
1269     }
1270 
1271     @Test
1272     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1273             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId",
1274             "android.view.accessibility.AccessibilityNodeInfo#getChild"})
testAccessibilityDataSensitive_excludedFromParent()1275     public void testAccessibilityDataSensitive_excludedFromParent() {
1276         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1277         try {
1278             final AccessibilityNodeInfo parentContainer =
1279                     service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1280                             mActivity.getResources().getResourceName(R.id.containerView)).get(0);
1281 
1282             assertThat(parentContainer.getChildCount()).isEqualTo(0);
1283             assertThat(parentContainer.getChild(0)).isNull();
1284         } finally {
1285             service.disableSelfAndRemove();
1286         }
1287     }
1288 
1289     @Test
1290     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1291             "android.view.accessibility.AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId"})
testAccessibilityDataSensitive_innerChildHidden()1292     public void testAccessibilityDataSensitive_innerChildHidden() {
1293         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1294 
1295         try {
1296             assertThat(service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1297                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1298         } finally {
1299             service.disableSelfAndRemove();
1300         }
1301     }
1302 
1303     @Test
1304     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1305             "android.view.accessibility.AccessibilityManager#sendAccessibilityEvent"})
testAccessibilityDataSensitive_hiddenFromEventPropagation()1306     public void testAccessibilityDataSensitive_hiddenFromEventPropagation() {
1307         final StubEventCapturingAccessibilityService service = getServiceForA11yToolTests(false);
1308         try {
1309             final View innerView = mActivity.findViewById(R.id.innerView);
1310             innerView.setOnClickListener(v -> {
1311                 // empty, but necessary for performClick to return true
1312             });
1313             assertTrue(innerView.isAccessibilityDataSensitive());
1314             assertTrue(innerView.isClickable());
1315 
1316             service.setEventFilter(filterForEventType(TYPE_VIEW_CLICKED));
1317             sInstrumentation.runOnMainSync(() -> assertThat(innerView.performClick()).isTrue());
1318             assertThrows("Received TYPE_VIEW_CLICKED event from accessibilityDataSensitive view.",
1319                     AssertionError.class,
1320                     () -> service.waitOnEvent(DEFAULT_TIMEOUT_MS, "(expected to timeout)"));
1321         } finally {
1322             service.disableSelfAndRemove();
1323         }
1324     }
1325 
1326     @Test
1327     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive"})
testAccessibilityDataSensitive_hiddenIfFilterTouchesWhenObscured()1328     public void testAccessibilityDataSensitive_hiddenIfFilterTouchesWhenObscured() {
1329         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1330         try {
1331             View containerView = mActivity.findViewById(R.id.containerView);
1332             assertThat(containerView.isAccessibilityDataSensitive()).isFalse();
1333             assertThat(containerView.getFilterTouchesWhenObscured()).isFalse();
1334 
1335             mActivity.findViewById(R.id.containerView).setFilterTouchesWhenObscured(true);
1336 
1337             assertThat(containerView.isAccessibilityDataSensitive()).isTrue();
1338             assertThat(service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(
1339                     mActivity.getResources().getResourceName(R.id.containerView))).isEmpty();
1340         } finally {
1341             service.disableSelfAndRemove();
1342         }
1343     }
1344 
1345     @Test
1346     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1347             "android.view.View#setAccessibilityDataSensitive"})
testAccessibilityDataSensitive_changingValueUpdatesChildren_noFirst()1348     public void testAccessibilityDataSensitive_changingValueUpdatesChildren_noFirst() {
1349         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1350         try {
1351             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1352             // The view starts as ADS=true as defined in the XML.
1353             View adsView = mActivity.findViewById(R.id.adsView);
1354             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1355 
1356             // Set to NO, ensure we can find this view & all (grand)children.
1357             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_NO);
1358             assertThat(adsView.isAccessibilityDataSensitive()).isFalse();
1359             assertThat(root.findAccessibilityNodeInfosByViewId(
1360                     mActivity.getResources().getResourceName(R.id.adsView))).isNotEmpty();
1361             assertThat(root.findAccessibilityNodeInfosByViewId(
1362                     mActivity.getResources().getResourceName(
1363                             R.id.innerContainerView))).isNotEmpty();
1364             assertThat(root.findAccessibilityNodeInfosByViewId(
1365                     mActivity.getResources().getResourceName(R.id.innerView))).isNotEmpty();
1366 
1367             // Set back to YES, ensure this view & all (grand)children are hidden.
1368             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES);
1369             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1370             assertThat(root.findAccessibilityNodeInfosByViewId(
1371                     mActivity.getResources().getResourceName(R.id.adsView))).isEmpty();
1372             assertThat(root.findAccessibilityNodeInfosByViewId(
1373                     mActivity.getResources().getResourceName(R.id.innerContainerView))).isEmpty();
1374             assertThat(root.findAccessibilityNodeInfosByViewId(
1375                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1376         } finally {
1377             service.disableSelfAndRemove();
1378         }
1379     }
1380 
1381     @Test
1382     @ApiTest(apis = {"android.view.View#isAccessibilityDataSensitive",
1383             "android.view.View#setAccessibilityDataSensitive"})
testAccessibilityDataSensitive_changingValueUpdatesChildren_yesFirst()1384     public void testAccessibilityDataSensitive_changingValueUpdatesChildren_yesFirst() {
1385         final InstrumentedAccessibilityService service = getServiceForA11yToolTests(false);
1386         try {
1387             final AccessibilityNodeInfo root = service.getRootInActiveWindow();
1388             // The view starts as AccessibilityDataSensitive=true as defined in the XML.
1389             View adsView = mActivity.findViewById(R.id.adsView);
1390             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1391 
1392             // Explicitly set to YES, ensure this view & all (grand)children are hidden.
1393             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES);
1394             assertThat(adsView.isAccessibilityDataSensitive()).isTrue();
1395             assertThat(root.findAccessibilityNodeInfosByViewId(
1396                     mActivity.getResources().getResourceName(R.id.adsView))).isEmpty();
1397             assertThat(root.findAccessibilityNodeInfosByViewId(
1398                     mActivity.getResources().getResourceName(R.id.innerContainerView))).isEmpty();
1399             assertThat(root.findAccessibilityNodeInfosByViewId(
1400                     mActivity.getResources().getResourceName(R.id.innerView))).isEmpty();
1401 
1402             // Set to NO, ensure we can find this view & all (grand)children.
1403             adsView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_NO);
1404             assertThat(adsView.isAccessibilityDataSensitive()).isFalse();
1405             assertThat(root.findAccessibilityNodeInfosByViewId(
1406                     mActivity.getResources().getResourceName(R.id.adsView))).isNotEmpty();
1407             assertThat(root.findAccessibilityNodeInfosByViewId(
1408                     mActivity.getResources().getResourceName(
1409                             R.id.innerContainerView))).isNotEmpty();
1410             assertThat(root.findAccessibilityNodeInfosByViewId(
1411                     mActivity.getResources().getResourceName(R.id.innerView))).isNotEmpty();
1412         } finally {
1413             service.disableSelfAndRemove();
1414         }
1415     }
1416 
1417     @Test
1418     @ApiTest(apis = {
1419             "android.view.accessibility.AccessibilityManager#isRequestFromAccessibilityTool"})
testAccessibilityDataSensitive_requestIsFromAccessibilityTool_TrueForTool()1420     public void testAccessibilityDataSensitive_requestIsFromAccessibilityTool_TrueForTool() {
1421         checkIsRequestFromAccessibilityTool(true);
1422     }
1423 
1424     @Test
1425     @ApiTest(apis = {
1426             "android.view.accessibility.AccessibilityManager#isRequestFromAccessibilityTool"})
testAccessibilityDataSensitive_requestIsFromAccessibilityTool_FalseForNonTool()1427     public void testAccessibilityDataSensitive_requestIsFromAccessibilityTool_FalseForNonTool() {
1428         checkIsRequestFromAccessibilityTool(false);
1429     }
1430 
checkIsRequestFromAccessibilityTool(boolean serviceIsAccessibilityTool)1431     private void checkIsRequestFromAccessibilityTool(boolean serviceIsAccessibilityTool) {
1432         final InstrumentedAccessibilityService service =
1433             getServiceForA11yToolTests(serviceIsAccessibilityTool);
1434         try {
1435             final View view = mActivity.findViewById(R.id.listview);
1436             final String viewId = mActivity.getResources().getResourceName(R.id.listview);
1437             final AccessibilityManager accessibilityManager =
1438                     (AccessibilityManager) sInstrumentation.getContext().getSystemService(
1439                             Service.ACCESSIBILITY_SERVICE);
1440 
1441             final Object waitLock = new Object();
1442             final AtomicReference<Boolean> fromTool = new AtomicReference<>();
1443             view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1444                 @Override
1445                 public void onInitializeAccessibilityNodeInfo(View host,
1446                         AccessibilityNodeInfo info) {
1447                     super.onInitializeAccessibilityNodeInfo(host, info);
1448                     synchronized (waitLock) {
1449                         fromTool.set(accessibilityManager.isRequestFromAccessibilityTool());
1450                         waitLock.notifyAll();
1451                     }
1452                 }
1453             });
1454 
1455             // Trigger node creation from the service-under-test.
1456             service.getRootInActiveWindow().findAccessibilityNodeInfosByViewId(viewId);
1457 
1458             TestUtils.waitOn(waitLock,
1459                     () -> fromTool.get() != null && fromTool.get() == serviceIsAccessibilityTool,
1460                     DEFAULT_TIMEOUT_MS,
1461                     "Expected isRequestFromAccessibilityTool to be "
1462                         + serviceIsAccessibilityTool);
1463         } finally {
1464             service.disableSelfAndRemove();
1465         }
1466     }
1467 
1468     @Test
1469     @ApiTest(apis = {
1470             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_NavigateHierarchy()1471     public void testDirectAccessibilityConnection_NavigateHierarchy() throws Throwable {
1472         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1473         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1474 
1475         assertThat(layoutNode).isNotNull();
1476         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1477 
1478         // Access this node's children.
1479         assertThat(layoutNode.getChildCount()).isGreaterThan(0);
1480         for (int i = layoutNode.getChildCount() - 1; i >= 0; i--) {
1481             assertThat(layoutNode.getChild(i)).isNotNull();
1482         }
1483 
1484         // Find the root node by accessing parents going up the hierarchy.
1485         AccessibilityNodeInfo rootNode = layoutNode;
1486         while (rootNode.getParent() != null) {
1487             rootNode = rootNode.getParent();
1488         }
1489         assertThat(rootNode).isEqualTo(layoutView.getRootView().createAccessibilityNodeInfo());
1490 
1491         // Find more nodes, starting from the root.
1492         assertThat(rootNode.findAccessibilityNodeInfosByViewId(
1493                 "android.accessibilityservice.cts:id/button")).isNotEmpty();
1494         assertThat(rootNode.findAccessibilityNodeInfosByText(
1495                 mActivity.getString(R.string.button_title))).isNotEmpty();
1496 
1497         // Find and search the focus.
1498         try {
1499             // Enable touch exploration, needed for performAction(ACTION_ACCESSIBILITY_FOCUS).
1500             enableTouchExploration(true);
1501             final AccessibilityNodeInfo buttonNode = rootNode.findAccessibilityNodeInfosByViewId(
1502                     "android.accessibilityservice.cts:id/button").get(0);
1503             sUiAutomation.executeAndWaitForEvent(
1504                     () -> assertTrue(
1505                             buttonNode.performAction(
1506                                     AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)),
1507                     filterForEventType(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED),
1508                     DEFAULT_TIMEOUT_MS);
1509             assertThat(rootNode.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)).isEqualTo(
1510                     buttonNode);
1511             assertThat(rootNode.focusSearch(View.FOCUS_FORWARD)).isNotNull();
1512         } finally {
1513             enableTouchExploration(false);
1514         }
1515     }
1516 
1517     @Test
1518     @ApiTest(apis = {
1519             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_CanPerformAction()1520     public void testDirectAccessibilityConnection_CanPerformAction() {
1521         View button = mActivity.findViewById(R.id.button);
1522         AtomicBoolean clicked = new AtomicBoolean(false);
1523         button.setOnClickListener((view) -> clicked.set(true));
1524         AccessibilityNodeInfo buttonNode = button.createAccessibilityNodeInfo();
1525 
1526         assertThat(buttonNode).isNotNull();
1527         buttonNode.setQueryFromAppProcessEnabled(button.getRootView(), true);
1528 
1529         assertThat(buttonNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)).isTrue();
1530         assertThat(clicked.get()).isTrue();
1531     }
1532 
1533     @Test
1534     @ApiTest(apis = {
1535             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_CanDisable()1536     public void testDirectAccessibilityConnection_CanDisable() {
1537         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1538         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1539         assertThat(layoutNode).isNotNull();
1540 
1541         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1542         assertThat(layoutNode.getParent()).isNotNull();
1543 
1544         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), false);
1545         try {
1546             layoutNode.getParent();
1547             fail("Should not be able to navigate node tree on node without any connection.");
1548         } catch (IllegalStateException e) {
1549             // expected due to undefined connection ID
1550         }
1551     }
1552 
1553     @Test
1554     @ApiTest(apis = {
1555             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_AccessibilityManagerEnabled()1556     public void testDirectAccessibilityConnection_AccessibilityManagerEnabled() {
1557         // Note: this test checks AM#hasAnyDirectConnection() as a proxy for #isEnabled because
1558         // #isEnabled is also modified by the UiAutomation used in this test.
1559 
1560         View layoutView = mActivity.findViewById(R.id.buttonLayout);
1561         AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1562         final AccessibilityManager accessibilityManager =
1563                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
1564                         Service.ACCESSIBILITY_SERVICE);
1565 
1566         // Ensure no DirectConnection to start.
1567         assertThat(accessibilityManager.hasAnyDirectConnection()).isFalse();
1568 
1569         // Enable app-process querying, which adds a connection for this node.
1570         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), true);
1571         assertThat(accessibilityManager.hasAnyDirectConnection()).isTrue();
1572 
1573         // Disable app-process querying for this node.
1574         layoutNode.setQueryFromAppProcessEnabled(layoutView.getRootView(), false);
1575         // The connection should still exist until ViewRootImpl detaches from the window, in case
1576         // other nodes in this view hierarchy use the connection.
1577         assertThat(accessibilityManager.hasAnyDirectConnection()).isTrue();
1578 
1579         // Detach the ViewRootImpl from the window by finishing the activity, then wait for the
1580         // change notification that comes from ViewRootImpl itself, after which the connection
1581         // should now be gone.
1582         final Object waitLock = new Object();
1583         final AtomicBoolean hasAnyDirectConnection = new AtomicBoolean(true);
1584         accessibilityManager.addAccessibilityStateChangeListener(
1585                 enabled -> {
1586                     synchronized (waitLock) {
1587                         hasAnyDirectConnection.set(accessibilityManager.hasAnyDirectConnection());
1588                         waitLock.notifyAll();
1589                     }
1590                 });
1591         mActivity.runOnUiThread(() -> mActivity.finish());
1592         TestUtils.waitOn(waitLock, () -> !hasAnyDirectConnection.get(), DEFAULT_TIMEOUT_MS,
1593                 "AccessibilityManager#hasAnyDirectConnection() still true");
1594     }
1595 
1596     @Test
1597     @ApiTest(apis = {
1598             "android.view.accessibility.AccessibilityNodeInfo#setQueryFromAppProcessEnabled"})
testDirectAccessibilityConnection_UsesCurrentWindowSpec()1599     public void testDirectAccessibilityConnection_UsesCurrentWindowSpec() throws Throwable {
1600         // Store the initial bounds of the ANI.
1601         final View layoutView = mActivity.findViewById(R.id.buttonLayout);
1602         final AccessibilityNodeInfo layoutNode = layoutView.createAccessibilityNodeInfo();
1603         final Rect initialBounds = new Rect();
1604         layoutNode.setQueryFromAppProcessEnabled(layoutView, true);
1605         layoutNode.getBoundsInScreen(initialBounds);
1606 
1607         // Magnify the screen.
1608         final StubMagnificationAccessibilityService service =
1609                 InstrumentedAccessibilityService.enableService(
1610                         StubMagnificationAccessibilityService.class);
1611         try {
1612             final MagnificationConfig magnificationConfig =
1613                     new MagnificationConfig.Builder().setMode(MAGNIFICATION_MODE_FULLSCREEN)
1614                             .setScale(2f).build();
1615             service.runOnServiceSync(
1616                     () -> service.getMagnificationController()
1617                             .setMagnificationConfig(magnificationConfig, false));
1618 
1619             // Check that the ANI bounds have changed.
1620             TestUtils.waitUntil("Failed to refresh node with updated boundsInScreen",
1621                     (int) DEFAULT_TIMEOUT_MS / 1000,
1622                     () -> {
1623                         final Rect boundsAfterMagnification = new Rect();
1624                         layoutNode.refresh();
1625                         layoutNode.getBoundsInScreen(boundsAfterMagnification);
1626                         return !boundsAfterMagnification.equals(initialBounds);
1627                     });
1628         } finally {
1629             service.disableSelfAndRemove();
1630         }
1631     }
1632 
1633     @Test
1634     @ApiTest(apis = {
1635             "android.view.accessibility.AccessibilityNodeInfo"
1636                     + "#setMinDurationBetweenContentChanges",
1637             "android.view.accessibility.AccessibilityNodeInfo"
1638                     + "#getMinDurationBetweenContentChanges"})
testSetMinDurationBetweenContentChanges()1639     public void testSetMinDurationBetweenContentChanges() {
1640         final View testView = mActivity.findViewById(R.id.buttonLayout);
1641         final AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
1642         nodeInfo.setMinDurationBetweenContentChanges(Duration.ofMillis(200));
1643         assertThat(nodeInfo.getMinDurationBetweenContentChanges().toMillis()).isEqualTo(200);
1644     }
1645 
1646     @Test
1647     @ApiTest(apis = {
1648             "android.view.accessibility.AccessibilityNodeInfo"
1649                     + "#setRequestInitialAccessibilityFocus",
1650             "android.view.accessibility.AccessibilityNodeInfo"
1651                     + "#hasRequestInitialAccessibilityFocus"})
testSetRequestInitialAccessibilityFocus()1652     public void testSetRequestInitialAccessibilityFocus() {
1653         final View testView = mActivity.findViewById(R.id.buttonLayout);
1654         final AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
1655         nodeInfo.setRequestInitialAccessibilityFocus(true);
1656         assertThat(nodeInfo.hasRequestInitialAccessibilityFocus()).isTrue();
1657     }
1658 
1659 
1660     @AsbSecurityTest(cveBugId = {243378132})
1661     @Test
testUninstallPackage_DisablesMultipleServices()1662     public void testUninstallPackage_DisablesMultipleServices() throws Exception {
1663         final String apkPath =
1664                 "/data/local/tmp/cts/content/CtsAccessibilityMultipleServicesApp.apk";
1665         final String packageName = "foo.bar.multipleservices";
1666         final ComponentName service1 = ComponentName.createRelative(packageName, ".StubService1");
1667         final ComponentName service2 = ComponentName.createRelative(packageName, ".StubService2");
1668         // Match AccessibilityManagerService#COMPONENT_NAME_SEPARATOR
1669         final String componentNameSeparator = ":";
1670 
1671         final String originalEnabledServicesSetting = getEnabledServicesSetting();
1672 
1673         try {
1674             // Install the apk in this test method, instead of as part of the target preparer, to
1675             // allow repeated --iterations of the test.
1676             assertThat(ShellUtils.runShellCommand("pm install " + apkPath)).startsWith("Success");
1677 
1678             // Enable the two services and wait until AccessibilityManager reports them as enabled.
1679             final String servicesToEnable = getEnabledServicesSetting() + componentNameSeparator
1680                     + service1.flattenToShortString() + componentNameSeparator
1681                     + service2.flattenToShortString();
1682             ShellCommandBuilder.create(sInstrumentation)
1683                     .putSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
1684                             servicesToEnable)
1685                     .putSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED, "1")
1686                     .run();
1687             TestUtils.waitUntil("Failed to enable 2 services from package " + packageName,
1688                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
1689                     () -> getEnabledServices().stream().filter(
1690                             info -> info.getId().startsWith(packageName)).count() == 2);
1691 
1692             // Uninstall the package that contains the services.
1693             assertThat(ShellUtils.runShellCommand("pm uninstall " + packageName)).startsWith(
1694                     "Success");
1695 
1696             // Ensure the uninstall removed the services from the secure setting.
1697             TestUtils.waitUntil(
1698                     "Failed to disable services after uninstalling package " + packageName,
1699                     (int) TIMEOUT_SERVICE_ENABLE / 1000,
1700                     () -> !getEnabledServicesSetting().contains(packageName));
1701         } finally {
1702             ShellCommandBuilder.create(sInstrumentation)
1703                     .putSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
1704                             originalEnabledServicesSetting)
1705                     .run();
1706             ShellUtils.runShellCommand("pm uninstall " + packageName);
1707         }
1708     }
1709 
1710     @Test
1711     @ApiTest(apis = {
1712             "android.view.accessibility.AccessibilityNodeInfo#setContainerTitle"})
testSetContainerTitle()1713     public void testSetContainerTitle() {
1714         View testView = mActivity.findViewById(R.id.buttonLayout);
1715         AccessibilityNodeInfo nodeInfo = testView.createAccessibilityNodeInfo();
1716         nodeInfo.setContainerTitle("Container title");
1717         assertEquals("Container title", nodeInfo.getContainerTitle());
1718 
1719         nodeInfo.setContainerTitle(null);
1720         assertEquals(null, nodeInfo.getContainerTitle());
1721     }
1722 
1723     @Test
1724     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_interceptsEventFromRequestedSource_SetAndUnset()1725     public void testOnMotionEvent_interceptsEventFromRequestedSource_SetAndUnset() {
1726         final int requestedSource = InputDevice.SOURCE_JOYSTICK;
1727         final StubMotionInterceptingAccessibilityService service =
1728                 mMotionInterceptingServiceRule.enableService();
1729         service.setMotionEventSources(requestedSource);
1730         assertThat(service.getServiceInfo().getMotionEventSources()).isEqualTo(requestedSource);
1731         final Object waitObject = new Object();
1732         final AtomicInteger eventCount = new AtomicInteger(0);
1733         service.setOnMotionEventListener(motionEvent -> {
1734             synchronized (waitObject) {
1735                 if (motionEvent.getSource() == requestedSource) {
1736                     eventCount.incrementAndGet();
1737                 }
1738                 waitObject.notifyAll();
1739             }
1740         });
1741 
1742         // Inject 2 events to the input filter.
1743         sUiAutomation.injectInputEventToInputFilter(createMotionEvent(requestedSource));
1744         sUiAutomation.injectInputEventToInputFilter(createMotionEvent(requestedSource));
1745         // We should find 2 events.
1746         TestUtils.waitOn(waitObject, () -> eventCount.get() == 2,
1747                 TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
1748                 "Service did not receive MotionEvent");
1749 
1750         // Stop listening to events for this source, then inject 1 more event to the input filter.
1751         service.setMotionEventSources(0 /* no sources */);
1752         assertThat(service.getServiceInfo().getMotionEventSources()).isEqualTo(0);
1753         sUiAutomation.injectInputEventToInputFilter(createMotionEvent(requestedSource));
1754         // Assert we only received the original 2.
1755         try {
1756             TestUtils.waitOn(waitObject, () -> eventCount.get() == 3,
1757                     TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
1758                     "(expected)");
1759         } catch (AssertionError e) {
1760             // expected
1761         }
1762         assertThat(eventCount.get()).isEqualTo(2);
1763     }
1764 
1765     @Test
1766     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_ignoresEventFromDifferentSource()1767     public void testOnMotionEvent_ignoresEventFromDifferentSource() {
1768         final int requestedSource = InputDevice.SOURCE_JOYSTICK;
1769         final int actualSource = InputDevice.SOURCE_ROTARY_ENCODER;
1770         final StubMotionInterceptingAccessibilityService service =
1771                 mMotionInterceptingServiceRule.enableService();
1772         service.setMotionEventSources(requestedSource);
1773         final Object waitObject = new Object();
1774         final AtomicBoolean foundEvent = new AtomicBoolean(false);
1775         service.setOnMotionEventListener(motionEvent -> {
1776             synchronized (waitObject) {
1777                 if (motionEvent.getSource() == requestedSource) {
1778                     foundEvent.set(true);
1779                 }
1780                 waitObject.notifyAll();
1781             }
1782         });
1783 
1784         sUiAutomation.injectInputEventToInputFilter(createMotionEvent(actualSource));
1785 
1786         try {
1787             TestUtils.waitOn(waitObject, foundEvent::get, TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
1788                     "(expected)");
1789         } catch (AssertionError e) {
1790             // expected
1791         }
1792         assertThat(foundEvent.get()).isFalse();
1793     }
1794 
1795     @Test
1796     @ApiTest(apis = {"android.accessibilityservice.AccessibilityService#onMotionEvent"})
testOnMotionEvent_ignoresTouchscreenEventWhenTouchExplorationEnabled()1797     public void testOnMotionEvent_ignoresTouchscreenEventWhenTouchExplorationEnabled() {
1798         final int requestedSource = InputDevice.SOURCE_TOUCHSCREEN;
1799         final StubMotionInterceptingAccessibilityService motionInterceptingService =
1800                 mMotionInterceptingServiceRule.enableService();
1801         TouchExplorationStubAccessibilityService touchExplorationService =
1802                 enableService(TouchExplorationStubAccessibilityService.class);
1803         try {
1804             motionInterceptingService.setMotionEventSources(requestedSource);
1805             final Object waitObject = new Object();
1806             final AtomicBoolean foundEvent = new AtomicBoolean(false);
1807             motionInterceptingService.setOnMotionEventListener(motionEvent -> {
1808                 synchronized (waitObject) {
1809                     if (motionEvent.getSource() == requestedSource) {
1810                         foundEvent.set(true);
1811                     }
1812                     waitObject.notifyAll();
1813                 }
1814             });
1815 
1816             sUiAutomation.injectInputEventToInputFilter(createMotionEvent(requestedSource));
1817 
1818             try {
1819                 TestUtils.waitOn(waitObject, foundEvent::get,
1820                         TIMEOUT_FOR_MOTION_EVENT_INTERCEPTION_MS,
1821                         "(expected)");
1822             } catch (AssertionError e) {
1823                 // expected
1824             }
1825             assertThat(foundEvent.get()).isFalse();
1826         } finally {
1827             touchExplorationService.disableSelfAndRemove();
1828         }
1829     }
1830 
createMotionEvent(int source)1831     private MotionEvent createMotionEvent(int source) {
1832         // Only source is used by these tests, so set other properties to valid defaults.
1833         final long eventTime = SystemClock.uptimeMillis();
1834         final MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
1835         props.id = 0;
1836         return MotionEvent.obtain(eventTime,
1837                 eventTime,
1838                 MotionEvent.ACTION_MOVE,
1839                 1 /* pointerCount */,
1840                 new MotionEvent.PointerProperties[]{props},
1841                 new MotionEvent.PointerCoords[]{new MotionEvent.PointerCoords()},
1842                 0 /* metaState */,
1843                 0 /* buttonState */,
1844                 0 /* xPrecision */,
1845                 0 /* yPrecision */,
1846                 1 /* deviceId */,
1847                 0 /* edgeFlags */,
1848                 source,
1849                 0 /* flags */);
1850     }
1851 
getEnabledServices()1852     private List<AccessibilityServiceInfo> getEnabledServices() {
1853         return ((AccessibilityManager) sInstrumentation.getContext().getSystemService(
1854                 Context.ACCESSIBILITY_SERVICE)).getEnabledAccessibilityServiceList(
1855                 FEEDBACK_ALL_MASK);
1856     }
1857 
getEnabledServicesSetting()1858     private String getEnabledServicesSetting() {
1859         final String result = Settings.Secure.getString(
1860                 sInstrumentation.getContext().getContentResolver(),
1861                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
1862         return result != null ? result : "";
1863     }
1864 
assertPackageName(AccessibilityNodeInfo node, String packageName)1865     private static void assertPackageName(AccessibilityNodeInfo node, String packageName) {
1866         if (node == null) {
1867             return;
1868         }
1869         assertEquals(packageName, node.getPackageName());
1870         final int childCount = node.getChildCount();
1871         for (int i = 0; i < childCount; i++) {
1872             AccessibilityNodeInfo child = node.getChild(i);
1873             if (child != null) {
1874                 assertPackageName(child, packageName);
1875             }
1876         }
1877     }
1878 
enableTouchExploration(boolean enabled)1879     private static void enableTouchExploration(boolean enabled)
1880             throws InterruptedException {
1881         final int TIMEOUT_FOR_SERVICE_ENABLE = 10000; // millis; 10s
1882         final Object waitObject = new Object();
1883         final AtomicBoolean atomicBoolean = new AtomicBoolean(!enabled);
1884         AccessibilityManager.TouchExplorationStateChangeListener serviceListener = (boolean b) -> {
1885             synchronized (waitObject) {
1886                 atomicBoolean.set(b);
1887                 waitObject.notifyAll();
1888             }
1889         };
1890         final AccessibilityManager manager =
1891                 (AccessibilityManager) sInstrumentation.getContext().getSystemService(
1892                         Service.ACCESSIBILITY_SERVICE);
1893         manager.addTouchExplorationStateChangeListener(serviceListener);
1894 
1895         final AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
1896         assert info != null;
1897         if (enabled) {
1898             info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1899         } else {
1900             info.flags &= ~AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
1901         }
1902         sUiAutomation.setServiceInfo(info);
1903 
1904         final long timeoutTime = System.currentTimeMillis() + TIMEOUT_FOR_SERVICE_ENABLE;
1905         synchronized (waitObject) {
1906             while ((enabled != atomicBoolean.get()) && (System.currentTimeMillis() < timeoutTime)) {
1907                 waitObject.wait(timeoutTime - System.currentTimeMillis());
1908             }
1909         }
1910         if (enabled) {
1911             assertTrue("Touch exploration state listener not called when services enabled",
1912                     atomicBoolean.get());
1913             assertTrue("Timed out enabling accessibility",
1914                     manager.isEnabled() && manager.isTouchExplorationEnabled());
1915         } else {
1916             assertFalse("Touch exploration state listener not called when services disabled",
1917                     atomicBoolean.get());
1918             assertFalse("Timed out disabling accessibility",
1919                     manager.isEnabled() && manager.isTouchExplorationEnabled());
1920         }
1921         manager.removeTouchExplorationStateChangeListener(serviceListener);
1922     }
1923 
1924     /**
1925      * Returns a service for testing how accessibility tools or non-tools react to the
1926      * {@link View#isAccessibilityDataSensitive} property.
1927      *
1928      * @return {@link StubA11yToolAccessibilityService} when <code>isAccessibilityTool</code> is
1929      * true, otherwise returns {@link StubNonA11yToolAccessibilityService}.
1930      */
getServiceForA11yToolTests( boolean isAccessibilityTool)1931     private StubEventCapturingAccessibilityService getServiceForA11yToolTests(
1932             boolean isAccessibilityTool) {
1933         final StubEventCapturingAccessibilityService service;
1934         if (isAccessibilityTool) {
1935             service = InstrumentedAccessibilityService.enableService(
1936                     StubA11yToolAccessibilityService.class);
1937         } else {
1938             service = InstrumentedAccessibilityService.enableService(
1939                     StubNonA11yToolAccessibilityService.class);
1940         }
1941         final AccessibilityServiceInfo info = service.getServiceInfo();
1942         if (info == null || info.isAccessibilityTool() != isAccessibilityTool) {
1943             service.disableSelfAndRemove();
1944             fail("Expected service to have isAccessibilityTool=" + isAccessibilityTool);
1945         }
1946         return service;
1947     }
1948 
matchHover(int action, int x, int y)1949     private static MotionEvent matchHover(int action, int x, int y) {
1950         return argThat(new CtsMouseUtil.PositionMatcher(action, x, y));
1951     }
1952 
injectHoverEvent(long downTime, boolean isFirstHoverEvent, int xOnScreen, int yOnScreen)1953     private static void injectHoverEvent(long downTime, boolean isFirstHoverEvent,
1954             int xOnScreen, int yOnScreen) {
1955         final long eventTime = isFirstHoverEvent ? SystemClock.uptimeMillis() : downTime;
1956         MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_HOVER_MOVE,
1957                 xOnScreen, yOnScreen, 0);
1958         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
1959         sInstrumentation.sendPointerSync(event);
1960         event.recycle();
1961     }
1962 
getAppWidgetProviderInfo()1963     private AppWidgetProviderInfo getAppWidgetProviderInfo() {
1964         final ComponentName componentName = new ComponentName(
1965                 "foo.bar.baz", "foo.bar.baz.MyAppWidgetProvider");
1966         final List<AppWidgetProviderInfo> providers = getAppWidgetManager().getInstalledProviders();
1967         final int providerCount = providers.size();
1968         for (int i = 0; i < providerCount; i++) {
1969             final AppWidgetProviderInfo provider = providers.get(i);
1970             if (componentName.equals(provider.provider)
1971                     && Process.myUserHandle().equals(provider.getProfile())) {
1972                 return provider;
1973             }
1974         }
1975         return null;
1976     }
1977 
grantBindAppWidgetPermission()1978     private void grantBindAppWidgetPermission() throws Exception {
1979         ShellCommandBuilder.execShellCommand(sUiAutomation,
1980                 GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
1981     }
1982 
revokeBindAppWidgetPermission()1983     private void revokeBindAppWidgetPermission() throws Exception {
1984         ShellCommandBuilder.execShellCommand(sUiAutomation,
1985                 REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND + getCurrentUser());
1986     }
1987 
getAppWidgetManager()1988     private AppWidgetManager getAppWidgetManager() {
1989         return (AppWidgetManager) sInstrumentation.getTargetContext()
1990                 .getSystemService(Context.APPWIDGET_SERVICE);
1991     }
1992 
hasAppWidgets()1993     private boolean hasAppWidgets() {
1994         return sInstrumentation.getTargetContext().getPackageManager()
1995                 .hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS);
1996     }
1997 
1998     /**
1999      * Compares all properties of the <code>first</code> and the
2000      * <code>second</code>.
2001      */
equalsAccessiblityEvent(AccessibilityEvent first, AccessibilityEvent second)2002     private boolean equalsAccessiblityEvent(AccessibilityEvent first, AccessibilityEvent second) {
2003          return first.getEventType() == second.getEventType()
2004             && first.isChecked() == second.isChecked()
2005             && first.getCurrentItemIndex() == second.getCurrentItemIndex()
2006             && first.isEnabled() == second.isEnabled()
2007             && first.getFromIndex() == second.getFromIndex()
2008             && first.getItemCount() == second.getItemCount()
2009             && first.isPassword() == second.isPassword()
2010             && first.getRemovedCount() == second.getRemovedCount()
2011             && first.isScrollable()== second.isScrollable()
2012             && first.getToIndex() == second.getToIndex()
2013             && first.getRecordCount() == second.getRecordCount()
2014             && first.getScrollX() == second.getScrollX()
2015             && first.getScrollY() == second.getScrollY()
2016             && first.getAddedCount() == second.getAddedCount()
2017             && first.getDisplayId() == second.getDisplayId()
2018             && TextUtils.equals(first.getBeforeText(), second.getBeforeText())
2019             && TextUtils.equals(first.getClassName(), second.getClassName())
2020             && TextUtils.equals(first.getContentDescription(), second.getContentDescription())
2021             && equalsNotificationAsParcelableData(first, second)
2022             && equalsText(first, second);
2023     }
2024 
2025     /**
2026      * Compares the {@link android.os.Parcelable} data of the
2027      * <code>first</code> and <code>second</code>.
2028      */
equalsNotificationAsParcelableData(AccessibilityEvent first, AccessibilityEvent second)2029     private boolean equalsNotificationAsParcelableData(AccessibilityEvent first,
2030             AccessibilityEvent second) {
2031         Notification firstNotification = (Notification) first.getParcelableData();
2032         Notification secondNotification = (Notification) second.getParcelableData();
2033         if (firstNotification == null) {
2034             return (secondNotification == null);
2035         } else if (secondNotification == null) {
2036             return false;
2037         }
2038         return TextUtils.equals(firstNotification.tickerText, secondNotification.tickerText);
2039     }
2040 
2041     /**
2042      * Compares the text of the <code>first</code> and <code>second</code> text.
2043      */
equalsText(AccessibilityEvent first, AccessibilityEvent second)2044     private boolean equalsText(AccessibilityEvent first, AccessibilityEvent second) {
2045         List<CharSequence> firstText = first.getText();
2046         List<CharSequence> secondText = second.getText();
2047         if (firstText.size() != secondText.size()) {
2048             return false;
2049         }
2050         Iterator<CharSequence> firstIterator = firstText.iterator();
2051         Iterator<CharSequence> secondIterator = secondText.iterator();
2052         for (int i = 0; i < firstText.size(); i++) {
2053             if (!firstIterator.next().toString().equals(secondIterator.next().toString())) {
2054                 return false;
2055             }
2056         }
2057         return true;
2058     }
2059 
hasTooltipShowing(int id)2060     private boolean hasTooltipShowing(int id) {
2061         return getOnMain(sInstrumentation, () -> {
2062             final View viewWithTooltip = mActivity.findViewById(id);
2063             if (viewWithTooltip == null) {
2064                 return false;
2065             }
2066             final View tooltipView = viewWithTooltip.getTooltipView();
2067             return (tooltipView != null) && (tooltipView.getParent() != null);
2068         });
2069     }
2070 
getCurrentUser()2071     private static int getCurrentUser() {
2072         return android.os.Process.myUserHandle().getIdentifier();
2073     }
2074 }
2075