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.permissionui.cts 18 19 import android.Manifest 20 import android.app.Activity 21 import android.app.ActivityManager 22 import android.app.Instrumentation 23 import android.content.ComponentName 24 import android.content.Intent 25 import android.content.Intent.ACTION_REVIEW_APP_DATA_SHARING_UPDATES 26 import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK 27 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK 28 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE 29 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE 30 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_OTHER 31 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE 32 import android.content.pm.PackageInstaller.SessionParams 33 import android.content.pm.PackageManager 34 import android.net.Uri 35 import android.os.Build 36 import android.os.Process 37 import android.provider.DeviceConfig 38 import android.provider.Settings 39 import android.text.Spanned 40 import android.text.style.ClickableSpan 41 import android.view.View 42 import android.view.accessibility.AccessibilityNodeInfo 43 import androidx.test.uiautomator.By 44 import androidx.test.uiautomator.BySelector 45 import androidx.test.uiautomator.StaleObjectException 46 import androidx.test.uiautomator.UiObjectNotFoundException 47 import androidx.test.uiautomator.UiScrollable 48 import androidx.test.uiautomator.UiSelector 49 import androidx.test.uiautomator.Until 50 import com.android.compatibility.common.util.SystemUtil 51 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity 52 import com.android.compatibility.common.util.SystemUtil.eventually 53 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity 54 import com.android.modules.utils.build.SdkLevel 55 import java.util.concurrent.CompletableFuture 56 import java.util.concurrent.TimeUnit 57 import java.util.regex.Pattern 58 import org.junit.After 59 import org.junit.Assert 60 import org.junit.Assert.assertEquals 61 import org.junit.Assert.assertNotNull 62 import org.junit.Assert.assertTrue 63 import org.junit.Before 64 65 abstract class BaseUsePermissionTest : BasePermissionTest() { 66 companion object { 67 const val APP_APK_NAME_31 = "CtsUsePermissionApp31.apk" 68 const val APP_APK_NAME_31_WITH_ASL = "CtsUsePermissionApp31WithAsl.apk" 69 const val APP_APK_NAME_LATEST = "CtsUsePermissionAppLatest.apk" 70 71 const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk" 72 const val APP_APK_PATH_22_CALENDAR_ONLY = 73 "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk" 74 const val APP_APK_PATH_22_NONE = "$APK_DIRECTORY/CtsUsePermissionApp22None.apk" 75 const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk" 76 const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk" 77 const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk" 78 const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk" 79 const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk" 80 const val APP_APK_PATH_30 = "$APK_DIRECTORY/CtsUsePermissionApp30.apk" 81 const val APP_APK_PATH_31 = "$APK_DIRECTORY/$APP_APK_NAME_31" 82 const val APP_APK_PATH_32 = "$APK_DIRECTORY/CtsUsePermissionApp32.apk" 83 84 const val APP_APK_PATH_30_WITH_BACKGROUND = 85 "$APK_DIRECTORY/CtsUsePermissionApp30WithBackground.apk" 86 const val APP_APK_PATH_30_WITH_BLUETOOTH = 87 "$APK_DIRECTORY/CtsUsePermissionApp30WithBluetooth.apk" 88 const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk" 89 const val APP_APK_PATH_LATEST_NONE = "$APK_DIRECTORY/CtsUsePermissionAppLatestNone.apk" 90 const val APP_APK_PATH_WITH_OVERLAY = "$APK_DIRECTORY/CtsUsePermissionAppWithOverlay.apk" 91 const val APP_APK_PATH_CREATE_NOTIFICATION_CHANNELS_31 = 92 "$APK_DIRECTORY/CtsCreateNotificationChannelsApp31.apk" 93 const val APP_APK_PATH_MEDIA_PERMISSION_33_WITH_STORAGE = 94 "$APK_DIRECTORY/CtsMediaPermissionApp33WithStorage.apk" 95 const val APP_APK_PATH_IMPLICIT_USER_SELECT_STORAGE = 96 "$APK_DIRECTORY/CtsUsePermissionAppImplicitUserSelectStorage.apk" 97 const val APP_APK_PATH_STORAGE_33 = "$APK_DIRECTORY/CtsUsePermissionAppStorage33.apk" 98 const val APP_APK_PATH_OTHER_APP = "$APK_DIRECTORY/CtsDifferentPkgNameApp.apk" 99 const val APP_APK_PATH_TWO_PERM_REQUESTS = 100 "$APK_DIRECTORY/CtsAppThatMakesTwoPermRequests.apk" 101 const val APP_PACKAGE_NAME = "android.permissionui.cts.usepermission" 102 const val OTHER_APP_PACKAGE_NAME = "android.permissionui.cts.usepermissionother" 103 const val TEST_INSTALLER_PACKAGE_NAME = "android.permissionui.cts" 104 105 const val ALLOW_ALL_BUTTON = 106 "com.android.permissioncontroller:id/permission_allow_all_button" 107 const val SELECT_BUTTON = 108 "com.android.permissioncontroller:id/permission_allow_selected_button" 109 const val DONT_SELECT_MORE_BUTTON = 110 "com.android.permissioncontroller:id/permission_dont_allow_more_selected_button" 111 const val ALLOW_BUTTON = "com.android.permissioncontroller:id/permission_allow_button" 112 const val ALLOW_FOREGROUND_BUTTON = 113 "com.android.permissioncontroller:id/permission_allow_foreground_only_button" 114 const val DENY_BUTTON = "com.android.permissioncontroller:id/permission_deny_button" 115 const val DENY_AND_DONT_ASK_AGAIN_BUTTON = 116 "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button" 117 const val NO_UPGRADE_BUTTON = 118 "com.android.permissioncontroller:id/permission_no_upgrade_button" 119 const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON = 120 "com.android.permissioncontroller:" + 121 "id/permission_no_upgrade_and_dont_ask_again_button" 122 123 const val ALLOW_ALWAYS_RADIO_BUTTON = 124 "com.android.permissioncontroller:id/allow_always_radio_button" 125 const val ALLOW_RADIO_BUTTON = "com.android.permissioncontroller:id/allow_radio_button" 126 const val ALLOW_FOREGROUND_RADIO_BUTTON = 127 "com.android.permissioncontroller:id/allow_foreground_only_radio_button" 128 const val ASK_RADIO_BUTTON = "com.android.permissioncontroller:id/ask_radio_button" 129 const val DENY_RADIO_BUTTON = "com.android.permissioncontroller:id/deny_radio_button" 130 const val SELECT_RADIO_BUTTON = "com.android.permissioncontroller:id/select_radio_button" 131 const val EDIT_PHOTOS_BUTTON = "com.android.permissioncontroller:id/edit_selected_button" 132 133 const val NOTIF_TEXT = "permgrouprequest_notifications" 134 const val ALLOW_BUTTON_TEXT = "grant_dialog_button_allow" 135 const val ALLOW_ALL_FILES_BUTTON_TEXT = "app_permission_button_allow_all_files" 136 const val ALLOW_FOREGROUND_BUTTON_TEXT = "grant_dialog_button_allow_foreground" 137 const val ALLOW_FOREGROUND_PREFERENCE_TEXT = "permission_access_only_foreground" 138 const val ASK_BUTTON_TEXT = "app_permission_button_ask" 139 const val ALLOW_ONE_TIME_BUTTON_TEXT = "grant_dialog_button_allow_one_time" 140 const val DENY_BUTTON_TEXT = "grant_dialog_button_deny" 141 const val DENY_ANYWAY_BUTTON_TEXT = "grant_dialog_button_deny_anyway" 142 const val DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT = 143 "grant_dialog_button_deny_and_dont_ask_again" 144 const val NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT = "grant_dialog_button_no_upgrade" 145 const val ECM_ALERT_DIALOG_OK_BUTTON_TEXT = "enhanced_confirmation_dialog_ok" 146 const val ALERT_DIALOG_MESSAGE = "android:id/message" 147 const val ALERT_DIALOG_OK_BUTTON = "android:id/button1" 148 const val APP_PERMISSION_RATIONALE_TITLE_TEXT = "app_location_permission_rationale_title" 149 const val APP_PERMISSION_RATIONALE_SUBTITLE_TEXT = 150 "app_location_permission_rationale_subtitle" 151 const val GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW = 152 "com.android.permissioncontroller:id/permission_rationale_container" 153 const val PERMISSION_RATIONALE_ACTIVITY_TITLE_VIEW = 154 "com.android.permissioncontroller:id/permission_rationale_title" 155 const val DATA_SHARING_SOURCE_TITLE_ID = 156 "com.android.permissioncontroller:id/data_sharing_source_title" 157 const val DATA_SHARING_SOURCE_MESSAGE_ID = 158 "com.android.permissioncontroller:id/data_sharing_source_message" 159 const val PURPOSE_TITLE_ID = "com.android.permissioncontroller:id/purpose_title" 160 const val PURPOSE_MESSAGE_ID = "com.android.permissioncontroller:id/purpose_message" 161 const val LEARN_MORE_TITLE_ID = "com.android.permissioncontroller:id/learn_more_title" 162 const val HELP_URL_ECM = 163 "com.android.permissioncontroller:id/help_url_action_disabled_by_restricted_settings" 164 const val LEARN_MORE_MESSAGE_ID = "com.android.permissioncontroller:id/learn_more_message" 165 const val DETAIL_MESSAGE_ID = "com.android.permissioncontroller:id/detail_message" 166 const val PERMISSION_RATIONALE_SETTINGS_SECTION = 167 "com.android.permissioncontroller:id/settings_section" 168 const val SETTINGS_TITLE_ID = "com.android.permissioncontroller:id/settings_title" 169 const val SETTINGS_MESSAGE_ID = "com.android.permissioncontroller:id/settings_message" 170 171 const val REQUEST_LOCATION_MESSAGE = "permgrouprequest_location" 172 173 const val DATA_SHARING_UPDATES = "Data sharing updates for location" 174 const val DATA_SHARING_UPDATES_SUBTITLE = 175 "These apps have changed the way they may share your location data. They may not" + 176 " have shared it before, or may now share it for advertising or marketing" + 177 " purposes." 178 const val DATA_SHARING_NO_UPDATES_MESSAGE = "No updates at this time" 179 const val UPDATES_IN_LAST_30_DAYS = "Updated within 30 days" 180 const val DATA_SHARING_UPDATES_FOOTER_MESSAGE = 181 "The developers of these apps provided info about their data sharing practices" + 182 " to an app store. They may update it over time.\n\nData sharing" + 183 " practices may vary based on your app version, use, region, and age." 184 const val LEARN_ABOUT_DATA_SHARING = "Learn about data sharing" 185 const val LOCATION_PERMISSION = "Location permission" 186 const val APP_PACKAGE_NAME_SUBSTRING = "android.permissionui" 187 const val NOW_SHARED_WITH_THIRD_PARTIES = 188 "Your location data is now shared with third " + "parties" 189 const val NOW_SHARED_WITH_THIRD_PARTIES_FOR_ADS = 190 "Your location data is now shared with " + "third parties for advertising or marketing" 191 const val PROPERTY_DATA_SHARING_UPDATE_PERIOD_MILLIS = "data_sharing_update_period_millis" 192 const val PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP = 193 "max_safety_labels_persisted_per_app" 194 195 // The highest SDK for which the system will show a "low SDK" warning when launching the app 196 const val MAX_SDK_FOR_SDK_WARNING = 27 197 const val MIN_SDK_FOR_RUNTIME_PERMS = 23 198 199 val TEST_INSTALLER_ACTIVITY_COMPONENT_NAME = 200 ComponentName(context, TestInstallerActivity::class.java) 201 202 val MEDIA_PERMISSIONS: Set<String> = 203 mutableSetOf( 204 Manifest.permission.ACCESS_MEDIA_LOCATION, 205 Manifest.permission.READ_MEDIA_AUDIO, 206 Manifest.permission.READ_MEDIA_IMAGES, 207 Manifest.permission.READ_MEDIA_VIDEO, 208 ) 209 .apply { 210 if (SdkLevel.isAtLeastU()) { 211 add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) 212 } 213 } 214 .toSet() 215 216 val STORAGE_AND_MEDIA_PERMISSIONS = 217 MEDIA_PERMISSIONS.plus(Manifest.permission.READ_EXTERNAL_STORAGE) 218 .plus(Manifest.permission.WRITE_EXTERNAL_STORAGE) 219 220 @JvmStatic protected val PICKER_ENABLED_SETTING = "photo_picker_prompt_enabled" 221 222 @JvmStatic 223 protected fun isPhotoPickerPermissionPromptEnabled(): Boolean { 224 return SdkLevel.isAtLeastU() && 225 !isTv && 226 !isAutomotive && 227 !isWatch && 228 callWithShellPermissionIdentity { 229 DeviceConfig.getBoolean( 230 DeviceConfig.NAMESPACE_PRIVACY, 231 PICKER_ENABLED_SETTING, 232 true 233 ) 234 } 235 } 236 } 237 238 enum class PermissionState { 239 ALLOWED, 240 DENIED, 241 DENIED_WITH_PREJUDICE 242 } 243 244 private val platformResources = context.createPackageContext("android", 0).resources 245 private val permissionToLabelResNameMap = 246 mapOf( 247 // Contacts 248 android.Manifest.permission.READ_CONTACTS to "@android:string/permgrouplab_contacts", 249 android.Manifest.permission.WRITE_CONTACTS to "@android:string/permgrouplab_contacts", 250 // Calendar 251 android.Manifest.permission.READ_CALENDAR to "@android:string/permgrouplab_calendar", 252 android.Manifest.permission.WRITE_CALENDAR to "@android:string/permgrouplab_calendar", 253 // SMS 254 android.Manifest.permission_group.SMS to "@android:string/permgrouplab_sms", 255 android.Manifest.permission.SEND_SMS to "@android:string/permgrouplab_sms", 256 android.Manifest.permission.RECEIVE_SMS to "@android:string/permgrouplab_sms", 257 android.Manifest.permission.READ_SMS to "@android:string/permgrouplab_sms", 258 android.Manifest.permission.RECEIVE_WAP_PUSH to "@android:string/permgrouplab_sms", 259 android.Manifest.permission.RECEIVE_MMS to "@android:string/permgrouplab_sms", 260 "android.permission.READ_CELL_BROADCASTS" to "@android:string/permgrouplab_sms", 261 // Storage 262 android.Manifest.permission.READ_EXTERNAL_STORAGE to 263 "@android:string/permgrouplab_storage", 264 android.Manifest.permission.WRITE_EXTERNAL_STORAGE to 265 "@android:string/permgrouplab_storage", 266 // Location 267 android.Manifest.permission.ACCESS_FINE_LOCATION to 268 "@android:string/permgrouplab_location", 269 android.Manifest.permission.ACCESS_COARSE_LOCATION to 270 "@android:string/permgrouplab_location", 271 android.Manifest.permission.ACCESS_BACKGROUND_LOCATION to 272 "@android:string/permgrouplab_location", 273 // Phone 274 android.Manifest.permission_group.PHONE to "@android:string/permgrouplab_phone", 275 android.Manifest.permission.READ_PHONE_STATE to "@android:string/permgrouplab_phone", 276 android.Manifest.permission.CALL_PHONE to "@android:string/permgrouplab_phone", 277 "android.permission.ACCESS_IMS_CALL_SERVICE" to "@android:string/permgrouplab_phone", 278 android.Manifest.permission.READ_CALL_LOG to "@android:string/permgrouplab_phone", 279 android.Manifest.permission.WRITE_CALL_LOG to "@android:string/permgrouplab_phone", 280 android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permgrouplab_phone", 281 android.Manifest.permission.USE_SIP to "@android:string/permgrouplab_phone", 282 android.Manifest.permission.PROCESS_OUTGOING_CALLS to 283 "@android:string/permgrouplab_phone", 284 // Microphone 285 android.Manifest.permission.RECORD_AUDIO to "@android:string/permgrouplab_microphone", 286 // Camera 287 android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera", 288 // Body sensors 289 android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors", 290 android.Manifest.permission.BODY_SENSORS_BACKGROUND to 291 "@android:string/permgrouplab_sensors", 292 // Bluetooth 293 android.Manifest.permission.BLUETOOTH_CONNECT to 294 "@android:string/permgrouplab_nearby_devices", 295 android.Manifest.permission.BLUETOOTH_SCAN to 296 "@android:string/permgrouplab_nearby_devices", 297 // Aural 298 android.Manifest.permission.READ_MEDIA_AUDIO to 299 "@android:string/permgrouplab_readMediaAural", 300 // Visual 301 android.Manifest.permission.READ_MEDIA_IMAGES to 302 "@android:string/permgrouplab_readMediaVisual", 303 android.Manifest.permission.READ_MEDIA_VIDEO to 304 "@android:string/permgrouplab_readMediaVisual" 305 ) 306 307 @Before 308 @After 309 fun uninstallApp() { 310 uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false) 311 } 312 313 override fun installPackage( 314 apkPath: String, 315 reinstall: Boolean, 316 grantRuntimePermissions: Boolean, 317 expectSuccess: Boolean, 318 installSource: String? 319 ) { 320 installPackage( 321 apkPath, 322 reinstall, 323 grantRuntimePermissions, 324 expectSuccess, 325 installSource, 326 false 327 ) 328 } 329 330 fun installPackage( 331 apkPath: String, 332 reinstall: Boolean = false, 333 grantRuntimePermissions: Boolean = false, 334 expectSuccess: Boolean = true, 335 installSource: String? = null, 336 skipClearLowSdkDialog: Boolean = false 337 ) { 338 super.installPackage( 339 apkPath, 340 reinstall, 341 grantRuntimePermissions, 342 expectSuccess, 343 installSource 344 ) 345 346 val targetSdk = getTargetSdk() 347 // If the targetSDK is high enough, the low sdk warning won't show. If the SDK is 348 // below runtime permissions, the dialog will be delayed by the permission review screen. 349 // If success is not expected, don't bother trying 350 if ( 351 targetSdk > MAX_SDK_FOR_SDK_WARNING || 352 targetSdk < MIN_SDK_FOR_RUNTIME_PERMS || 353 !expectSuccess || 354 skipClearLowSdkDialog 355 ) { 356 return 357 } 358 359 val finishOnCreateIntent = 360 Intent().apply { 361 component = 362 ComponentName(APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity") 363 flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK 364 } 365 366 // Check if an activity resolves for the test app. If it doesn't, then our test app doesn't 367 // have the usual set of activities, and likely won't be opened, and thus, won't show the 368 // dialog 369 callWithShellPermissionIdentity { 370 context.packageManager.resolveActivity(finishOnCreateIntent, PackageManager.MATCH_ALL) 371 } ?: return 372 373 // Start the test app, and expect the targetSDK warning dialog 374 context.startActivity(finishOnCreateIntent) 375 clearTargetSdkWarning() 376 // Kill the test app, so that the next time we launch, we don't see the app warning dialog 377 killTestApp() 378 } 379 380 protected fun clearTargetSdkWarning(timeoutMillis: Long = TIMEOUT_MILLIS) { 381 if (SdkLevel.isAtLeastV()) { 382 // In V and above, the target SDK dialog can be disabled via system property 383 return 384 } 385 386 val targetSdkWarningVisible = 387 uiDevice.wait( 388 Until.hasObject( 389 By.textStartsWith("This app was built for an older version of Android") 390 .displayId(displayId) 391 ), 392 timeoutMillis 393 ) 394 if (targetSdkWarningVisible) { 395 try { 396 uiDevice.findObject(By.res("android:id/button1").displayId(displayId)).click() 397 } catch (e: StaleObjectException) { 398 // Click sometimes fails with StaleObjectException (b/280430717). 399 e.printStackTrace() 400 } 401 } 402 } 403 404 protected fun killTestApp() { 405 pressBack() 406 pressBack() 407 runWithShellPermissionIdentity { 408 val am = context.getSystemService(ActivityManager::class.java)!! 409 am.forceStopPackage(APP_PACKAGE_NAME) 410 } 411 waitForIdle() 412 } 413 414 protected fun clickPermissionReviewContinue() { 415 if (isAutomotive || isWatch) { 416 clickAndWaitForWindowTransition( 417 By.text(getPermissionControllerString("review_button_continue")) 418 .displayId(displayId), 419 TIMEOUT_MILLIS * 2 420 ) 421 } else { 422 clickAndWaitForWindowTransition( 423 By.res("com.android.permissioncontroller:id/continue_button").displayId(displayId) 424 ) 425 } 426 } 427 428 protected fun clickPermissionReviewContinueAndClearSdkWarning() { 429 clickPermissionReviewContinue() 430 clearTargetSdkWarning() 431 } 432 433 protected fun installPackageWithInstallSourceAndEmptyMetadata(apkName: String) { 434 installPackageViaSession(apkName, AppMetadata.createEmptyAppMetadata()) 435 } 436 437 protected fun installPackageWithInstallSourceAndMetadata(apkName: String) { 438 installPackageViaSession(apkName, AppMetadata.createDefaultAppMetadata()) 439 } 440 441 protected fun installPackageWithInstallSourceAndMetadataFromStore(apkName: String) { 442 installPackageViaSession( 443 apkName, 444 AppMetadata.createDefaultAppMetadata(), 445 PACKAGE_SOURCE_STORE 446 ) 447 } 448 449 protected fun installPackageWithInstallSourceAndMetadataFromLocalFile(apkName: String) { 450 installPackageViaSession( 451 apkName, 452 AppMetadata.createDefaultAppMetadata(), 453 PACKAGE_SOURCE_LOCAL_FILE 454 ) 455 } 456 457 protected fun installPackageWithInstallSourceAndMetadataFromDownloadedFile(apkName: String) { 458 installPackageViaSession( 459 apkName, 460 AppMetadata.createDefaultAppMetadata(), 461 PACKAGE_SOURCE_DOWNLOADED_FILE 462 ) 463 } 464 465 protected fun installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms( 466 apkName: String 467 ) { 468 installPackageViaSession( 469 apkName, 470 AppMetadata.createDefaultAppMetadata(), 471 PACKAGE_SOURCE_DOWNLOADED_FILE, 472 allowlistedRestrictedPermissions = SessionParams.RESTRICTED_PERMISSIONS_ALL 473 ) 474 } 475 476 protected fun installPackageWithInstallSourceAndMetadataFromOther(apkName: String) { 477 installPackageViaSession( 478 apkName, 479 AppMetadata.createDefaultAppMetadata(), 480 PACKAGE_SOURCE_OTHER 481 ) 482 } 483 484 protected fun installPackageWithInstallSourceAndNoMetadata(apkName: String) { 485 installPackageViaSession(apkName) 486 } 487 488 protected fun installPackageWithInstallSourceAndNoMetadataFromStore(apkName: String) { 489 installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_STORE) 490 } 491 492 protected fun installPackageWithInstallSourceAndNoMetadataFromLocalFile(apkName: String) { 493 installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_LOCAL_FILE) 494 } 495 496 protected fun installPackageWithInstallSourceAndNoMetadataFromDownloadedFile(apkName: String) { 497 installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_DOWNLOADED_FILE) 498 } 499 500 protected fun installPackageWithInstallSourceAndNoMetadataFromOther(apkName: String) { 501 installPackageViaSession(apkName, packageSource = PACKAGE_SOURCE_OTHER) 502 } 503 504 protected fun installPackageWithInstallSourceAndInvalidMetadata(apkName: String) { 505 installPackageViaSession(apkName, AppMetadata.createInvalidAppMetadata()) 506 } 507 508 protected fun installPackageWithInstallSourceAndMetadataWithoutTopLevelVersion( 509 apkName: String 510 ) { 511 installPackageViaSession( 512 apkName, 513 AppMetadata.createInvalidAppMetadataWithoutTopLevelVersion() 514 ) 515 } 516 517 protected fun installPackageWithInstallSourceAndMetadataWithInvalidTopLevelVersion( 518 apkName: String 519 ) { 520 installPackageViaSession( 521 apkName, 522 AppMetadata.createInvalidAppMetadataWithInvalidTopLevelVersion() 523 ) 524 } 525 526 protected fun installPackageWithInstallSourceAndMetadataWithoutSafetyLabelVersion( 527 apkName: String 528 ) { 529 installPackageViaSession( 530 apkName, 531 AppMetadata.createInvalidAppMetadataWithoutSafetyLabelVersion() 532 ) 533 } 534 535 protected fun installPackageWithInstallSourceAndMetadataWithInvalidSafetyLabelVersion( 536 apkName: String 537 ) { 538 installPackageViaSession( 539 apkName, 540 AppMetadata.createInvalidAppMetadataWithInvalidSafetyLabelVersion() 541 ) 542 } 543 544 protected fun installPackageWithoutInstallSource(apkName: String) { 545 // TODO(b/257293222): Update/remove when hooking up PackageManager APIs 546 installPackage(apkName) 547 } 548 549 protected fun assertPermissionRationaleActivityTitleIsVisible(expected: Boolean) { 550 findView(By.res(PERMISSION_RATIONALE_ACTIVITY_TITLE_VIEW).displayId(displayId), 551 expected = expected) 552 } 553 554 protected fun assertPermissionRationaleActivityDataSharingSourceSectionVisible( 555 expected: Boolean 556 ) { 557 findView(By.res(DATA_SHARING_SOURCE_TITLE_ID).displayId(displayId), expected = expected) 558 findView(By.res(DATA_SHARING_SOURCE_MESSAGE_ID).displayId(displayId), expected = expected) 559 } 560 561 protected fun assertPermissionRationaleActivityPurposeSectionVisible(expected: Boolean) { 562 findView(By.res(PURPOSE_TITLE_ID).displayId(displayId), expected = expected) 563 findView(By.res(PURPOSE_MESSAGE_ID).displayId(displayId), expected = expected) 564 } 565 566 protected fun assertPermissionRationaleActivityLearnMoreSectionVisible(expected: Boolean) { 567 findView(By.res(LEARN_MORE_TITLE_ID).displayId(displayId), expected = expected) 568 findView(By.res(LEARN_MORE_MESSAGE_ID).displayId(displayId), expected = expected) 569 } 570 571 protected fun assertPermissionRationaleActivitySettingsSectionVisible(expected: Boolean) { 572 findView(By.res(PERMISSION_RATIONALE_SETTINGS_SECTION).displayId(displayId), 573 expected = expected) 574 findView(By.res(SETTINGS_TITLE_ID).displayId(displayId), expected = expected) 575 findView(By.res(SETTINGS_MESSAGE_ID).displayId(displayId), expected = expected) 576 } 577 578 protected fun assertPermissionRationaleDialogIsVisible( 579 expected: Boolean, 580 showSettingsSection: Boolean = true 581 ) { 582 assertPermissionRationaleActivityTitleIsVisible(expected) 583 assertPermissionRationaleActivityDataSharingSourceSectionVisible(expected) 584 assertPermissionRationaleActivityPurposeSectionVisible(expected) 585 assertPermissionRationaleActivityLearnMoreSectionVisible(expected) 586 if (expected) { 587 assertPermissionRationaleActivitySettingsSectionVisible(showSettingsSection) 588 } 589 } 590 591 protected fun assertPermissionRationaleContainerOnGrantDialogIsVisible(expected: Boolean) { 592 findView(By.res(GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW).displayId(displayId), 593 expected = expected) 594 } 595 596 protected fun clickPermissionReviewCancel() { 597 if (isAutomotive || isWatch) { 598 clickAndWaitForWindowTransition( 599 By.text(getPermissionControllerString("review_button_cancel")).displayId(displayId) 600 ) 601 } else { 602 clickAndWaitForWindowTransition( 603 By.res("com.android.permissioncontroller:id/cancel_button").displayId(displayId) 604 ) 605 } 606 } 607 608 protected fun approvePermissionReview() { 609 startAppActivityAndAssertResultCode(Activity.RESULT_OK) { 610 clickPermissionReviewContinueAndClearSdkWarning() 611 } 612 } 613 614 protected fun cancelPermissionReview() { 615 startAppActivityAndAssertResultCode(Activity.RESULT_CANCELED) { 616 clickPermissionReviewCancel() 617 } 618 } 619 620 protected fun assertAppDoesNotNeedPermissionReview() { 621 startAppActivityAndAssertResultCode(Activity.RESULT_OK) {} 622 } 623 624 protected inline fun startAppActivityAndAssertResultCode( 625 expectedResultCode: Int, 626 block: () -> Unit 627 ) { 628 val future = 629 startActivityForFuture( 630 Intent().apply { 631 component = 632 ComponentName(APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity") 633 } 634 ) 635 block() 636 assertEquals( 637 expectedResultCode, 638 future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).resultCode 639 ) 640 } 641 642 protected inline fun requestAppPermissionsForNoResult( 643 vararg permissions: String?, 644 crossinline block: () -> Unit 645 ) { 646 // Request the permissions 647 doAndWaitForWindowTransition { 648 context.startActivity( 649 Intent().apply { 650 component = 651 ComponentName( 652 APP_PACKAGE_NAME, 653 "$APP_PACKAGE_NAME.RequestPermissionsActivity" 654 ) 655 putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions) 656 addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) 657 } 658 ) 659 } 660 // Perform the post-request action 661 block() 662 } 663 664 protected inline fun requestAppPermissions( 665 vararg permissions: String?, 666 askTwice: Boolean = false, 667 waitForWindowTransition: Boolean = !isWatch, 668 crossinline block: () -> Unit 669 ): Instrumentation.ActivityResult { 670 // Request the permissions 671 lateinit var future: CompletableFuture<Instrumentation.ActivityResult> 672 doAndWaitForWindowTransition { 673 future = 674 startActivityForFuture( 675 Intent().apply { 676 component = 677 ComponentName( 678 APP_PACKAGE_NAME, 679 "$APP_PACKAGE_NAME.RequestPermissionsActivity" 680 ) 681 putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions) 682 putExtra("$APP_PACKAGE_NAME.ASK_TWICE", askTwice) 683 } 684 ) 685 } 686 687 // Notification permission prompt is shown first, so get it out of the way 688 clickNotificationPermissionRequestAllowButtonIfAvailable() 689 // Perform the post-request action 690 if (waitForWindowTransition) { 691 doAndWaitForWindowTransition { block() } 692 } else { 693 block() 694 } 695 return future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 696 } 697 698 protected inline fun requestAppPermissionsAndAssertResult( 699 permissions: Array<out String?>, 700 permissionAndExpectedGrantResults: Array<out Pair<String?, Boolean>>, 701 askTwice: Boolean = false, 702 waitForWindowTransition: Boolean = !isWatch, 703 crossinline block: () -> Unit 704 ) { 705 var shouldWaitForWindowTransition = waitForWindowTransition 706 // Do not wait for windowTransition after action is performed on auto, when permissions 707 // are being denied. The click deny function explicitly waits for window to transition 708 if (isAutomotive) { 709 var somePermissionsTrue = false 710 // http://go/nl-kt-best-practices#for-loop-vs-foreach 711 for (it in permissionAndExpectedGrantResults) { 712 somePermissionsTrue = somePermissionsTrue || it.second 713 } 714 // When all permissions being requested are to be denied 715 // do not wait for windowTransition 716 if (!somePermissionsTrue) { 717 shouldWaitForWindowTransition = false 718 } 719 } 720 val result = 721 requestAppPermissions( 722 *permissions, 723 askTwice = askTwice, 724 waitForWindowTransition = shouldWaitForWindowTransition, 725 block = block 726 ) 727 assertEquals( 728 "Permission request result had unexpected resultCode:", 729 Activity.RESULT_OK, 730 result.resultCode 731 ) 732 733 val responseSize: Int = 734 result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!!.size 735 assertEquals( 736 "Permission request result had unexpected number of grant results:", 737 responseSize, 738 result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.size 739 ) 740 741 // Note that the behavior around requesting `null` permissions changed in the platform 742 // in Android U. Currently, null permissions are ignored and left out of the result set. 743 assertTrue( 744 "Permission request result had fewer permissions than request", 745 permissions.size >= responseSize 746 ) 747 assertEquals( 748 "Permission request result had unexpected grant results:", 749 permissionAndExpectedGrantResults.filter { it.first != null }.toList(), 750 result.resultData!! 751 .getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!! 752 .filterNotNull() 753 .zip( 754 result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!!.map { 755 it == PackageManager.PERMISSION_GRANTED 756 } 757 ) 758 ) 759 760 permissionAndExpectedGrantResults.forEach { 761 it.first?.let { permission -> assertAppHasPermission(permission, it.second) } 762 } 763 } 764 765 protected inline fun requestAppPermissionsAndAssertResult( 766 vararg permissionAndExpectedGrantResults: Pair<String?, Boolean>, 767 askTwice: Boolean = false, 768 waitForWindowTransition: Boolean = !isWatch, 769 crossinline block: () -> Unit 770 ) { 771 requestAppPermissionsAndAssertResult( 772 permissionAndExpectedGrantResults.map { it.first }.toTypedArray(), 773 permissionAndExpectedGrantResults, 774 askTwice, 775 waitForWindowTransition, 776 block 777 ) 778 } 779 780 // Perform the requested action, then wait both for the action to complete, and for at least 781 // one window transition to occur since the moment the action begins executing. 782 protected inline fun doAndWaitForWindowTransition(crossinline block: () -> Unit) { 783 val timeoutOccurred = 784 !uiDevice.performActionAndWait( 785 { block() }, 786 Until.newWindow(), 787 NEW_WINDOW_TIMEOUT_MILLIS 788 ) 789 790 if (timeoutOccurred) { 791 throw RuntimeException("Timed out waiting for window transition.") 792 } 793 } 794 795 protected fun findPermissionRequestAllowButton(timeoutMillis: Long = 20000) { 796 if (isAutomotive || isWatch) { 797 waitFindObject(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)) 798 .displayId(displayId), timeoutMillis) 799 } else { 800 waitFindObject(By.res(ALLOW_BUTTON).displayId(displayId), timeoutMillis) 801 } 802 } 803 804 protected fun clickPermissionRequestAllowButton(timeoutMillis: Long = 20000) { 805 if (isAutomotive || isWatch) { 806 click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)).displayId(displayId), 807 timeoutMillis) 808 } else { 809 click(By.res(ALLOW_BUTTON).displayId(displayId), timeoutMillis) 810 } 811 } 812 813 protected fun clickPermissionRequestAllowAllButton(timeoutMillis: Long = 20000) { 814 click(By.res(ALLOW_ALL_BUTTON).displayId(displayId), timeoutMillis) 815 } 816 817 /** 818 * Only for use in tests that are not testing the notification permission popup, on T devices 819 */ 820 protected fun clickNotificationPermissionRequestAllowButtonIfAvailable() { 821 if (SdkLevel.isAtLeastT() && getTargetSdk() < Build.VERSION_CODES.TIRAMISU) { 822 val notificationPermissionRequestVisible = 823 uiDevice.wait( 824 Until.hasObject( 825 By.text(getPermissionControllerString(NOTIF_TEXT, APP_PACKAGE_NAME)) 826 .displayId(displayId) 827 ), 828 1000 829 ) 830 if (notificationPermissionRequestVisible) { 831 if (isAutomotive) { 832 click(By.text(getPermissionControllerString(ALLOW_BUTTON_TEXT)) 833 .displayId(displayId)) 834 } else { 835 click(By.res(ALLOW_BUTTON).displayId(displayId)) 836 } 837 } 838 } 839 } 840 841 protected fun clickPermissionRequestSettingsLinkAndAllowAlways() { 842 clickPermissionRequestSettingsLink() 843 eventually({ clickAllowAlwaysInSettings() }, TIMEOUT_MILLIS * 2) 844 pressBack() 845 } 846 847 protected fun clickAllowAlwaysInSettings() { 848 if (isAutomotive || isTv || isWatch) { 849 click(By.text(getPermissionControllerString("app_permission_button_allow_always")) 850 .displayId(displayId)) 851 } else { 852 click(By.res("com.android.permissioncontroller:id/allow_always_radio_button") 853 .displayId(displayId)) 854 } 855 } 856 857 protected fun clickAllowForegroundInSettings() { 858 click(By.res(ALLOW_FOREGROUND_RADIO_BUTTON).displayId(displayId)) 859 } 860 861 protected fun clicksDenyInSettings() { 862 if (isAutomotive || isWatch) { 863 click(By.text(getPermissionControllerString("app_permission_button_deny")) 864 .displayId(displayId)) 865 } else { 866 click(By.res("com.android.permissioncontroller:id/deny_radio_button") 867 .displayId(displayId)) 868 } 869 } 870 871 protected fun findPermissionRequestAllowForegroundButton(timeoutMillis: Long = 20000) { 872 if (isAutomotive || isWatch) { 873 waitFindObject( 874 By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) 875 .displayId(displayId), 876 timeoutMillis 877 ) 878 } else { 879 waitFindObject(By.res(ALLOW_FOREGROUND_BUTTON).displayId(displayId), timeoutMillis) 880 } 881 } 882 883 protected fun clickPermissionRequestAllowForegroundButton(timeoutMillis: Long = 20_000) { 884 if (isAutomotive || isWatch) { 885 click( 886 By.text(getPermissionControllerString(ALLOW_FOREGROUND_BUTTON_TEXT)) 887 .displayId(displayId), 888 timeoutMillis 889 ) 890 } else { 891 click(By.res(ALLOW_FOREGROUND_BUTTON).displayId(displayId), timeoutMillis) 892 } 893 } 894 895 protected fun clickPermissionRequestDenyButton() { 896 if (isAutomotive) { 897 scrollToBottom() 898 clickAndWaitForWindowTransition( 899 By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId) 900 ) 901 } else if (isWatch || isTv) { 902 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId)) 903 } else { 904 click(By.res(DENY_BUTTON).displayId(displayId)) 905 } 906 } 907 908 protected fun clickPermissionRequestSettingsLinkAndDeny() { 909 clickPermissionRequestSettingsLink() 910 eventually({ clicksDenyInSettings() }, TIMEOUT_MILLIS * 2) 911 pressBack() 912 } 913 914 protected fun clickPermissionRequestSettingsLink() { 915 eventually { 916 if (isWatch) { 917 clickPermissionRequestSettingsLinkForWear() 918 return@eventually 919 } 920 // UiObject2 doesn't expose CharSequence. 921 val node = 922 if (isAutomotive) { 923 // Should match "Allow in settings." (location) and "go to settings." (body 924 // sensors) 925 uiAutomation.rootInActiveWindow 926 .findAccessibilityNodeInfosByText(" settings.")[0] 927 } else { 928 uiAutomation.rootInActiveWindow 929 .findAccessibilityNodeInfosByViewId(DETAIL_MESSAGE_ID)[0] 930 } 931 if (!node.isVisibleToUser) { 932 scrollToBottom() 933 } 934 assertTrue(node.isVisibleToUser) 935 936 val text = node.text as Spanned 937 val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] 938 // We could pass in null here in Java, but we need an instance in Kotlin. 939 doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) } 940 } 941 } 942 943 private fun clickPermissionRequestSettingsLinkForWear() { 944 // Find detail message. 945 val text = waitFindObject(By.textContains(" settings.").displayId(displayId)) 946 947 // Move the view to the top of the screen. 948 var visibleBounds = text.getVisibleBounds() 949 val centerX = (visibleBounds.left + visibleBounds.right) / 2 950 uiDevice.drag(centerX, visibleBounds.top, centerX, 0, 10) 951 952 // Click the deep link. 953 // Not sure where the clickable text is. So try different point in the last line 954 // of the 5 line text. 955 val bounds = text.getVisibleBounds() 956 val xdelta = 0.2 * bounds.width() 957 val y = bounds.bottom - (0.05 * bounds.height()) 958 var clickedOnLink: Boolean = false 959 for (i in 1..4) { 960 val x = bounds.left + (i * xdelta) 961 uiDevice.click(x.toInt(), y.toInt()) 962 waitForIdleLong() 963 val nextScreenNode: AccessibilityNodeInfo? = 964 findAccessibilityNodeInfosByTextForSurfaceView( 965 uiAutomation.rootInActiveWindow, 966 "All the time" 967 ) 968 if (nextScreenNode != null) { 969 clickedOnLink = true 970 break 971 } 972 } 973 assertTrue("Could not click on the settings link correctly", clickedOnLink) 974 } 975 976 protected fun clickPermissionRequestDenyAndDontAskAgainButton() { 977 if (isAutomotive) { 978 scrollToBottom() 979 clickAndWaitForWindowTransition( 980 By.text(getPermissionControllerString(DENY_AND_DONT_ASK_AGAIN_BUTTON_TEXT)) 981 .displayId(displayId) 982 ) 983 } else if (isWatch) { 984 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId)) 985 } else { 986 click(By.res(DENY_AND_DONT_ASK_AGAIN_BUTTON).displayId(displayId)) 987 } 988 } 989 990 // Only used in TV and Watch form factors 991 protected fun clickPermissionRequestDontAskAgainButton() { 992 if (isWatch) { 993 click(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)).displayId(displayId)) 994 } else { 995 click( 996 By.res("com.android.permissioncontroller:id/permission_deny_dont_ask_again_button") 997 .displayId(displayId) 998 ) 999 } 1000 } 1001 1002 protected fun clickPermissionRequestNoUpgradeAndDontAskAgainButton() { 1003 if (isAutomotive || isWatch) { 1004 click(By.text(getPermissionControllerString(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON_TEXT)) 1005 .displayId(displayId)) 1006 } else { 1007 click(By.res(NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON).displayId(displayId)) 1008 } 1009 } 1010 1011 protected fun clickPermissionRationaleContentInAppPermission() { 1012 clickAndWaitForWindowTransition( 1013 By.text(getPermissionControllerString(APP_PERMISSION_RATIONALE_SUBTITLE_TEXT)) 1014 .displayId(displayId) 1015 ) 1016 } 1017 1018 protected fun clickPermissionRationaleViewInGrantDialog() { 1019 clickAndWaitForWindowTransition( 1020 By.res(GRANT_DIALOG_PERMISSION_RATIONALE_CONTAINER_VIEW).displayId(displayId)) 1021 } 1022 1023 protected fun grantAppPermissionsByUi(vararg permissions: String) { 1024 setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false) 1025 } 1026 1027 protected fun grantRuntimePermissions(vararg permissions: String) { 1028 for (permission in permissions) { 1029 uiAutomation.grantRuntimePermission(APP_PACKAGE_NAME, permission) 1030 } 1031 } 1032 1033 protected fun revokeAppPermissionsByUi( 1034 vararg permissions: String, 1035 isLegacyApp: Boolean = false 1036 ) { 1037 setAppPermissionState( 1038 *permissions, 1039 state = PermissionState.DENIED, 1040 isLegacyApp = isLegacyApp 1041 ) 1042 } 1043 1044 private fun navigateToAppPermissionSettings() { 1045 if (isTv) { 1046 clearTargetSdkWarning(1000L) 1047 pressHome() 1048 } else { 1049 pressBack() 1050 pressBack() 1051 pressBack() 1052 } 1053 1054 // Try multiple times as the AppInfo page might have read stale data 1055 eventually( 1056 { 1057 try { 1058 // Open the app details settings 1059 doAndWaitForWindowTransition { 1060 context.startActivity( 1061 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 1062 data = Uri.fromParts("package", APP_PACKAGE_NAME, null) 1063 addCategory(Intent.CATEGORY_DEFAULT) 1064 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1065 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 1066 } 1067 ) 1068 } 1069 if (isTv) { 1070 pressDPadDown() 1071 pressDPadDown() 1072 pressDPadDown() 1073 pressDPadDown() 1074 } 1075 // Open the permissions UI 1076 clickAndWaitForWindowTransition(byTextRes(R.string.permissions).enabled(true)) 1077 } catch (e: Exception) { 1078 pressBack() 1079 throw e 1080 } 1081 }, 1082 TIMEOUT_MILLIS 1083 ) 1084 } 1085 1086 private fun getTargetSdk(packageName: String = APP_PACKAGE_NAME): Int { 1087 return callWithShellPermissionIdentity { 1088 try { 1089 context.packageManager.getApplicationInfo(packageName, 0).targetSdkVersion 1090 } catch (e: PackageManager.NameNotFoundException) { 1091 -1 1092 } 1093 } 1094 } 1095 1096 protected fun navigateToIndividualPermissionSetting( 1097 permission: String, 1098 manuallyNavigate: Boolean = false 1099 ) { 1100 val useLegacyNavigation = isWatch || isAutomotive || manuallyNavigate 1101 if (useLegacyNavigation) { 1102 navigateToAppPermissionSettings() 1103 val permissionLabel = getPermissionLabel(permission) 1104 if (isWatch) { 1105 clickAndWaitForWindowTransition(By.text(permissionLabel).displayId(displayId), 1106 40_000) 1107 } else { 1108 clickPermissionControllerUi(By.text(permissionLabel).displayId(displayId)) 1109 } 1110 return 1111 } 1112 doAndWaitForWindowTransition { 1113 runWithShellPermissionIdentity { 1114 context.startActivity( 1115 Intent(Intent.ACTION_MANAGE_APP_PERMISSION).apply { 1116 putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME) 1117 putExtra(Intent.EXTRA_PERMISSION_NAME, permission) 1118 putExtra(Intent.EXTRA_USER, Process.myUserHandle()) 1119 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1120 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 1121 } 1122 ) 1123 } 1124 } 1125 } 1126 1127 @Suppress("DEPRECATION") 1128 protected fun startManageAppPermissionsActivity() { 1129 doAndWaitForWindowTransition { 1130 runWithShellPermissionIdentity { 1131 context.startActivity( 1132 Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply { 1133 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1134 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 1135 putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME) 1136 } 1137 ) 1138 } 1139 } 1140 } 1141 1142 /** Starts activity with intent [ACTION_REVIEW_APP_DATA_SHARING_UPDATES]. */ 1143 fun startAppDataSharingUpdatesActivity() { 1144 doAndWaitForWindowTransition { 1145 runWithShellPermissionIdentity { 1146 context.startActivity( 1147 Intent(ACTION_REVIEW_APP_DATA_SHARING_UPDATES).apply { 1148 addFlags(FLAG_ACTIVITY_NEW_TASK) 1149 } 1150 ) 1151 } 1152 } 1153 } 1154 1155 private fun setAppPermissionState( 1156 vararg permissions: String, 1157 state: PermissionState, 1158 isLegacyApp: Boolean, 1159 manuallyNavigate: Boolean = false, 1160 ) { 1161 val useLegacyNavigation = isWatch || isAutomotive || manuallyNavigate 1162 if (useLegacyNavigation) { 1163 navigateToAppPermissionSettings() 1164 } 1165 1166 val navigatedGroupLabels = mutableSetOf<String>() 1167 for (permission in permissions) { 1168 // Find the permission screen 1169 val permissionLabel = getPermissionLabel(permission) 1170 if (navigatedGroupLabels.contains(getPermissionLabel(permission))) { 1171 continue 1172 } 1173 navigatedGroupLabels.add(permissionLabel) 1174 if (useLegacyNavigation) { 1175 if (isWatch) { 1176 click(By.text(permissionLabel).displayId(displayId), 40_000) 1177 } else if (isAutomotive) { 1178 clickPermissionControllerUi(permissionLabel) 1179 } else { 1180 clickPermissionControllerUi(By.text(permissionLabel).displayId(displayId)) 1181 } 1182 } else { 1183 doAndWaitForWindowTransition { 1184 runWithShellPermissionIdentity { 1185 context.startActivity( 1186 Intent(Intent.ACTION_MANAGE_APP_PERMISSION).apply { 1187 putExtra(Intent.EXTRA_PACKAGE_NAME, APP_PACKAGE_NAME) 1188 putExtra(Intent.EXTRA_PERMISSION_NAME, permission) 1189 putExtra(Intent.EXTRA_USER, Process.myUserHandle()) 1190 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1191 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 1192 } 1193 ) 1194 } 1195 } 1196 } 1197 1198 val wasGranted = 1199 if (isAutomotive) { 1200 // Automotive doesn't support one time permissions, and thus 1201 // won't show an "Ask every time" message 1202 !waitFindObject( 1203 By.text(getPermissionControllerString("app_permission_button_deny")) 1204 .displayId(displayId) 1205 ) 1206 .isChecked 1207 } else if (isTv || isWatch) { 1208 !(waitFindObject(By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) 1209 .displayId(displayId)) 1210 .isChecked || 1211 (!isLegacyApp && 1212 hasAskButton(permission) && 1213 waitFindObject(By.text(getPermissionControllerString(ASK_BUTTON_TEXT)) 1214 .displayId(displayId)) 1215 .isChecked)) 1216 } else { 1217 !(waitFindObject(By.res(DENY_RADIO_BUTTON).displayId(displayId)).isChecked || 1218 (!isLegacyApp && 1219 hasAskButton(permission) && 1220 waitFindObject(By.res(ASK_RADIO_BUTTON).displayId(displayId)) 1221 .isChecked)) 1222 } 1223 var alreadyChecked = false 1224 val button = 1225 waitFindObject( 1226 if (isAutomotive) { 1227 // Automotive doesn't support one time permissions, and thus 1228 // won't show an "Ask every time" message 1229 when (state) { 1230 PermissionState.ALLOWED -> 1231 if (showsForegroundOnlyButton(permission)) { 1232 By.text( 1233 getPermissionControllerString( 1234 "app_permission_button_allow_foreground" 1235 ) 1236 ).displayId(displayId) 1237 } else { 1238 By.text( 1239 getPermissionControllerString("app_permission_button_allow") 1240 ).displayId(displayId) 1241 } 1242 PermissionState.DENIED -> 1243 By.text(getPermissionControllerString("app_permission_button_deny")) 1244 .displayId(displayId) 1245 PermissionState.DENIED_WITH_PREJUDICE -> 1246 By.text(getPermissionControllerString("app_permission_button_deny")) 1247 .displayId(displayId) 1248 } 1249 } else if (isTv || isWatch) { 1250 when (state) { 1251 PermissionState.ALLOWED -> 1252 if (showsForegroundOnlyButton(permission)) { 1253 By.text( 1254 getPermissionControllerString( 1255 ALLOW_FOREGROUND_PREFERENCE_TEXT 1256 ) 1257 ).displayId(displayId) 1258 } else { 1259 byAnyText( 1260 getPermissionControllerResString(ALLOW_BUTTON_TEXT), 1261 getPermissionControllerResString( 1262 ALLOW_ALL_FILES_BUTTON_TEXT 1263 ) 1264 ) 1265 } 1266 PermissionState.DENIED -> 1267 if (!isLegacyApp && hasAskButton(permission)) { 1268 By.text(getPermissionControllerString(ASK_BUTTON_TEXT)) 1269 .displayId(displayId) 1270 } else { 1271 By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) 1272 .displayId(displayId) 1273 } 1274 PermissionState.DENIED_WITH_PREJUDICE -> 1275 By.text(getPermissionControllerString(DENY_BUTTON_TEXT)) 1276 .displayId(displayId) 1277 } 1278 } else { 1279 when (state) { 1280 PermissionState.ALLOWED -> 1281 if (showsForegroundOnlyButton(permission)) { 1282 By.res(ALLOW_FOREGROUND_RADIO_BUTTON).displayId(displayId) 1283 } else if (showsAlwaysButton(permission)) { 1284 By.res(ALLOW_ALWAYS_RADIO_BUTTON).displayId(displayId) 1285 } else { 1286 By.res(ALLOW_RADIO_BUTTON).displayId(displayId) 1287 } 1288 PermissionState.DENIED -> 1289 if (!isLegacyApp && hasAskButton(permission)) { 1290 By.res(ASK_RADIO_BUTTON).displayId(displayId) 1291 } else { 1292 By.res(DENY_RADIO_BUTTON).displayId(displayId) 1293 } 1294 PermissionState.DENIED_WITH_PREJUDICE -> By.res(DENY_RADIO_BUTTON) 1295 .displayId(displayId) 1296 } 1297 } 1298 ) 1299 alreadyChecked = button.isChecked 1300 if (!alreadyChecked) { 1301 button.click() 1302 } 1303 1304 val shouldShowStorageWarning = 1305 SdkLevel.isAtLeastT() && 1306 getTargetSdk() <= Build.VERSION_CODES.S_V2 && 1307 permission in MEDIA_PERMISSIONS 1308 if (shouldShowStorageWarning) { 1309 if (isWatch) { 1310 click( 1311 By.desc( 1312 getPermissionControllerString("media_confirm_dialog_positive_button") 1313 ).displayId(displayId) 1314 ) 1315 } else { 1316 click(By.res(ALERT_DIALOG_OK_BUTTON).displayId(displayId)) 1317 } 1318 } else if (!alreadyChecked && isLegacyApp && wasGranted) { 1319 if (!isTv) { 1320 // Wait for alert dialog to popup, then scroll to the bottom of it 1321 if (isWatch) { 1322 waitFindObject( 1323 By.text(getPermissionControllerString("old_sdk_deny_warning")) 1324 .displayId(displayId) 1325 ) 1326 } else { 1327 waitFindObject(By.res(ALERT_DIALOG_MESSAGE).displayId(displayId)) 1328 } 1329 scrollToBottom() 1330 } 1331 1332 // Due to the limited real estate, Wear uses buttons with icons instead of text 1333 // for dialogs 1334 if (isWatch) { 1335 click(By.desc(getPermissionControllerString("ok")).displayId(displayId)) 1336 } else { 1337 val resources = 1338 context 1339 .createPackageContext(packageManager.permissionControllerPackageName, 0) 1340 .resources 1341 val confirmTextRes = 1342 resources.getIdentifier( 1343 "com.android.permissioncontroller:string/grant_dialog_button_deny_anyway", 1344 null, 1345 null 1346 ) 1347 1348 val confirmText = resources.getString(confirmTextRes) 1349 click(byTextStartsWithCaseInsensitive(confirmText)) 1350 } 1351 } 1352 pressBack() 1353 } 1354 pressBack() 1355 pressBack() 1356 } 1357 1358 private fun getPermissionLabel(permission: String): String { 1359 val labelResName = permissionToLabelResNameMap[permission] 1360 assertNotNull("Unknown permission $permission", labelResName) 1361 val labelRes = platformResources.getIdentifier(labelResName, null, null) 1362 return platformResources.getString(labelRes) 1363 } 1364 1365 private fun hasAskButton(permission: String): Boolean = 1366 when (permission) { 1367 android.Manifest.permission.CAMERA, 1368 android.Manifest.permission.RECORD_AUDIO, 1369 android.Manifest.permission.ACCESS_FINE_LOCATION, 1370 android.Manifest.permission.ACCESS_COARSE_LOCATION, 1371 android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true 1372 else -> false 1373 } 1374 1375 private fun showsAllowPhotosButton(permission: String): Boolean { 1376 if (!isPhotoPickerPermissionPromptEnabled()) { 1377 return false 1378 } 1379 return when (permission) { 1380 Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, 1381 Manifest.permission.READ_MEDIA_IMAGES, 1382 Manifest.permission.READ_MEDIA_VIDEO -> true 1383 else -> false 1384 } 1385 } 1386 1387 private fun showsForegroundOnlyButton(permission: String): Boolean = 1388 when (permission) { 1389 android.Manifest.permission.CAMERA, 1390 android.Manifest.permission.RECORD_AUDIO -> true 1391 else -> false 1392 } 1393 1394 private fun showsAlwaysButton(permission: String): Boolean = 1395 when (permission) { 1396 android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true 1397 else -> false 1398 } 1399 1400 private fun scrollToBottom() { 1401 val scrollable = 1402 UiScrollable(UiSelector().scrollable(true)).apply { 1403 if (isWatch) { 1404 swipeDeadZonePercentage = 0.1 1405 } else { 1406 swipeDeadZonePercentage = 0.25 1407 } 1408 } 1409 waitForIdle() 1410 if (scrollable.exists()) { 1411 try { 1412 scrollable.flingToEnd(10) 1413 } catch (e: UiObjectNotFoundException) { 1414 // flingToEnd() sometimes still fails despite waitForIdle() and the exists() check 1415 // (b/246984354). 1416 e.printStackTrace() 1417 } 1418 } 1419 } 1420 1421 protected fun findAccessibilityNodeInfosByTextForSurfaceView( 1422 node: AccessibilityNodeInfo, 1423 text: String 1424 ): AccessibilityNodeInfo? { 1425 if (node.text != null && node.text.contains(text)) return node 1426 for (i in 0 until node.childCount) { 1427 val child = node.getChild(i) 1428 if (child != null) { 1429 return findAccessibilityNodeInfosByTextForSurfaceView(child, text) ?: continue 1430 } 1431 } 1432 return null 1433 } 1434 1435 private fun byTextRes(textRes: Int): BySelector = 1436 By.text(context.getString(textRes)).displayId(displayId) 1437 1438 private fun byTextStartsWithCaseInsensitive(prefix: String): BySelector = 1439 By.text(Pattern.compile("(?i)^${Pattern.quote(prefix)}.*$")).displayId(displayId) 1440 1441 protected fun assertAppHasPermission(permissionName: String, expectPermission: Boolean) { 1442 val checkPermissionResult = packageManager.checkPermission(permissionName, APP_PACKAGE_NAME) 1443 assertTrue( 1444 "Invalid permission check result: $checkPermissionResult", 1445 checkPermissionResult == PackageManager.PERMISSION_GRANTED || 1446 checkPermissionResult == PackageManager.PERMISSION_DENIED 1447 ) 1448 if (!expectPermission && checkPermissionResult == PackageManager.PERMISSION_GRANTED) { 1449 Assert.fail( 1450 "Unexpected permission check result for $permissionName: " + 1451 "expected -1 (PERMISSION_DENIED) but was 0 (PERMISSION_GRANTED)" 1452 ) 1453 } 1454 if (expectPermission && checkPermissionResult == PackageManager.PERMISSION_DENIED) { 1455 Assert.fail( 1456 "Unexpected permission check result for $permissionName: " + 1457 "expected 0 (PERMISSION_GRANTED) but was -1 (PERMISSION_DENIED)" 1458 ) 1459 } 1460 } 1461 1462 protected fun assertAppHasCalendarAccess(expectAccess: Boolean) { 1463 val future = 1464 startActivityForFuture( 1465 Intent().apply { 1466 component = 1467 ComponentName( 1468 APP_PACKAGE_NAME, 1469 "$APP_PACKAGE_NAME.CheckCalendarAccessActivity" 1470 ) 1471 } 1472 ) 1473 clickNotificationPermissionRequestAllowButtonIfAvailable() 1474 val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 1475 assertEquals(Activity.RESULT_OK, result.resultCode) 1476 assertTrue(result.resultData!!.hasExtra("$APP_PACKAGE_NAME.HAS_ACCESS")) 1477 assertEquals( 1478 expectAccess, 1479 result.resultData!!.getBooleanExtra("$APP_PACKAGE_NAME.HAS_ACCESS", false) 1480 ) 1481 } 1482 1483 protected fun assertPermissionFlags(permName: String, vararg flags: Pair<Int, Boolean>) { 1484 val user = Process.myUserHandle() 1485 SystemUtil.runWithShellPermissionIdentity { 1486 val currFlags = packageManager.getPermissionFlags(permName, APP_PACKAGE_NAME, user) 1487 for ((flag, set) in flags) { 1488 assertEquals("flag $flag: ", set, currFlags and flag != 0) 1489 } 1490 } 1491 } 1492 1493 protected fun clickECMAlertDialogOKButton(waitForWindowTransition: Boolean = !isWatch) { 1494 var action = { click(By.res(ALERT_DIALOG_OK_BUTTON).displayId(displayId), TIMEOUT_MILLIS) } 1495 if (isWatch) { 1496 val okButtonText = 1497 getPermissionControllerResString(ECM_ALERT_DIALOG_OK_BUTTON_TEXT) ?: "OK" 1498 action = { click(By.text(okButtonText).displayId(displayId), TIMEOUT_MILLIS) } 1499 } 1500 1501 if (waitForWindowTransition) { 1502 doAndWaitForWindowTransition { action() } 1503 } else { 1504 action() 1505 } 1506 } 1507 } 1508