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