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