Home | History | Annotate | Download | only in cts
      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.accessibilityservice.cts.utils.AccessibilityEventFilterUtils
     20         .filterForEventType;
     21 import static android.accessibilityservice.cts.utils.RunOnMainUtils.getOnMain;
     22 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
     23         .ACTION_HIDE_TOOLTIP;
     24 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
     25         .ACTION_SHOW_TOOLTIP;
     26 
     27 import static org.hamcrest.core.IsEqual.equalTo;
     28 import static org.hamcrest.core.IsNull.nullValue;
     29 import static org.hamcrest.core.IsNull.notNullValue;
     30 import static org.hamcrest.Matchers.in;
     31 import static org.hamcrest.Matchers.not;
     32 import static org.junit.Assert.assertThat;
     33 
     34 import android.accessibilityservice.cts.activities.AccessibilityEndToEndActivity;
     35 import android.app.Activity;
     36 import android.app.AlertDialog;
     37 import android.app.Instrumentation;
     38 import android.app.Notification;
     39 import android.app.NotificationChannel;
     40 import android.app.NotificationManager;
     41 import android.app.PendingIntent;
     42 import android.app.Service;
     43 import android.app.UiAutomation;
     44 import android.appwidget.AppWidgetHost;
     45 import android.appwidget.AppWidgetManager;
     46 import android.appwidget.AppWidgetProviderInfo;
     47 import android.content.ComponentName;
     48 import android.content.Context;
     49 import android.content.Intent;
     50 import android.content.pm.PackageManager;
     51 import android.content.res.Configuration;
     52 import android.os.Process;
     53 import android.platform.test.annotations.AppModeFull;
     54 import android.platform.test.annotations.Presubmit;
     55 import android.test.suitebuilder.annotation.MediumTest;
     56 import android.text.TextUtils;
     57 import android.util.Log;
     58 import android.view.View;
     59 import android.view.accessibility.AccessibilityEvent;
     60 import android.view.accessibility.AccessibilityManager;
     61 import android.view.accessibility.AccessibilityNodeInfo;
     62 import android.widget.Button;
     63 import android.widget.EditText;
     64 import android.widget.ListView;
     65 
     66 import java.util.Iterator;
     67 import java.util.List;
     68 import java.util.concurrent.TimeoutException;
     69 
     70 /**
     71  * This class performs end-to-end testing of the accessibility feature by
     72  * creating an {@link Activity} and poking around so {@link AccessibilityEvent}s
     73  * are generated and their correct dispatch verified.
     74  */
     75 public class AccessibilityEndToEndTest extends
     76         AccessibilityActivityTestCase<AccessibilityEndToEndActivity> {
     77 
     78     private static final String LOG_TAG = "AccessibilityEndToEndTest";
     79 
     80     private static final String GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND =
     81             "appwidget grantbind --package android.accessibilityservice.cts --user 0";
     82 
     83     private static final String REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND =
     84             "appwidget revokebind --package android.accessibilityservice.cts --user 0";
     85 
     86     private static final String APP_WIDGET_PROVIDER_PACKAGE = "foo.bar.baz";
     87 
     88     /**
     89      * Creates a new instance for testing {@link AccessibilityEndToEndActivity}.
     90      */
     91     public AccessibilityEndToEndTest() {
     92         super(AccessibilityEndToEndActivity.class);
     93     }
     94 
     95     @MediumTest
     96     @Presubmit
     97     public void testTypeViewSelectedAccessibilityEvent() throws Throwable {
     98         // create and populate the expected event
     99         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    100         expected.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
    101         expected.setClassName(ListView.class.getName());
    102         expected.setPackageName(getActivity().getPackageName());
    103         expected.getText().add(getActivity().getString(R.string.second_list_item));
    104         expected.setItemCount(2);
    105         expected.setCurrentItemIndex(1);
    106         expected.setEnabled(true);
    107         expected.setScrollable(false);
    108         expected.setFromIndex(0);
    109         expected.setToIndex(1);
    110 
    111         final ListView listView = (ListView) getActivity().findViewById(R.id.listview);
    112 
    113         AccessibilityEvent awaitedEvent =
    114             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    115                 new Runnable() {
    116             @Override
    117             public void run() {
    118                 // trigger the event
    119                 getActivity().runOnUiThread(new Runnable() {
    120                     @Override
    121                     public void run() {
    122                         listView.setSelection(1);
    123                     }
    124                 });
    125             }},
    126             new UiAutomation.AccessibilityEventFilter() {
    127                 // check the received event
    128                 @Override
    129                 public boolean accept(AccessibilityEvent event) {
    130                     return equalsAccessiblityEvent(event, expected);
    131                 }
    132             },
    133             TIMEOUT_ASYNC_PROCESSING);
    134         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    135     }
    136 
    137     @MediumTest
    138     @Presubmit
    139     public void testTypeViewClickedAccessibilityEvent() throws Throwable {
    140         // create and populate the expected event
    141         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    142         expected.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED);
    143         expected.setClassName(Button.class.getName());
    144         expected.setPackageName(getActivity().getPackageName());
    145         expected.getText().add(getActivity().getString(R.string.button_title));
    146         expected.setEnabled(true);
    147 
    148         final Button button = (Button) getActivity().findViewById(R.id.button);
    149 
    150         AccessibilityEvent awaitedEvent =
    151             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    152                 new Runnable() {
    153             @Override
    154             public void run() {
    155                 // trigger the event
    156                 getActivity().runOnUiThread(new Runnable() {
    157                     @Override
    158                     public void run() {
    159                         button.performClick();
    160                     }
    161                 });
    162             }},
    163             new UiAutomation.AccessibilityEventFilter() {
    164                 // check the received event
    165                 @Override
    166                 public boolean accept(AccessibilityEvent event) {
    167                     return equalsAccessiblityEvent(event, expected);
    168                 }
    169             },
    170             TIMEOUT_ASYNC_PROCESSING);
    171         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    172     }
    173 
    174     @MediumTest
    175     @Presubmit
    176     public void testTypeViewLongClickedAccessibilityEvent() throws Throwable {
    177         // create and populate the expected event
    178         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    179         expected.setEventType(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
    180         expected.setClassName(Button.class.getName());
    181         expected.setPackageName(getActivity().getPackageName());
    182         expected.getText().add(getActivity().getString(R.string.button_title));
    183         expected.setEnabled(true);
    184 
    185         final Button button = (Button) getActivity().findViewById(R.id.button);
    186 
    187         AccessibilityEvent awaitedEvent =
    188             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    189                 new Runnable() {
    190             @Override
    191             public void run() {
    192                 // trigger the event
    193                 getActivity().runOnUiThread(new Runnable() {
    194                     @Override
    195                     public void run() {
    196                         button.performLongClick();
    197                     }
    198                 });
    199             }},
    200             new UiAutomation.AccessibilityEventFilter() {
    201                 // check the received event
    202                 @Override
    203                 public boolean accept(AccessibilityEvent event) {
    204                     return equalsAccessiblityEvent(event, expected);
    205                 }
    206             },
    207             TIMEOUT_ASYNC_PROCESSING);
    208         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    209     }
    210 
    211     @MediumTest
    212     @Presubmit
    213     public void testTypeViewFocusedAccessibilityEvent() throws Throwable {
    214         // create and populate the expected event
    215         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    216         expected.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    217         expected.setClassName(Button.class.getName());
    218         expected.setPackageName(getActivity().getPackageName());
    219         expected.getText().add(getActivity().getString(R.string.button_title));
    220         expected.setItemCount(4);
    221         expected.setCurrentItemIndex(3);
    222         expected.setEnabled(true);
    223 
    224         final Button button = (Button) getActivity().findViewById(R.id.buttonWithTooltip);
    225 
    226         AccessibilityEvent awaitedEvent =
    227             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    228                     () -> getActivity().runOnUiThread(() -> button.requestFocus()),
    229                     (event) -> equalsAccessiblityEvent(event, expected),
    230                     TIMEOUT_ASYNC_PROCESSING);
    231         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    232     }
    233 
    234     @MediumTest
    235     @Presubmit
    236     public void testTypeViewTextChangedAccessibilityEvent() throws Throwable {
    237         // focus the edit text
    238         final EditText editText = (EditText) getActivity().findViewById(R.id.edittext);
    239 
    240         AccessibilityEvent awaitedFocusEvent =
    241             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    242                 new Runnable() {
    243             @Override
    244             public void run() {
    245                 // trigger the event
    246                 getActivity().runOnUiThread(new Runnable() {
    247                     @Override
    248                     public void run() {
    249                         editText.requestFocus();
    250                     }
    251                 });
    252             }},
    253             new UiAutomation.AccessibilityEventFilter() {
    254                 // check the received event
    255                 @Override
    256                 public boolean accept(AccessibilityEvent event) {
    257                     return event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED;
    258                 }
    259             },
    260             TIMEOUT_ASYNC_PROCESSING);
    261         assertNotNull("Did not receive expected focuss event.", awaitedFocusEvent);
    262 
    263         final String beforeText = getActivity().getString(R.string.text_input_blah);
    264         final String newText = getActivity().getString(R.string.text_input_blah_blah);
    265         final String afterText = beforeText.substring(0, 3) + newText;
    266 
    267         // create and populate the expected event
    268         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    269         expected.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
    270         expected.setClassName(EditText.class.getName());
    271         expected.setPackageName(getActivity().getPackageName());
    272         expected.getText().add(afterText);
    273         expected.setBeforeText(beforeText);
    274         expected.setFromIndex(3);
    275         expected.setAddedCount(9);
    276         expected.setRemovedCount(1);
    277         expected.setEnabled(true);
    278 
    279         AccessibilityEvent awaitedTextChangeEvent =
    280             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    281                 new Runnable() {
    282             @Override
    283             public void run() {
    284                 // trigger the event
    285                 getActivity().runOnUiThread(new Runnable() {
    286                     @Override
    287                     public void run() {
    288                         editText.getEditableText().replace(3, 4, newText);
    289                     }
    290                 });
    291             }},
    292             new UiAutomation.AccessibilityEventFilter() {
    293                 // check the received event
    294                 @Override
    295                 public boolean accept(AccessibilityEvent event) {
    296                     return equalsAccessiblityEvent(event, expected);
    297                 }
    298             },
    299             TIMEOUT_ASYNC_PROCESSING);
    300         assertNotNull("Did not receive expected event: " + expected, awaitedTextChangeEvent);
    301     }
    302 
    303     @MediumTest
    304     @Presubmit
    305     public void testTypeWindowStateChangedAccessibilityEvent() throws Throwable {
    306         // create and populate the expected event
    307         final AccessibilityEvent expected = AccessibilityEvent.obtain();
    308         expected.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    309         expected.setClassName(AlertDialog.class.getName());
    310         expected.setPackageName(getActivity().getPackageName());
    311         expected.getText().add(getActivity().getString(R.string.alert_title));
    312         expected.getText().add(getActivity().getString(R.string.alert_message));
    313         expected.setEnabled(true);
    314 
    315         AccessibilityEvent awaitedEvent =
    316             getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    317                 new Runnable() {
    318             @Override
    319             public void run() {
    320                 // trigger the event
    321                 getActivity().runOnUiThread(new Runnable() {
    322                     @Override
    323                     public void run() {
    324                         (new AlertDialog.Builder(getActivity()).setTitle(R.string.alert_title)
    325                                 .setMessage(R.string.alert_message)).create().show();
    326                     }
    327                 });
    328             }},
    329             new UiAutomation.AccessibilityEventFilter() {
    330                 // check the received event
    331                 @Override
    332                 public boolean accept(AccessibilityEvent event) {
    333                     return equalsAccessiblityEvent(event, expected);
    334                 }
    335             },
    336             TIMEOUT_ASYNC_PROCESSING);
    337         assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    338     }
    339 
    340     @MediumTest
    341     @AppModeFull
    342     @SuppressWarnings("deprecation")
    343     @Presubmit
    344     public void testTypeNotificationStateChangedAccessibilityEvent() throws Throwable {
    345         // No notification UI on televisions.
    346         if ((getActivity().getResources().getConfiguration().uiMode
    347                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
    348             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
    349                     " - No notification UI on televisions.");
    350             return;
    351         }
    352         PackageManager pm = getInstrumentation().getTargetContext().getPackageManager();
    353         if (pm.hasSystemFeature(pm.FEATURE_WATCH)) {
    354             Log.i(LOG_TAG, "Skipping: testTypeNotificationStateChangedAccessibilityEvent" +
    355                     " - Watches have different notification system.");
    356             return;
    357         }
    358 
    359         String message = getActivity().getString(R.string.notification_message);
    360 
    361         final NotificationManager notificationManager =
    362                 (NotificationManager) getActivity().getSystemService(Service.NOTIFICATION_SERVICE);
    363         final NotificationChannel channel =
    364                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
    365         try {
    366             // create the notification to send
    367             channel.enableVibration(true);
    368             channel.enableLights(true);
    369             channel.setBypassDnd(true);
    370             notificationManager.createNotificationChannel(channel);
    371             NotificationChannel created =
    372                     notificationManager.getNotificationChannel(channel.getId());
    373             final int notificationId = 1;
    374             final Notification notification =
    375                     new Notification.Builder(getActivity(), channel.getId())
    376                             .setSmallIcon(android.R.drawable.stat_notify_call_mute)
    377                             .setContentIntent(PendingIntent.getActivity(getActivity(), 0,
    378                                     new Intent(),
    379                                     PendingIntent.FLAG_CANCEL_CURRENT))
    380                             .setTicker(message)
    381                             .setContentTitle("")
    382                             .setContentText("")
    383                             .setPriority(Notification.PRIORITY_MAX)
    384                             // Mark the notification as "interruptive" by specifying a vibration
    385                             // pattern. This ensures it's announced properly on watch-type devices.
    386                             .setVibrate(new long[]{})
    387                             .build();
    388 
    389             // create and populate the expected event
    390             final AccessibilityEvent expected = AccessibilityEvent.obtain();
    391             expected.setEventType(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
    392             expected.setClassName(Notification.class.getName());
    393             expected.setPackageName(getActivity().getPackageName());
    394             expected.getText().add(message);
    395             expected.setParcelableData(notification);
    396 
    397             AccessibilityEvent awaitedEvent =
    398                     getInstrumentation().getUiAutomation().executeAndWaitForEvent(
    399                             new Runnable() {
    400                                 @Override
    401                                 public void run() {
    402                                     // trigger the event
    403                                     getActivity().runOnUiThread(new Runnable() {
    404                                         @Override
    405                                         public void run() {
    406                                             // trigger the event
    407                                             notificationManager
    408                                                     .notify(notificationId, notification);
    409                                             getActivity().finish();
    410                                         }
    411                                     });
    412                                 }
    413                             },
    414                             new UiAutomation.AccessibilityEventFilter() {
    415                                 // check the received event
    416                                 @Override
    417                                 public boolean accept(AccessibilityEvent event) {
    418                                     return equalsAccessiblityEvent(event, expected);
    419                                 }
    420                             },
    421                             TIMEOUT_ASYNC_PROCESSING);
    422             assertNotNull("Did not receive expected event: " + expected, awaitedEvent);
    423         } finally {
    424             notificationManager.deleteNotificationChannel(channel.getId());
    425         }
    426     }
    427 
    428     @MediumTest
    429     public void testInterrupt_notifiesService() {
    430         getInstrumentation()
    431                 .getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
    432         InstrumentedAccessibilityService service = InstrumentedAccessibilityService.enableService(
    433                 getInstrumentation(), InstrumentedAccessibilityService.class);
    434         try {
    435             assertFalse(service.wasOnInterruptCalled());
    436 
    437             getActivity().runOnUiThread(() -> {
    438                 AccessibilityManager accessibilityManager = (AccessibilityManager) getActivity()
    439                         .getSystemService(Service.ACCESSIBILITY_SERVICE);
    440                 accessibilityManager.interrupt();
    441             });
    442 
    443             Object waitObject = service.getInterruptWaitObject();
    444             synchronized (waitObject) {
    445                 if (!service.wasOnInterruptCalled()) {
    446                     try {
    447                         waitObject.wait(TIMEOUT_ASYNC_PROCESSING);
    448                     } catch (InterruptedException e) {
    449                         // Do nothing
    450                     }
    451                 }
    452             }
    453             assertTrue(service.wasOnInterruptCalled());
    454         } finally {
    455             service.disableSelfAndRemove();
    456         }
    457     }
    458 
    459     @MediumTest
    460     public void testPackageNameCannotBeFaked() throws Exception {
    461         getActivity().runOnUiThread(() -> {
    462             // Set the activity to report fake package for events and nodes
    463             getActivity().setReportedPackageName("foo.bar.baz");
    464 
    465             // Make sure node package cannot be faked
    466             AccessibilityNodeInfo root = getInstrumentation().getUiAutomation()
    467                     .getRootInActiveWindow();
    468             assertPackageName(root, getActivity().getPackageName());
    469         });
    470 
    471         // Make sure event package cannot be faked
    472         try {
    473             getInstrumentation().getUiAutomation().executeAndWaitForEvent(() ->
    474                 getInstrumentation().runOnMainSync(() ->
    475                     getActivity().findViewById(R.id.button).requestFocus())
    476                 , (AccessibilityEvent event) ->
    477                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
    478                             && event.getPackageName().equals(getActivity().getPackageName())
    479                 , TIMEOUT_ASYNC_PROCESSING);
    480         } catch (TimeoutException e) {
    481             fail("Events from fake package should be fixed to use the correct package");
    482         }
    483     }
    484 
    485     @AppModeFull
    486     @MediumTest
    487     @Presubmit
    488     public void testPackageNameCannotBeFakedAppWidget() throws Exception {
    489         if (!hasAppWidgets()) {
    490             return;
    491         }
    492 
    493         getInstrumentation().runOnMainSync(() -> {
    494             // Set the activity to report fake package for events and nodes
    495             getActivity().setReportedPackageName(APP_WIDGET_PROVIDER_PACKAGE);
    496 
    497             // Make sure we cannot report nodes as if from the widget package
    498             AccessibilityNodeInfo root = getInstrumentation().getUiAutomation()
    499                     .getRootInActiveWindow();
    500             assertPackageName(root, getActivity().getPackageName());
    501         });
    502 
    503         // Make sure we cannot send events as if from the widget package
    504         try {
    505             getInstrumentation().getUiAutomation().executeAndWaitForEvent(() ->
    506                 getInstrumentation().runOnMainSync(() ->
    507                     getActivity().findViewById(R.id.button).requestFocus())
    508                 , (AccessibilityEvent event) ->
    509                     event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
    510                             && event.getPackageName().equals(getActivity().getPackageName())
    511                 , TIMEOUT_ASYNC_PROCESSING);
    512         } catch (TimeoutException e) {
    513             fail("Should not be able to send events from a widget package if no widget hosted");
    514         }
    515 
    516         // Create a host and start listening.
    517         final AppWidgetHost host = new AppWidgetHost(getInstrumentation().getTargetContext(), 0);
    518         host.deleteHost();
    519         host.startListening();
    520 
    521         // Well, app do not have this permission unless explicitly granted
    522         // by the user. Now we will pretend for the user and grant it.
    523         grantBindAppWidgetPermission();
    524 
    525         // Allocate an app widget id to bind.
    526         final int appWidgetId = host.allocateAppWidgetId();
    527         try {
    528             // Grab a provider we defined to be bound.
    529             final AppWidgetProviderInfo provider = getAppWidgetProviderInfo();
    530 
    531             // Bind the widget.
    532             final boolean widgetBound = getAppWidgetManager().bindAppWidgetIdIfAllowed(
    533                     appWidgetId, provider.getProfile(), provider.provider, null);
    534             assertTrue(widgetBound);
    535 
    536             // Make sure the app can use the package of a widget it hosts
    537             getInstrumentation().runOnMainSync(() -> {
    538                 // Make sure we can report nodes as if from the widget package
    539                 AccessibilityNodeInfo root = getInstrumentation().getUiAutomation()
    540                         .getRootInActiveWindow();
    541                 assertPackageName(root, APP_WIDGET_PROVIDER_PACKAGE);
    542             });
    543 
    544             // Make sure we can send events as if from the widget package
    545             try {
    546                 getInstrumentation().getUiAutomation().executeAndWaitForEvent(() ->
    547                     getInstrumentation().runOnMainSync(() ->
    548                         getActivity().findViewById(R.id.button).performClick())
    549                     , (AccessibilityEvent event) ->
    550                             event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED
    551                                     && event.getPackageName().equals(APP_WIDGET_PROVIDER_PACKAGE)
    552                     , TIMEOUT_ASYNC_PROCESSING);
    553             } catch (TimeoutException e) {
    554                 fail("Should be able to send events from a widget package if widget hosted");
    555             }
    556         } finally {
    557             // Clean up.
    558             host.deleteAppWidgetId(appWidgetId);
    559             host.deleteHost();
    560             revokeBindAppWidgetPermission();
    561         }
    562     }
    563 
    564     @MediumTest
    565     @Presubmit
    566     public void testViewHeadingReportedToAccessibility() throws Exception {
    567         final Instrumentation instrumentation = getInstrumentation();
    568         final EditText editText = (EditText) getOnMain(instrumentation, () -> {
    569             return getActivity().findViewById(R.id.edittext);
    570         });
    571         // Make sure the edittext was populated properly from xml
    572         final boolean editTextIsHeading = getOnMain(instrumentation, () -> {
    573             return editText.isAccessibilityHeading();
    574         });
    575         assertTrue("isAccessibilityHeading not populated properly from xml", editTextIsHeading);
    576 
    577         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    578         final AccessibilityNodeInfo editTextNode = uiAutomation.getRootInActiveWindow()
    579                 .findAccessibilityNodeInfosByViewId(
    580                         "android.accessibilityservice.cts:id/edittext")
    581                 .get(0);
    582         assertTrue("isAccessibilityHeading not reported to accessibility",
    583                 editTextNode.isHeading());
    584 
    585         uiAutomation.executeAndWaitForEvent(() -> instrumentation.runOnMainSync(() ->
    586                         editText.setAccessibilityHeading(false)),
    587                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
    588                 TIMEOUT_ASYNC_PROCESSING);
    589         editTextNode.refresh();
    590         assertFalse("isAccessibilityHeading not reported to accessibility after update",
    591                 editTextNode.isHeading());
    592     }
    593 
    594     @MediumTest
    595     @Presubmit
    596     public void testTooltipTextReportedToAccessibility() {
    597         final Instrumentation instrumentation = getInstrumentation();
    598         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    599         final AccessibilityNodeInfo buttonNode = uiAutomation.getRootInActiveWindow()
    600                 .findAccessibilityNodeInfosByViewId(
    601                         "android.accessibilityservice.cts:id/buttonWithTooltip")
    602                 .get(0);
    603         assertEquals("Tooltip text not reported to accessibility",
    604                 instrumentation.getContext().getString(R.string.button_tooltip),
    605                 buttonNode.getTooltipText());
    606     }
    607 
    608     @MediumTest
    609     public void testTooltipTextActionsReportedToAccessibility() throws Exception {
    610         final Instrumentation instrumentation = getInstrumentation();
    611         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    612         final AccessibilityNodeInfo buttonNode = uiAutomation.getRootInActiveWindow()
    613                 .findAccessibilityNodeInfosByViewId(
    614                         "android.accessibilityservice.cts:id/buttonWithTooltip")
    615                 .get(0);
    616         assertFalse(hasTooltipShowing(R.id.buttonWithTooltip));
    617         assertThat(ACTION_SHOW_TOOLTIP, in(buttonNode.getActionList()));
    618         assertThat(ACTION_HIDE_TOOLTIP, not(in(buttonNode.getActionList())));
    619         uiAutomation.executeAndWaitForEvent(() -> buttonNode.performAction(
    620                 ACTION_SHOW_TOOLTIP.getId()),
    621                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
    622                 TIMEOUT_ASYNC_PROCESSING);
    623 
    624         // The button should now be showing the tooltip, so it should have the option to hide it.
    625         buttonNode.refresh();
    626         assertThat(ACTION_HIDE_TOOLTIP, in(buttonNode.getActionList()));
    627         assertThat(ACTION_SHOW_TOOLTIP, not(in(buttonNode.getActionList())));
    628         assertTrue(hasTooltipShowing(R.id.buttonWithTooltip));
    629     }
    630 
    631     @MediumTest
    632     public void testTraversalBeforeReportedToAccessibility() throws Exception {
    633         final Instrumentation instrumentation = getInstrumentation();
    634         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    635         final AccessibilityNodeInfo buttonNode = uiAutomation.getRootInActiveWindow()
    636                 .findAccessibilityNodeInfosByViewId(
    637                         "android.accessibilityservice.cts:id/buttonWithTooltip")
    638                 .get(0);
    639         final AccessibilityNodeInfo beforeNode = buttonNode.getTraversalBefore();
    640         assertThat(beforeNode, notNullValue());
    641         assertThat(beforeNode.getViewIdResourceName(),
    642                 equalTo("android.accessibilityservice.cts:id/edittext"));
    643 
    644         uiAutomation.executeAndWaitForEvent(() -> instrumentation.runOnMainSync(
    645                 () -> getActivity().findViewById(R.id.buttonWithTooltip)
    646                         .setAccessibilityTraversalBefore(View.NO_ID)),
    647                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
    648                 TIMEOUT_ASYNC_PROCESSING);
    649 
    650         buttonNode.refresh();
    651         assertThat(buttonNode.getTraversalBefore(), nullValue());
    652     }
    653 
    654     @MediumTest
    655     public void testTraversalAfterReportedToAccessibility() throws Exception {
    656         final Instrumentation instrumentation = getInstrumentation();
    657         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    658         final AccessibilityNodeInfo editNode = uiAutomation.getRootInActiveWindow()
    659                 .findAccessibilityNodeInfosByViewId(
    660                         "android.accessibilityservice.cts:id/edittext")
    661                 .get(0);
    662         final AccessibilityNodeInfo afterNode = editNode.getTraversalAfter();
    663         assertThat(afterNode, notNullValue());
    664         assertThat(afterNode.getViewIdResourceName(),
    665                 equalTo("android.accessibilityservice.cts:id/buttonWithTooltip"));
    666 
    667         uiAutomation.executeAndWaitForEvent(() -> instrumentation.runOnMainSync(
    668                 () -> getActivity().findViewById(R.id.edittext)
    669                         .setAccessibilityTraversalAfter(View.NO_ID)),
    670                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
    671                 TIMEOUT_ASYNC_PROCESSING);
    672 
    673         editNode.refresh();
    674         assertThat(editNode.getTraversalAfter(), nullValue());
    675     }
    676 
    677     @MediumTest
    678     public void testLabelForReportedToAccessibility() throws Exception {
    679         final Instrumentation instrumentation = getInstrumentation();
    680         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
    681         uiAutomation.executeAndWaitForEvent(() -> instrumentation.runOnMainSync(() -> getActivity()
    682                 .findViewById(R.id.edittext).setLabelFor(R.id.buttonWithTooltip)),
    683                 filterForEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED),
    684                 TIMEOUT_ASYNC_PROCESSING);
    685         // TODO: b/78022650: This code should move above the executeAndWait event. It's here because
    686         // the a11y cache doesn't get notified when labelFor changes, so the node with the
    687         // labledBy isn't updated.
    688         final AccessibilityNodeInfo editNode = uiAutomation.getRootInActiveWindow()
    689                 .findAccessibilityNodeInfosByViewId(
    690                         "android.accessibilityservice.cts:id/edittext")
    691                 .get(0);
    692         editNode.refresh();
    693         final AccessibilityNodeInfo labelForNode = editNode.getLabelFor();
    694         assertThat(labelForNode, notNullValue());
    695         // Labeled node should indicate that it is labeled by the other one
    696         assertThat(labelForNode.getLabeledBy(), equalTo(editNode));
    697     }
    698 
    699     private static void assertPackageName(AccessibilityNodeInfo node, String packageName) {
    700         if (node == null) {
    701             return;
    702         }
    703         assertEquals(packageName, node.getPackageName());
    704         final int childCount = node.getChildCount();
    705         for (int i = 0; i < childCount; i++) {
    706             AccessibilityNodeInfo child = node.getChild(i);
    707             if (child != null) {
    708                 assertPackageName(child, packageName);
    709             }
    710         }
    711     }
    712 
    713     private AppWidgetProviderInfo getAppWidgetProviderInfo() {
    714         final ComponentName componentName = new ComponentName(
    715                 "foo.bar.baz", "foo.bar.baz.MyAppWidgetProvider");
    716         final List<AppWidgetProviderInfo> providers = getAppWidgetManager().getInstalledProviders();
    717         final int providerCount = providers.size();
    718         for (int i = 0; i < providerCount; i++) {
    719             final AppWidgetProviderInfo provider = providers.get(i);
    720             if (componentName.equals(provider.provider)
    721                     && Process.myUserHandle().equals(provider.getProfile())) {
    722                 return provider;
    723             }
    724         }
    725         return null;
    726     }
    727 
    728     private void grantBindAppWidgetPermission() throws Exception {
    729         ShellCommandBuilder.execShellCommand(getInstrumentation().getUiAutomation(),
    730                 GRANT_BIND_APP_WIDGET_PERMISSION_COMMAND);
    731     }
    732 
    733     private void revokeBindAppWidgetPermission() throws Exception {
    734         ShellCommandBuilder.execShellCommand(getInstrumentation().getUiAutomation(),
    735                 REVOKE_BIND_APP_WIDGET_PERMISSION_COMMAND);
    736     }
    737 
    738     private AppWidgetManager getAppWidgetManager() {
    739         return (AppWidgetManager) getInstrumentation().getTargetContext()
    740                 .getSystemService(Context.APPWIDGET_SERVICE);
    741     }
    742 
    743     private boolean hasAppWidgets() {
    744         return getInstrumentation().getTargetContext().getPackageManager()
    745                 .hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS);
    746     }
    747 
    748     /**
    749      * Compares all properties of the <code>first</code> and the
    750      * <code>second</code>.
    751      */
    752     private boolean equalsAccessiblityEvent(AccessibilityEvent first, AccessibilityEvent second) {
    753          return first.getEventType() == second.getEventType()
    754             && first.isChecked() == second.isChecked()
    755             && first.getCurrentItemIndex() == second.getCurrentItemIndex()
    756             && first.isEnabled() == second.isEnabled()
    757             && first.getFromIndex() == second.getFromIndex()
    758             && first.getItemCount() == second.getItemCount()
    759             && first.isPassword() == second.isPassword()
    760             && first.getRemovedCount() == second.getRemovedCount()
    761             && first.isScrollable()== second.isScrollable()
    762             && first.getToIndex() == second.getToIndex()
    763             && first.getRecordCount() == second.getRecordCount()
    764             && first.getScrollX() == second.getScrollX()
    765             && first.getScrollY() == second.getScrollY()
    766             && first.getAddedCount() == second.getAddedCount()
    767             && TextUtils.equals(first.getBeforeText(), second.getBeforeText())
    768             && TextUtils.equals(first.getClassName(), second.getClassName())
    769             && TextUtils.equals(first.getContentDescription(), second.getContentDescription())
    770             && equalsNotificationAsParcelableData(first, second)
    771             && equalsText(first, second);
    772     }
    773 
    774     /**
    775      * Compares the {@link android.os.Parcelable} data of the
    776      * <code>first</code> and <code>second</code>.
    777      */
    778     private boolean equalsNotificationAsParcelableData(AccessibilityEvent first,
    779             AccessibilityEvent second) {
    780         Notification firstNotification = (Notification) first.getParcelableData();
    781         Notification secondNotification = (Notification) second.getParcelableData();
    782         if (firstNotification == null) {
    783             return (secondNotification == null);
    784         } else if (secondNotification == null) {
    785             return false;
    786         }
    787         return TextUtils.equals(firstNotification.tickerText, secondNotification.tickerText);
    788     }
    789 
    790     /**
    791      * Compares the text of the <code>first</code> and <code>second</code> text.
    792      */
    793     private boolean equalsText(AccessibilityEvent first, AccessibilityEvent second) {
    794         List<CharSequence> firstText = first.getText();
    795         List<CharSequence> secondText = second.getText();
    796         if (firstText.size() != secondText.size()) {
    797             return false;
    798         }
    799         Iterator<CharSequence> firstIterator = firstText.iterator();
    800         Iterator<CharSequence> secondIterator = secondText.iterator();
    801         for (int i = 0; i < firstText.size(); i++) {
    802             if (!firstIterator.next().toString().equals(secondIterator.next().toString())) {
    803                 return false;
    804             }
    805         }
    806         return true;
    807     }
    808 
    809     private boolean hasTooltipShowing(int id) {
    810         return getOnMain(getInstrumentation(), () -> {
    811             final View viewWithTooltip = getActivity().findViewById(id);
    812             if (viewWithTooltip == null) {
    813                 return false;
    814             }
    815             final View tooltipView = viewWithTooltip.getTooltipView();
    816             return (tooltipView != null) && (tooltipView.getParent() != null);
    817         });
    818     }
    819 }
    820