1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.usepermission; 18 19 import static junit.framework.Assert.assertEquals; 20 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.fail; 23 24 import android.Manifest; 25 import android.app.Activity; 26 import android.app.Instrumentation; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.res.Resources; 31 import android.icu.text.CaseMap; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.SystemClock; 35 import android.provider.Settings; 36 import android.support.test.uiautomator.By; 37 import android.support.test.uiautomator.BySelector; 38 import android.support.test.uiautomator.Direction; 39 import android.support.test.uiautomator.UiDevice; 40 import android.support.test.uiautomator.UiObject2; 41 import android.support.test.uiautomator.UiScrollable; 42 import android.support.test.uiautomator.UiSelector; 43 import android.support.test.uiautomator.Until; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.view.accessibility.AccessibilityNodeInfo; 48 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 49 import android.widget.ScrollView; 50 51 import androidx.test.InstrumentationRegistry; 52 import androidx.test.runner.AndroidJUnit4; 53 54 import junit.framework.Assert; 55 56 import org.junit.Before; 57 import org.junit.runner.RunWith; 58 59 import java.util.List; 60 import java.util.Map; 61 import java.util.concurrent.Callable; 62 import java.util.concurrent.TimeoutException; 63 import java.util.regex.Pattern; 64 65 @RunWith(AndroidJUnit4.class) 66 public abstract class BasePermissionsTest { 67 private static final String PLATFORM_PACKAGE_NAME = "android"; 68 69 private static final long IDLE_TIMEOUT_MILLIS = 1000; 70 private static final long GLOBAL_TIMEOUT_MILLIS = 10000; 71 72 private static final long RETRY_TIMEOUT = 10 * GLOBAL_TIMEOUT_MILLIS; 73 private static final String LOG_TAG = "BasePermissionsTest"; 74 75 private static Map<String, String> sPermissionToLabelResNameMap = new ArrayMap<>(); 76 77 private Context mContext; 78 private Resources mPlatformResources; 79 private boolean mWatch; 80 getInstrumentation()81 protected static Instrumentation getInstrumentation() { 82 return InstrumentationRegistry.getInstrumentation(); 83 } 84 assertPermissionRequestResult(BasePermissionActivity.Result result, int requestCode, String[] permissions, boolean[] granted)85 protected static void assertPermissionRequestResult(BasePermissionActivity.Result result, 86 int requestCode, String[] permissions, boolean[] granted) { 87 assertEquals(requestCode, result.requestCode); 88 for (int i = 0; i < permissions.length; i++) { 89 assertEquals(permissions[i], result.permissions[i]); 90 assertEquals(granted[i] ? PackageManager.PERMISSION_GRANTED 91 : PackageManager.PERMISSION_DENIED, result.grantResults[i]); 92 93 } 94 } 95 getUiDevice()96 protected static UiDevice getUiDevice() { 97 return UiDevice.getInstance(getInstrumentation()); 98 } 99 launchActivity(String packageName, Class<?> clazz, Bundle extras)100 protected static Activity launchActivity(String packageName, 101 Class<?> clazz, Bundle extras) { 102 Intent intent = new Intent(Intent.ACTION_MAIN); 103 intent.setClassName(packageName, clazz.getName()); 104 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 105 if (extras != null) { 106 intent.putExtras(extras); 107 } 108 Activity activity = getInstrumentation().startActivitySync(intent); 109 getInstrumentation().waitForIdleSync(); 110 111 return activity; 112 } 113 initPermissionToLabelMap(boolean permissionReviewMode)114 private void initPermissionToLabelMap(boolean permissionReviewMode) { 115 if (!permissionReviewMode) { 116 // Contacts 117 sPermissionToLabelResNameMap.put( 118 Manifest.permission.READ_CONTACTS, "@android:string/permgrouplab_contacts"); 119 sPermissionToLabelResNameMap.put( 120 Manifest.permission.WRITE_CONTACTS, "@android:string/permgrouplab_contacts"); 121 // Calendar 122 sPermissionToLabelResNameMap.put( 123 Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar"); 124 sPermissionToLabelResNameMap.put( 125 Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar"); 126 // SMS 127 sPermissionToLabelResNameMap.put( 128 Manifest.permission.SEND_SMS, "@android:string/permgrouplab_sms"); 129 sPermissionToLabelResNameMap.put( 130 Manifest.permission.RECEIVE_SMS, "@android:string/permgrouplab_sms"); 131 sPermissionToLabelResNameMap.put( 132 Manifest.permission.READ_SMS, "@android:string/permgrouplab_sms"); 133 sPermissionToLabelResNameMap.put( 134 Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permgrouplab_sms"); 135 sPermissionToLabelResNameMap.put( 136 Manifest.permission.RECEIVE_MMS, "@android:string/permgrouplab_sms"); 137 sPermissionToLabelResNameMap.put( 138 "android.permission.READ_CELL_BROADCASTS", "@android:string/permgrouplab_sms"); 139 // Storage 140 sPermissionToLabelResNameMap.put( 141 Manifest.permission.READ_EXTERNAL_STORAGE, 142 "@android:string/permgrouplab_storage"); 143 sPermissionToLabelResNameMap.put( 144 Manifest.permission.WRITE_EXTERNAL_STORAGE, 145 "@android:string/permgrouplab_storage"); 146 // Location 147 sPermissionToLabelResNameMap.put( 148 Manifest.permission.ACCESS_FINE_LOCATION, 149 "@android:string/permgrouplab_location"); 150 sPermissionToLabelResNameMap.put( 151 Manifest.permission.ACCESS_COARSE_LOCATION, 152 "@android:string/permgrouplab_location"); 153 // Phone 154 sPermissionToLabelResNameMap.put( 155 Manifest.permission.READ_PHONE_STATE, "@android:string/permgrouplab_phone"); 156 sPermissionToLabelResNameMap.put( 157 Manifest.permission.CALL_PHONE, "@android:string/permgrouplab_phone"); 158 sPermissionToLabelResNameMap.put( 159 "android.permission.ACCESS_IMS_CALL_SERVICE", 160 "@android:string/permgrouplab_phone"); 161 sPermissionToLabelResNameMap.put( 162 Manifest.permission.READ_CALL_LOG, "@android:string/permgrouplab_phone"); 163 sPermissionToLabelResNameMap.put( 164 Manifest.permission.WRITE_CALL_LOG, "@android:string/permgrouplab_phone"); 165 sPermissionToLabelResNameMap.put( 166 Manifest.permission.ADD_VOICEMAIL, "@android:string/permgrouplab_phone"); 167 sPermissionToLabelResNameMap.put( 168 Manifest.permission.USE_SIP, "@android:string/permgrouplab_phone"); 169 sPermissionToLabelResNameMap.put( 170 Manifest.permission.PROCESS_OUTGOING_CALLS, 171 "@android:string/permgrouplab_phone"); 172 // Microphone 173 sPermissionToLabelResNameMap.put( 174 Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone"); 175 // Camera 176 sPermissionToLabelResNameMap.put( 177 Manifest.permission.CAMERA, "@android:string/permgrouplab_camera"); 178 // Body sensors 179 sPermissionToLabelResNameMap.put( 180 Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors"); 181 } else { 182 // Contacts 183 sPermissionToLabelResNameMap.put( 184 Manifest.permission.READ_CONTACTS, "@android:string/permlab_readContacts"); 185 sPermissionToLabelResNameMap.put( 186 Manifest.permission.WRITE_CONTACTS, "@android:string/permlab_writeContacts"); 187 // Calendar 188 sPermissionToLabelResNameMap.put( 189 Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar"); 190 sPermissionToLabelResNameMap.put( 191 Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar"); 192 // SMS 193 sPermissionToLabelResNameMap.put( 194 Manifest.permission.SEND_SMS, "@android:string/permlab_sendSms"); 195 sPermissionToLabelResNameMap.put( 196 Manifest.permission.RECEIVE_SMS, "@android:string/permlab_receiveSms"); 197 sPermissionToLabelResNameMap.put( 198 Manifest.permission.READ_SMS, "@android:string/permlab_readSms"); 199 sPermissionToLabelResNameMap.put( 200 Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permlab_receiveWapPush"); 201 sPermissionToLabelResNameMap.put( 202 Manifest.permission.RECEIVE_MMS, "@android:string/permlab_receiveMms"); 203 sPermissionToLabelResNameMap.put( 204 "android.permission.READ_CELL_BROADCASTS", 205 "@android:string/permlab_readCellBroadcasts"); 206 // Storage 207 sPermissionToLabelResNameMap.put( 208 Manifest.permission.READ_EXTERNAL_STORAGE, 209 "@android:string/permgrouplab_storage"); 210 sPermissionToLabelResNameMap.put( 211 Manifest.permission.WRITE_EXTERNAL_STORAGE, 212 "@android:string/permgrouplab_storage"); 213 // Location 214 sPermissionToLabelResNameMap.put( 215 Manifest.permission.ACCESS_FINE_LOCATION, 216 "@android:string/permgrouplab_location"); 217 sPermissionToLabelResNameMap.put( 218 Manifest.permission.ACCESS_COARSE_LOCATION, 219 "@android:string/permgrouplab_location"); 220 // Phone 221 sPermissionToLabelResNameMap.put( 222 Manifest.permission.READ_PHONE_STATE, "@android:string/permlab_readPhoneState"); 223 sPermissionToLabelResNameMap.put( 224 Manifest.permission.CALL_PHONE, "@android:string/permlab_callPhone"); 225 sPermissionToLabelResNameMap.put( 226 "android.permission.ACCESS_IMS_CALL_SERVICE", 227 "@android:string/permlab_accessImsCallService"); 228 sPermissionToLabelResNameMap.put( 229 Manifest.permission.READ_CALL_LOG, "@android:string/permlab_readCallLog"); 230 sPermissionToLabelResNameMap.put( 231 Manifest.permission.WRITE_CALL_LOG, "@android:string/permlab_writeCallLog"); 232 sPermissionToLabelResNameMap.put( 233 Manifest.permission.ADD_VOICEMAIL, "@android:string/permlab_addVoicemail"); 234 sPermissionToLabelResNameMap.put( 235 Manifest.permission.USE_SIP, "@android:string/permlab_use_sip"); 236 sPermissionToLabelResNameMap.put( 237 Manifest.permission.PROCESS_OUTGOING_CALLS, 238 "@android:string/permlab_processOutgoingCalls"); 239 // Microphone 240 sPermissionToLabelResNameMap.put( 241 Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone"); 242 // Camera 243 sPermissionToLabelResNameMap.put( 244 Manifest.permission.CAMERA, "@android:string/permgrouplab_camera"); 245 // Body sensors 246 sPermissionToLabelResNameMap.put( 247 Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors"); 248 } 249 } 250 251 @Before beforeTest()252 public void beforeTest() { 253 mContext = InstrumentationRegistry.getTargetContext(); 254 try { 255 Context platformContext = mContext.createPackageContext(PLATFORM_PACKAGE_NAME, 0); 256 mPlatformResources = platformContext.getResources(); 257 } catch (PackageManager.NameNotFoundException e) { 258 /* cannot happen */ 259 } 260 261 PackageManager packageManager = mContext.getPackageManager(); 262 mWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH); 263 initPermissionToLabelMap(packageManager.arePermissionsIndividuallyControlled()); 264 265 UiObject2 button = getUiDevice().findObject(By.text("Close")); 266 if (button != null) { 267 button.click(); 268 } 269 } 270 requestPermissions( String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction)271 protected BasePermissionActivity.Result requestPermissions( 272 String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction) 273 throws Exception { 274 // Start an activity 275 BasePermissionActivity activity = (BasePermissionActivity) launchActivity( 276 getInstrumentation().getTargetContext().getPackageName(), clazz, null); 277 278 activity.waitForOnCreate(); 279 280 // Request the permissions 281 activity.requestPermissions(permissions, requestCode); 282 283 // Define a more conservative idle criteria 284 getInstrumentation().getUiAutomation().waitForIdle( 285 IDLE_TIMEOUT_MILLIS, GLOBAL_TIMEOUT_MILLIS); 286 287 // Perform the post-request action 288 if (postRequestAction != null) { 289 postRequestAction.run(); 290 } 291 292 BasePermissionActivity.Result result = activity.getResult(); 293 activity.finish(); 294 return result; 295 } 296 clickAllowButton()297 protected void clickAllowButton() throws Exception { 298 scrollToBottomIfWatch(); 299 waitForIdle(); 300 getUiDevice().wait(Until.findObject(By.res( 301 "com.android.permissioncontroller:id/permission_allow_button")), 302 GLOBAL_TIMEOUT_MILLIS).click(); 303 } 304 clickAllowAlwaysButton()305 protected void clickAllowAlwaysButton() throws Exception { 306 waitForIdle(); 307 getUiDevice().wait(Until.findObject(By.res( 308 "com.android.permissioncontroller:id/permission_allow_always_button")), 309 GLOBAL_TIMEOUT_MILLIS).click(); 310 } 311 clickAllowForegroundButton()312 protected void clickAllowForegroundButton() throws Exception { 313 waitForIdle(); 314 getUiDevice().wait(Until.findObject(By.res( 315 "com.android.permissioncontroller:id/permission_allow_foreground_only_button")), 316 GLOBAL_TIMEOUT_MILLIS).click(); 317 } 318 clickDenyButton()319 protected void clickDenyButton() throws Exception { 320 scrollToBottomIfWatch(); 321 waitForIdle(); 322 getUiDevice().wait(Until.findObject(By.res( 323 "com.android.permissioncontroller:id/permission_deny_button")), 324 GLOBAL_TIMEOUT_MILLIS).click(); 325 } 326 clickDenyAndDontAskAgainButton()327 protected void clickDenyAndDontAskAgainButton() throws Exception { 328 waitForIdle(); 329 getUiDevice().wait(Until.findObject(By.res( 330 "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button")), 331 GLOBAL_TIMEOUT_MILLIS).click(); 332 } 333 clickDontAskAgainButton()334 protected void clickDontAskAgainButton() throws Exception { 335 scrollToBottomIfWatch(); 336 waitForIdle(); 337 getUiDevice().wait(Until.findObject(By.res( 338 "com.android.permissioncontroller:id/permission_deny_dont_ask_again_button")), 339 GLOBAL_TIMEOUT_MILLIS).click(); 340 } 341 grantPermission(String permission)342 protected void grantPermission(String permission) throws Exception { 343 grantPermissions(new String[]{permission}); 344 } 345 grantPermissions(String[] permissions)346 protected void grantPermissions(String[] permissions) throws Exception { 347 setPermissionGrantState(permissions, true, false); 348 } 349 revokePermission(String permission)350 protected void revokePermission(String permission) throws Exception { 351 revokePermissions(new String[] {permission}, false); 352 } 353 revokePermissions(String[] permissions, boolean legacyApp)354 protected void revokePermissions(String[] permissions, boolean legacyApp) throws Exception { 355 setPermissionGrantState(permissions, false, legacyApp); 356 } 357 scrollToBottomIfWatch()358 private void scrollToBottomIfWatch() throws Exception { 359 if (mWatch) { 360 getUiDevice().wait(Until.findObject(By.clazz(ScrollView.class)), GLOBAL_TIMEOUT_MILLIS); 361 UiScrollable scrollable = 362 new UiScrollable(new UiSelector().className(ScrollView.class)); 363 if (scrollable.exists()) { 364 scrollable.flingToEnd(10); 365 } 366 } 367 } 368 setPermissionGrantState(String[] permissions, boolean granted, boolean legacyApp)369 private void setPermissionGrantState(String[] permissions, boolean granted, 370 boolean legacyApp) throws Exception { 371 getUiDevice().pressBack(); 372 waitForIdle(); 373 getUiDevice().pressBack(); 374 waitForIdle(); 375 getUiDevice().pressBack(); 376 waitForIdle(); 377 378 if (isTv()) { 379 getUiDevice().pressHome(); 380 waitForIdle(); 381 } 382 383 // Open the app details settings 384 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 385 intent.addCategory(Intent.CATEGORY_DEFAULT); 386 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 387 intent.setData(Uri.parse("package:" + mContext.getPackageName())); 388 startActivity(intent); 389 390 waitForIdle(); 391 392 // Open the permissions UI 393 String label = mContext.getResources().getString(R.string.Permissions); 394 AccessibilityNodeInfo permLabelView = getNodeTimed(() -> findByText(label), true); 395 Assert.assertNotNull("Permissions label should be present", permLabelView); 396 397 AccessibilityNodeInfo permItemView = findCollectionItem(permLabelView); 398 399 click(permItemView); 400 401 waitForIdle(); 402 403 for (String permission : permissions) { 404 // Find the permission screen 405 String permissionLabel = getPermissionLabel(permission); 406 407 UiObject2 permissionView = null; 408 long start = System.currentTimeMillis(); 409 while (permissionView == null && start + RETRY_TIMEOUT > System.currentTimeMillis()) { 410 permissionView = getUiDevice().wait(Until.findObject(By.text(permissionLabel)), 411 GLOBAL_TIMEOUT_MILLIS); 412 413 if (permissionView == null) { 414 getUiDevice().findObject(By.res("android:id/list_container")) 415 .scroll(Direction.DOWN, 1); 416 } 417 } 418 419 permissionView.click(); 420 waitForIdle(); 421 422 String denyLabel = mContext.getResources().getString(R.string.Deny); 423 424 final boolean wasGranted = !getUiDevice().wait(Until.findObject(By.text(denyLabel)), 425 GLOBAL_TIMEOUT_MILLIS).isChecked(); 426 if (granted != wasGranted) { 427 // Toggle the permission 428 429 if (granted) { 430 String allowLabel = mContext.getResources().getString(R.string.Allow); 431 getUiDevice().findObject(By.text(allowLabel)).click(); 432 } else { 433 getUiDevice().findObject(By.text(denyLabel)).click(); 434 } 435 waitForIdle(); 436 437 if (wasGranted && legacyApp) { 438 scrollToBottomIfWatch(); 439 Context context = getInstrumentation().getContext(); 440 String packageName = context.getPackageManager() 441 .getPermissionControllerPackageName(); 442 String resIdName = "com.android.permissioncontroller" 443 + ":string/grant_dialog_button_deny_anyway"; 444 Resources resources = context 445 .createPackageContext(packageName, 0).getResources(); 446 final int confirmResId = resources.getIdentifier(resIdName, null, null); 447 String confirmTitle = CaseMap.toUpper().apply( 448 resources.getConfiguration().getLocales().get(0), 449 resources.getString(confirmResId)); 450 getUiDevice().wait(Until.findObject( 451 byTextStartsWithCaseInsensitive(confirmTitle)), 452 GLOBAL_TIMEOUT_MILLIS).click(); 453 454 waitForIdle(); 455 } 456 } 457 458 getUiDevice().pressBack(); 459 waitForIdle(); 460 } 461 462 getUiDevice().pressBack(); 463 waitForIdle(); 464 getUiDevice().pressBack(); 465 waitForIdle(); 466 } 467 byTextStartsWithCaseInsensitive(String prefix)468 private BySelector byTextStartsWithCaseInsensitive(String prefix) { 469 return By.text(Pattern.compile(String.format("(?i)^%s.*$", Pattern.quote(prefix)))); 470 } 471 getPermissionLabel(String permission)472 private String getPermissionLabel(String permission) throws Exception { 473 String labelResName = sPermissionToLabelResNameMap.get(permission); 474 assertNotNull("Unknown permisison " + permission, labelResName); 475 final int resourceId = mPlatformResources.getIdentifier(labelResName, null, null); 476 return mPlatformResources.getString(resourceId); 477 } 478 startActivity(final Intent intent)479 private void startActivity(final Intent intent) throws Exception { 480 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 481 () -> { 482 try { 483 getInstrumentation().getContext().startActivity(intent); 484 } catch (Exception e) { 485 Log.e(LOG_TAG, "Cannot start activity: " + intent, e); 486 fail("Cannot start activity: " + intent); 487 } 488 }, (AccessibilityEvent event) -> event.getEventType() 489 == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 490 , GLOBAL_TIMEOUT_MILLIS); 491 } 492 findByText(String text)493 private AccessibilityNodeInfo findByText(String text) throws Exception { 494 AccessibilityNodeInfo root = getInstrumentation().getUiAutomation().getRootInActiveWindow(); 495 AccessibilityNodeInfo result = findByText(root, text); 496 if (result != null) { 497 return result; 498 } 499 return findByTextInCollection(root, text); 500 } 501 findByText(AccessibilityNodeInfo root, String text)502 private static AccessibilityNodeInfo findByText(AccessibilityNodeInfo root, String text) { 503 List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(text); 504 for (AccessibilityNodeInfo node : nodes) { 505 if (node.getText().toString().equals(text)) { 506 return node; 507 } 508 } 509 return null; 510 } 511 findByTextInCollection(AccessibilityNodeInfo root, String text)512 private static AccessibilityNodeInfo findByTextInCollection(AccessibilityNodeInfo root, 513 String text) throws Exception { 514 AccessibilityNodeInfo result; 515 final int childCount = root.getChildCount(); 516 for (int i = 0; i < childCount; i++) { 517 AccessibilityNodeInfo child = root.getChild(i); 518 if (child == null) { 519 continue; 520 } 521 if (child.getCollectionInfo() != null) { 522 scrollTop(child); 523 result = getNodeTimed(() -> findByText(child, text), false); 524 if (result != null) { 525 return result; 526 } 527 try { 528 while (child.getActionList().contains( 529 AccessibilityAction.ACTION_SCROLL_FORWARD) || child.getActionList() 530 .contains(AccessibilityAction.ACTION_SCROLL_DOWN)) { 531 scrollForward(child); 532 result = getNodeTimed(() -> findByText(child, text), false); 533 if (result != null) { 534 return result; 535 } 536 } 537 } catch (TimeoutException e) { 538 /* ignore */ 539 } 540 } else { 541 result = findByTextInCollection(child, text); 542 if (result != null) { 543 return result; 544 } 545 } 546 } 547 return null; 548 } 549 scrollTop(AccessibilityNodeInfo node)550 private static void scrollTop(AccessibilityNodeInfo node) throws Exception { 551 try { 552 while (node.getActionList().contains(AccessibilityAction.ACTION_SCROLL_BACKWARD)) { 553 scroll(node, false); 554 } 555 } catch (TimeoutException e) { 556 /* ignore */ 557 } 558 } 559 scrollForward(AccessibilityNodeInfo node)560 private static void scrollForward(AccessibilityNodeInfo node) throws Exception { 561 scroll(node, true); 562 } 563 scroll(AccessibilityNodeInfo node, boolean forward)564 private static void scroll(AccessibilityNodeInfo node, boolean forward) throws Exception { 565 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 566 () -> { 567 if (isTv()) { 568 if (forward) { 569 getUiDevice().pressDPadDown(); 570 } else { 571 for (int i = 0; i < 50; i++) { 572 getUiDevice().pressDPadUp(); 573 } 574 } 575 } else { 576 node.performAction(forward 577 ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD 578 : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 579 } 580 }, 581 (AccessibilityEvent event) -> event.getEventType() 582 == AccessibilityEvent.TYPE_VIEW_SCROLLED 583 || event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 584 GLOBAL_TIMEOUT_MILLIS); 585 node.refresh(); 586 waitForIdle(); 587 } 588 click(AccessibilityNodeInfo node)589 private static void click(AccessibilityNodeInfo node) throws Exception { 590 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 591 () -> node.performAction(AccessibilityNodeInfo.ACTION_CLICK), 592 (AccessibilityEvent event) -> event.getEventType() 593 == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED 594 || event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED, 595 GLOBAL_TIMEOUT_MILLIS); 596 } 597 findCollectionItem(AccessibilityNodeInfo current)598 private static AccessibilityNodeInfo findCollectionItem(AccessibilityNodeInfo current) 599 throws Exception { 600 AccessibilityNodeInfo result = current; 601 while (result != null) { 602 // Nodes that are in the hierarchy but not yet on screen may not have collection item 603 // info populated. Use a parent with collection info as an indicator in those cases. 604 if (result.getCollectionItemInfo() != null || hasCollectionAsParent(result)) { 605 return result; 606 } 607 result = result.getParent(); 608 } 609 return null; 610 } 611 hasCollectionAsParent(AccessibilityNodeInfo node)612 private static boolean hasCollectionAsParent(AccessibilityNodeInfo node) { 613 return node.getParent() != null && node.getParent().getCollectionInfo() != null; 614 } 615 getNodeTimed( Callable<AccessibilityNodeInfo> callable, boolean retry)616 private static AccessibilityNodeInfo getNodeTimed( 617 Callable<AccessibilityNodeInfo> callable, boolean retry) throws Exception { 618 final long startTimeMillis = SystemClock.uptimeMillis(); 619 while (true) { 620 try { 621 AccessibilityNodeInfo node = callable.call(); 622 623 if (node != null) { 624 return node; 625 } 626 } catch (NullPointerException e) { 627 Log.e(LOG_TAG, "NPE while finding AccessibilityNodeInfo", e); 628 } 629 630 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 631 if (!retry || elapsedTimeMillis > RETRY_TIMEOUT) { 632 return null; 633 } 634 SystemClock.sleep(2 * elapsedTimeMillis); 635 } 636 } 637 waitForIdle()638 private static void waitForIdle() throws TimeoutException { 639 getInstrumentation().getUiAutomation().waitForIdle(IDLE_TIMEOUT_MILLIS, 640 GLOBAL_TIMEOUT_MILLIS); 641 } 642 isTv()643 private static boolean isTv() { 644 return getInstrumentation().getContext().getPackageManager() 645 .hasSystemFeature(PackageManager.FEATURE_LEANBACK); 646 } 647 } 648