1 /* <lambda>null2 * 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 android.permission3.cts 18 19 import android.app.Activity 20 import android.app.Instrumentation 21 import android.content.ComponentName 22 import android.content.Intent 23 import android.content.pm.PackageManager 24 import android.net.Uri 25 import android.os.Build 26 import android.provider.Settings 27 import android.support.test.uiautomator.By 28 import android.support.test.uiautomator.BySelector 29 import android.support.test.uiautomator.UiScrollable 30 import android.support.test.uiautomator.UiSelector 31 import android.support.test.uiautomator.StaleObjectException 32 import android.text.Spanned 33 import android.text.style.ClickableSpan 34 import android.util.Log 35 import android.view.View 36 import com.android.compatibility.common.util.SystemUtil.eventually 37 import org.junit.After 38 import org.junit.Assert.assertEquals 39 import org.junit.Assert.assertNotNull 40 import org.junit.Assert.assertTrue 41 import org.junit.Before 42 import java.util.concurrent.TimeUnit 43 import java.util.regex.Pattern 44 45 abstract class BaseUsePermissionTest : BasePermissionTest() { 46 companion object { 47 const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk" 48 const val APP_APK_PATH_22_CALENDAR_ONLY = 49 "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk" 50 const val APP_APK_PATH_22_NONE = "$APK_DIRECTORY/CtsUsePermissionApp22None.apk" 51 const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk" 52 const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk" 53 const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk" 54 const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk" 55 const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk" 56 const val APP_APK_PATH_30 = "$APK_DIRECTORY/CtsUsePermissionApp30.apk" 57 const val APP_APK_PATH_30_WITH_BACKGROUND = 58 "$APK_DIRECTORY/CtsUsePermissionApp30WithBackground.apk" 59 const val APP_APK_PATH_30_WITH_BLUETOOTH = 60 "$APK_DIRECTORY/CtsUsePermissionApp30WithBluetooth.apk" 61 const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk" 62 const val APP_APK_PATH_LATEST_NONE = "$APK_DIRECTORY/CtsUsePermissionAppLatestNone.apk" 63 const val APP_APK_PATH_WITH_OVERLAY = "$APK_DIRECTORY/CtsUsePermissionAppWithOverlay.apk" 64 const val APP_PACKAGE_NAME = "android.permission3.cts.usepermission" 65 66 const val ALLOW_BUTTON = 67 "com.android.permissioncontroller:id/permission_allow_button" 68 const val ALLOW_FOREGROUND_BUTTON = 69 "com.android.permissioncontroller:id/permission_allow_foreground_only_button" 70 const val DENY_BUTTON = "com.android.permissioncontroller:id/permission_deny_button" 71 const val DENY_AND_DONT_ASK_AGAIN_BUTTON = 72 "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button" 73 const val NO_UPGRADE_BUTTON = 74 "com.android.permissioncontroller:id/permission_no_upgrade_button" 75 const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON = 76 "com.android.permissioncontroller:" + 77 "id/permission_no_upgrade_and_dont_ask_again_button" 78 79 const val ALLOW_RADIO_BUTTON = "com.android.permissioncontroller:id/allow_radio_button" 80 const val ALLOW_FOREGROUND_RADIO_BUTTON = 81 "com.android.permissioncontroller:id/allow_foreground_only_radio_button" 82 const val ASK_RADIO_BUTTON = "com.android.permissioncontroller:id/ask_radio_button" 83 const val DENY_RADIO_BUTTON = "com.android.permissioncontroller:id/deny_radio_button" 84 85 const val ALLOW_BUTTON_TEXT = "grant_dialog_button_allow" 86 const val ALLOW_FOREGROUND_BUTTON_TEXT = "grant_dialog_button_allow_foreground" 87 const val ALLOW_FOREGROUND_PREFERENCE_TEXT = "permission_access_only_foreground" 88 const val ASK_BUTTON_TEXT = "app_permission_button_ask" 89 const val ALLOW_ONE_TIME_BUTTON_TEXT = "grant_dialog_button_allow_one_time" 90 const val DENY_BUTTON_TEXT = "grant_dialog_button_deny" 91 const val DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT = 92 "grant_dialog_button_deny_and_dont_ask_again" 93 const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT = "grant_dialog_button_no_upgrade" 94 } 95 96 enum class PermissionState { 97 ALLOWED, 98 DENIED, 99 DENIED_WITH_PREJUDICE 100 } 101 102 protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 103 protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) 104 protected val isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) 105 106 private val platformResources = context.createPackageContext("android", 0).resources 107 private val permissionToLabelResNameMap = mapOf( 108 // Contacts 109 android.Manifest.permission.READ_CONTACTS 110 to "@android:string/permgrouplab_contacts", 111 android.Manifest.permission.WRITE_CONTACTS 112 to "@android:string/permgrouplab_contacts", 113 // Calendar 114 android.Manifest.permission.READ_CALENDAR 115 to "@android:string/permgrouplab_calendar", 116 android.Manifest.permission.WRITE_CALENDAR 117 to "@android:string/permgrouplab_calendar", 118 // SMS 119 android.Manifest.permission.SEND_SMS to "@android:string/permgrouplab_sms", 120 android.Manifest.permission.RECEIVE_SMS to "@android:string/permgrouplab_sms", 121 android.Manifest.permission.READ_SMS to "@android:string/permgrouplab_sms", 122 android.Manifest.permission.RECEIVE_WAP_PUSH to "@android:string/permgrouplab_sms", 123 android.Manifest.permission.RECEIVE_MMS to "@android:string/permgrouplab_sms", 124 "android.permission.READ_CELL_BROADCASTS" to "@android:string/permgrouplab_sms", 125 // Storage 126 android.Manifest.permission.READ_EXTERNAL_STORAGE 127 to "@android:string/permgrouplab_storage", 128 android.Manifest.permission.WRITE_EXTERNAL_STORAGE 129 to "@android:string/permgrouplab_storage", 130 // Location 131 android.Manifest.permission.ACCESS_FINE_LOCATION 132 to "@android:string/permgrouplab_location", 133 android.Manifest.permission.ACCESS_COARSE_LOCATION 134 to "@android:string/permgrouplab_location", 135 // Phone 136 android.Manifest.permission.READ_PHONE_STATE 137 to "@android:string/permgrouplab_phone", 138 android.Manifest.permission.CALL_PHONE to "@android:string/permgrouplab_phone", 139 "android.permission.ACCESS_IMS_CALL_SERVICE" 140 to "@android:string/permgrouplab_phone", 141 android.Manifest.permission.READ_CALL_LOG to "@android:string/permgrouplab_phone", 142 android.Manifest.permission.WRITE_CALL_LOG to "@android:string/permgrouplab_phone", 143 android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permgrouplab_phone", 144 android.Manifest.permission.USE_SIP to "@android:string/permgrouplab_phone", 145 android.Manifest.permission.PROCESS_OUTGOING_CALLS 146 to "@android:string/permgrouplab_phone", 147 // Microphone 148 android.Manifest.permission.RECORD_AUDIO 149 to "@android:string/permgrouplab_microphone", 150 // Camera 151 android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera", 152 // Body sensors 153 android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors", 154 // Bluetooth 155 android.Manifest.permission.BLUETOOTH_CONNECT to 156 "@android:string/permgrouplab_nearby_devices", 157 android.Manifest.permission.BLUETOOTH_SCAN to 158 "@android:string/permgrouplab_nearby_devices" 159 ) 160 161 @Before 162 @After 163 fun uninstallApp() { 164 uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false) 165 } 166 167 protected fun clearTargetSdkWarning() = 168 click(By.res("android:id/button1")) 169 170 protected fun clickPermissionReviewContinue() { 171 if (isAutomotive || isWatch) { 172 click(By.text(getPermissionControllerString("review_button_continue"))) 173 } else { 174 click(By.res("com.android.permissioncontroller:id/continue_button")) 175 } 176 } 177 178 protected fun clickPermissionReviewCancel() { 179 if (isAutomotive || isWatch) { 180 click(By.text(getPermissionControllerString("review_button_cancel"))) 181 } else { 182 click(By.res("com.android.permissioncontroller:id/cancel_button")) 183 } 184 } 185 186 protected fun approvePermissionReview() { 187 startAppActivityAndAssertResultCode(Activity.RESULT_OK) { 188 clickPermissionReviewContinue() 189 } 190 } 191 192 protected fun cancelPermissionReview() { 193 startAppActivityAndAssertResultCode(Activity.RESULT_CANCELED) { 194 clickPermissionReviewCancel() 195 } 196 } 197 198 protected fun assertAppDoesNotNeedPermissionReview() { 199 startAppActivityAndAssertResultCode(Activity.RESULT_OK) {} 200 } 201 202 protected inline fun startAppActivityAndAssertResultCode( 203 expectedResultCode: Int, 204 block: () -> Unit 205 ) { 206 val future = startActivityForFuture( 207 Intent().apply { 208 component = ComponentName( 209 APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity" 210 ) 211 } 212 ) 213 block() 214 assertEquals( 215 expectedResultCode, future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).resultCode 216 ) 217 } 218 219 protected inline fun requestAppPermissionsForNoResult( 220 vararg permissions: String?, 221 block: () -> Unit 222 ) { 223 // Request the permissions 224 context.startActivity( 225 Intent().apply { 226 component = ComponentName( 227 APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.RequestPermissionsActivity" 228 ) 229 putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions) 230 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 231 } 232 ) 233 waitForIdle() 234 // Perform the post-request action 235 block() 236 } 237 238 protected inline fun requestAppPermissions( 239 vararg permissions: String?, 240 block: () -> Unit 241 ): Instrumentation.ActivityResult { 242 // Request the permissions 243 val future = startActivityForFuture( 244 Intent().apply { 245 component = ComponentName( 246 APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.RequestPermissionsActivity" 247 ) 248 putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions) 249 } 250 ) 251 waitForIdle() 252 // Perform the post-request action 253 block() 254 return future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 255 } 256 257 protected inline fun requestAppPermissionsAndAssertResult( 258 permissions: Array<out String?>, 259 permissionAndExpectedGrantResults: Array<out Pair<String?, Boolean>>, 260 block: () -> Unit 261 ) { 262 val result = requestAppPermissions(*permissions, block = block) 263 assertEquals(Activity.RESULT_OK, result.resultCode) 264 assertEquals( 265 result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!.size, 266 result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.size 267 ) 268 269 assertEquals( 270 permissionAndExpectedGrantResults.toList(), 271 result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!! 272 .zip( 273 result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!! 274 .map { it == PackageManager.PERMISSION_GRANTED } 275 ) 276 ) 277 permissionAndExpectedGrantResults.forEach { 278 it.first?.let { permission -> 279 assertAppHasPermission(permission, it.second) 280 } 281 } 282 } 283 284 protected inline fun requestAppPermissionsAndAssertResult( 285 vararg permissionAndExpectedGrantResults: Pair<String?, Boolean>, 286 block: () -> Unit 287 ) = requestAppPermissionsAndAssertResult( 288 permissionAndExpectedGrantResults.map { it.first }.toTypedArray(), 289 permissionAndExpectedGrantResults, 290 block 291 ) 292 293 protected fun clickPermissionRequestAllowButton() { 294 if (isAutomotive) { 295 click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT))) 296 } else { 297 click(By.res(ALLOW_BUTTON)) 298 } 299 } 300 301 protected fun clickPermissionRequestSettingsLinkAndAllowAlways() { 302 clickPermissionRequestSettingsLink() 303 eventually({ 304 clickAllowAlwaysInSettings() 305 }, TIMEOUT_MILLIS * 2) 306 pressBack() 307 } 308 309 protected fun clickAllowAlwaysInSettings() { 310 if (isAutomotive || isTv || isWatch) { 311 click(By.text(getPermissionControllerString("app_permission_button_allow_always"))) 312 } else { 313 click(By.res("com.android.permissioncontroller:id/allow_always_radio_button")) 314 } 315 } 316 317 protected fun clickPermissionRequestAllowForegroundButton(timeoutMillis: Long = 10_000) { 318 if (isAutomotive) { 319 click(By.text( 320 getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)), timeoutMillis) 321 } else { 322 click(By.res(ALLOW_FOREGROUND_BUTTON), timeoutMillis) 323 } 324 } 325 326 protected fun clickPermissionRequestDenyButton() { 327 if (isAutomotive || isWatch || isTv) { 328 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT))) 329 } else { 330 click(By.res(DENY_BUTTON)) 331 } 332 } 333 334 protected fun clickPermissionRequestSettingsLinkAndDeny() { 335 clickPermissionRequestSettingsLink() 336 if (isAutomotive || isWatch) { 337 click(By.text(getPermissionControllerString("app_permission_button_deny"))) 338 } else { 339 click(By.res("com.android.permissioncontroller:id/deny_radio_button")) 340 } 341 waitForIdle() 342 pressBack() 343 } 344 345 protected fun clickPermissionRequestSettingsLink() { 346 waitForIdle() 347 eventually { 348 // UiObject2 doesn't expose CharSequence. 349 val node = if (isAutomotive) { 350 uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByText( 351 "Allow in settings." 352 )[0] 353 } else { 354 uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByViewId( 355 "com.android.permissioncontroller:id/detail_message" 356 )[0] 357 } 358 assertTrue(node.isVisibleToUser) 359 val text = node.text as Spanned 360 val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] 361 // We could pass in null here in Java, but we need an instance in Kotlin. 362 clickableSpan.onClick(View(context)) 363 } 364 waitForIdle() 365 } 366 367 protected fun clickPermissionRequestDenyAndDontAskAgainButton() { 368 if (isAutomotive) { 369 click(By.text(getPermissionControllerString(DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT))) 370 } else if (isWatch) { 371 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT))) 372 } else { 373 click(By.res(DENY_AND_DONT_ASK_AGAIN_BUTTON)) 374 } 375 } 376 377 // Only used in TV and Watch form factors 378 protected fun clickPermissionRequestDontAskAgainButton() { 379 if (isWatch) { 380 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT))) 381 } else { 382 click( 383 By.res("com.android.permissioncontroller:id/permission_deny_dont_ask_again_button") 384 ) 385 } 386 } 387 388 protected fun clickPermissionRequestNoUpgradeAndDontAskAgainButton() { 389 if (isAutomotive) { 390 click(By.text(getPermissionControllerString(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT))) 391 } else { 392 click(By.res(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON)) 393 } 394 } 395 396 protected fun grantAppPermissions(vararg permissions: String, targetSdk: Int = 30) { 397 setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false, 398 targetSdk = targetSdk) 399 } 400 401 protected fun revokeAppPermissions( 402 vararg permissions: String, 403 isLegacyApp: Boolean = false, 404 targetSdk: Int = 30 405 ) { 406 setAppPermissionState(*permissions, state = PermissionState.DENIED, 407 isLegacyApp = isLegacyApp, targetSdk = targetSdk) 408 } 409 410 private fun setAppPermissionState( 411 vararg permissions: String, 412 state: PermissionState, 413 isLegacyApp: Boolean, 414 targetSdk: Int 415 ) { 416 if (isTv) { 417 // Dismiss DeprecatedTargetSdkVersionDialog, if present 418 if (waitFindObjectOrNull(By.text(APP_PACKAGE_NAME), 1000L) != null) { 419 pressBack() 420 } 421 pressHome() 422 } else { 423 pressBack() 424 pressBack() 425 pressBack() 426 } 427 428 // Try multiple times as the AppInfo page might have read stale data 429 eventually({ 430 try { 431 // Open the app details settings 432 context.startActivity( 433 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 434 data = Uri.fromParts("package", APP_PACKAGE_NAME, null) 435 addCategory(Intent.CATEGORY_DEFAULT) 436 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 437 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 438 } 439 ) 440 // Open the permissions UI 441 click(byTextRes(R.string.permissions).enabled(true)) 442 } catch (e: Exception) { 443 pressBack() 444 throw e 445 } 446 }, TIMEOUT_MILLIS) 447 448 for (permission in permissions) { 449 // Find the permission screen 450 val permissionLabel = getPermissionLabel(permission) 451 if (isWatch) { 452 click(By.text(permissionLabel), 40_000) 453 } else { 454 clickPermissionControllerUi(By.text(permissionLabel)) 455 } 456 457 val wasGranted = if (isAutomotive) { 458 // Automotive doesn't support one time permissions, and thus 459 // won't show an "Ask every time" message 460 !waitFindObject(By.text( 461 getPermissionControllerString("app_permission_button_deny"))).isChecked 462 } else if (isTv || isWatch) { 463 !(waitFindObject( 464 By.text(getPermissionControllerString(DENY_BUTTON_TEXT))).isChecked || 465 (!isLegacyApp && hasAskButton(permission) && waitFindObject( 466 By.text(getPermissionControllerString(ASK_BUTTON_TEXT))).isChecked)) 467 } else { 468 !(waitFindObject(By.res(DENY_RADIO_BUTTON)).isChecked || 469 (!isLegacyApp && hasAskButton(permission) && 470 waitFindObject(By.res(ASK_RADIO_BUTTON)).isChecked)) 471 } 472 var alreadyChecked = false 473 val button = waitFindObject( 474 if (isAutomotive) { 475 // Automotive doesn't support one time permissions, and thus 476 // won't show an "Ask every time" message 477 when (state) { 478 PermissionState.ALLOWED -> 479 if (showsForegroundOnlyButton(permission)) { 480 By.text(getPermissionControllerString( 481 "app_permission_button_allow_foreground")) 482 } else { 483 By.text(getPermissionControllerString( 484 "app_permission_button_allow")) 485 } 486 PermissionState.DENIED -> By.text( 487 getPermissionControllerString("app_permission_button_deny")) 488 PermissionState.DENIED_WITH_PREJUDICE -> By.text( 489 getPermissionControllerString("app_permission_button_deny")) 490 } 491 } else if (isTv || isWatch) { 492 when (state) { 493 PermissionState.ALLOWED -> 494 if (showsForegroundOnlyButton(permission)) { 495 By.text(getPermissionControllerString( 496 ALLOW_FOREGROUND_PREFERENCE_TEXT)) 497 } else { 498 By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)) 499 } 500 PermissionState.DENIED -> 501 if (!isLegacyApp && hasAskButton(permission)) { 502 By.text(getPermissionControllerString(ASK_BUTTON_TEXT)) 503 } else { 504 By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) 505 } 506 PermissionState.DENIED_WITH_PREJUDICE -> By.text( 507 getPermissionControllerString(DENY_BUTTON_TEXT)) 508 } 509 } else { 510 when (state) { 511 PermissionState.ALLOWED -> 512 if (showsForegroundOnlyButton(permission)) { 513 By.res(ALLOW_FOREGROUND_RADIO_BUTTON) 514 } else if (isMediaStorageButton(permission, targetSdk)) { 515 // Uses "allow_foreground_only_radio_button" as id 516 byTextRes(R.string.allow_media_storage) 517 } else if (isAllStorageButton(permission, targetSdk)) { 518 // Uses "allow_always_radio_button" as id 519 byTextRes(R.string.allow_external_storage) 520 } else { 521 By.res(ALLOW_RADIO_BUTTON) 522 } 523 PermissionState.DENIED -> 524 if (!isLegacyApp && hasAskButton(permission)) { 525 By.res(ASK_RADIO_BUTTON) 526 } else { 527 By.res(DENY_RADIO_BUTTON) 528 } 529 PermissionState.DENIED_WITH_PREJUDICE -> By.res(DENY_RADIO_BUTTON) 530 } 531 } 532 ) 533 alreadyChecked = button.isChecked 534 if (!alreadyChecked) { 535 button.click() 536 } 537 if (!alreadyChecked && isLegacyApp && wasGranted) { 538 if (!isTv) { 539 scrollToBottom() 540 } 541 542 // Due to the limited real estate, Wear uses buttons with icons instead of text 543 // for dialogs 544 if (isWatch) { 545 click(By.res( 546 "com.android.permissioncontroller:id/wear_alertdialog_positive_button")) 547 } else { 548 val resources = context.createPackageContext( 549 packageManager.permissionControllerPackageName, 0 550 ).resources 551 val confirmTextRes = resources.getIdentifier( 552 "com.android.permissioncontroller:string/grant_dialog_button_deny_anyway", 553 null, null 554 ) 555 556 val confirmText = resources.getString(confirmTextRes) 557 click(byTextStartsWithCaseInsensitive(confirmText)) 558 } 559 } 560 pressBack() 561 } 562 pressBack() 563 pressBack() 564 } 565 566 private fun getPermissionLabel(permission: String): String { 567 val labelResName = permissionToLabelResNameMap[permission] 568 assertNotNull("Unknown permission $permission", labelResName) 569 val labelRes = platformResources.getIdentifier(labelResName, null, null) 570 return platformResources.getString(labelRes) 571 } 572 573 private fun hasAskButton(permission: String): Boolean = 574 when (permission) { 575 android.Manifest.permission.CAMERA, 576 android.Manifest.permission.RECORD_AUDIO, 577 android.Manifest.permission.ACCESS_FINE_LOCATION, 578 android.Manifest.permission.ACCESS_COARSE_LOCATION, 579 android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true 580 else -> false 581 } 582 583 private fun showsForegroundOnlyButton(permission: String): Boolean = 584 when (permission) { 585 android.Manifest.permission.CAMERA, 586 android.Manifest.permission.RECORD_AUDIO -> true 587 else -> false 588 } 589 590 private fun isMediaStorageButton(permission: String, targetSdk: Int): Boolean = 591 if (isTv || isWatch) { 592 false 593 } else { 594 when (permission) { 595 android.Manifest.permission.READ_EXTERNAL_STORAGE, 596 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 597 android.Manifest.permission.ACCESS_MEDIA_LOCATION -> 598 // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set 599 targetSdk >= Build.VERSION_CODES.P 600 else -> false 601 } 602 } 603 604 private fun isAllStorageButton(permission: String, targetSdk: Int): Boolean = 605 if (isTv || isWatch) { 606 false 607 } else { 608 when (permission) { 609 android.Manifest.permission.READ_EXTERNAL_STORAGE, 610 android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 611 android.Manifest.permission.ACCESS_MEDIA_LOCATION -> 612 // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set 613 targetSdk < Build.VERSION_CODES.P 614 android.Manifest.permission.MANAGE_EXTERNAL_STORAGE -> true 615 else -> false 616 } 617 } 618 619 private fun scrollToBottom() { 620 val scrollable = UiScrollable(UiSelector().scrollable(true)).apply { 621 swipeDeadZonePercentage = 0.25 622 } 623 waitForIdle() 624 if (scrollable.exists()) { 625 scrollable.flingToEnd(10) 626 } 627 } 628 629 private fun byTextRes(textRes: Int): BySelector = By.text(context.getString(textRes)) 630 631 private fun byTextStartsWithCaseInsensitive(prefix: String): BySelector = 632 By.text(Pattern.compile("(?i)^${Pattern.quote(prefix)}.*$")) 633 634 protected fun assertAppHasPermission(permissionName: String, expectPermission: Boolean) { 635 assertEquals( 636 if (expectPermission) { 637 PackageManager.PERMISSION_GRANTED 638 } else { 639 PackageManager.PERMISSION_DENIED 640 }, 641 packageManager.checkPermission(permissionName, APP_PACKAGE_NAME) 642 ) 643 } 644 645 protected fun assertAppHasCalendarAccess(expectAccess: Boolean) { 646 val future = startActivityForFuture( 647 Intent().apply { 648 component = ComponentName( 649 APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.CheckCalendarAccessActivity" 650 ) 651 } 652 ) 653 val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 654 assertEquals(Activity.RESULT_OK, result.resultCode) 655 assertTrue(result.resultData!!.hasExtra("$APP_PACKAGE_NAME.HAS_ACCESS")) 656 assertEquals( 657 expectAccess, 658 result.resultData!!.getBooleanExtra("$APP_PACKAGE_NAME.HAS_ACCESS", false) 659 ) 660 } 661 } 662