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