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