| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.permission3.cts |
| |
| import android.app.Activity |
| import android.app.Instrumentation |
| import android.content.ComponentName |
| import android.content.Intent |
| import android.content.pm.PackageManager |
| import android.net.Uri |
| import android.os.Build |
| import android.provider.Settings |
| import android.support.test.uiautomator.By |
| import android.support.test.uiautomator.BySelector |
| import android.support.test.uiautomator.UiScrollable |
| import android.support.test.uiautomator.UiSelector |
| import android.text.Spanned |
| import android.text.style.ClickableSpan |
| import android.view.View |
| import org.junit.After |
| import org.junit.Assert.assertEquals |
| import org.junit.Assert.assertNotNull |
| import org.junit.Assert.assertTrue |
| import org.junit.Before |
| import java.util.concurrent.TimeUnit |
| import java.util.regex.Pattern |
| |
| abstract class BaseUsePermissionTest : BasePermissionTest() { |
| companion object { |
| const val APP_APK_PATH_22 = "$APK_DIRECTORY/CtsUsePermissionApp22.apk" |
| const val APP_APK_PATH_22_CALENDAR_ONLY = |
| "$APK_DIRECTORY/CtsUsePermissionApp22CalendarOnly.apk" |
| const val APP_APK_PATH_23 = "$APK_DIRECTORY/CtsUsePermissionApp23.apk" |
| const val APP_APK_PATH_25 = "$APK_DIRECTORY/CtsUsePermissionApp25.apk" |
| const val APP_APK_PATH_26 = "$APK_DIRECTORY/CtsUsePermissionApp26.apk" |
| const val APP_APK_PATH_28 = "$APK_DIRECTORY/CtsUsePermissionApp28.apk" |
| const val APP_APK_PATH_29 = "$APK_DIRECTORY/CtsUsePermissionApp29.apk" |
| const val APP_APK_PATH_LATEST = "$APK_DIRECTORY/CtsUsePermissionAppLatest.apk" |
| const val APP_PACKAGE_NAME = "android.permission3.cts.usepermission" |
| } |
| |
| enum class PermissionState { |
| ALLOWED, |
| DENIED, |
| DENIED_WITH_PREJUDICE |
| } |
| |
| protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) |
| protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) |
| |
| private val platformResources = context.createPackageContext("android", 0).resources |
| private val permissionToLabelResNameMap = |
| if (!packageManager.arePermissionsIndividuallyControlled()) { |
| mapOf( |
| // Contacts |
| android.Manifest.permission.READ_CONTACTS |
| to "@android:string/permgrouplab_contacts", |
| android.Manifest.permission.WRITE_CONTACTS |
| to "@android:string/permgrouplab_contacts", |
| // Calendar |
| android.Manifest.permission.READ_CALENDAR |
| to "@android:string/permgrouplab_calendar", |
| android.Manifest.permission.WRITE_CALENDAR |
| to "@android:string/permgrouplab_calendar", |
| // SMS |
| android.Manifest.permission.SEND_SMS to "@android:string/permgrouplab_sms", |
| android.Manifest.permission.RECEIVE_SMS to "@android:string/permgrouplab_sms", |
| android.Manifest.permission.READ_SMS to "@android:string/permgrouplab_sms", |
| android.Manifest.permission.RECEIVE_WAP_PUSH to "@android:string/permgrouplab_sms", |
| android.Manifest.permission.RECEIVE_MMS to "@android:string/permgrouplab_sms", |
| "android.permission.READ_CELL_BROADCASTS" to "@android:string/permgrouplab_sms", |
| // Storage |
| android.Manifest.permission.READ_EXTERNAL_STORAGE |
| to "@android:string/permgrouplab_storage", |
| android.Manifest.permission.WRITE_EXTERNAL_STORAGE |
| to "@android:string/permgrouplab_storage", |
| // Location |
| android.Manifest.permission.ACCESS_FINE_LOCATION |
| to "@android:string/permgrouplab_location", |
| android.Manifest.permission.ACCESS_COARSE_LOCATION |
| to "@android:string/permgrouplab_location", |
| // Phone |
| android.Manifest.permission.READ_PHONE_STATE |
| to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.CALL_PHONE to "@android:string/permgrouplab_phone", |
| "android.permission.ACCESS_IMS_CALL_SERVICE" |
| to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.READ_CALL_LOG to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.WRITE_CALL_LOG to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.USE_SIP to "@android:string/permgrouplab_phone", |
| android.Manifest.permission.PROCESS_OUTGOING_CALLS |
| to "@android:string/permgrouplab_phone", |
| // Microphone |
| android.Manifest.permission.RECORD_AUDIO |
| to "@android:string/permgrouplab_microphone", |
| // Camera |
| android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera", |
| // Body sensors |
| android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors" |
| ) |
| } else { |
| mapOf( |
| // Contacts |
| android.Manifest.permission.READ_CONTACTS to "@android:string/permlab_readContacts", |
| android.Manifest.permission.WRITE_CONTACTS |
| to "@android:string/permlab_writeContacts", |
| // Calendar |
| android.Manifest.permission.READ_CALENDAR |
| to "@android:string/permgrouplab_calendar", |
| android.Manifest.permission.WRITE_CALENDAR |
| to "@android:string/permgrouplab_calendar", |
| // SMS |
| android.Manifest.permission.SEND_SMS to "@android:string/permlab_sendSms", |
| android.Manifest.permission.RECEIVE_SMS to "@android:string/permlab_receiveSms", |
| android.Manifest.permission.READ_SMS to "@android:string/permlab_readSms", |
| android.Manifest.permission.RECEIVE_WAP_PUSH |
| to "@android:string/permlab_receiveWapPush", |
| android.Manifest.permission.RECEIVE_MMS to "@android:string/permlab_receiveMms", |
| "android.permission.READ_CELL_BROADCASTS" |
| to "@android:string/permlab_readCellBroadcasts", |
| // Storage |
| android.Manifest.permission.READ_EXTERNAL_STORAGE |
| to "@android:string/permgrouplab_storage", |
| android.Manifest.permission.WRITE_EXTERNAL_STORAGE |
| to "@android:string/permgrouplab_storage", |
| // Location |
| android.Manifest.permission.ACCESS_FINE_LOCATION |
| to "@android:string/permgrouplab_location", |
| android.Manifest.permission.ACCESS_COARSE_LOCATION |
| to "@android:string/permgrouplab_location", |
| // Phone |
| android.Manifest.permission.READ_PHONE_STATE |
| to "@android:string/permlab_readPhoneState", |
| android.Manifest.permission.CALL_PHONE to "@android:string/permlab_callPhone", |
| "android.permission.ACCESS_IMS_CALL_SERVICE" |
| to "@android:string/permlab_accessImsCallService", |
| android.Manifest.permission.READ_CALL_LOG to "@android:string/permlab_readCallLog", |
| android.Manifest.permission.WRITE_CALL_LOG |
| to "@android:string/permlab_writeCallLog", |
| android.Manifest.permission.ADD_VOICEMAIL to "@android:string/permlab_addVoicemail", |
| android.Manifest.permission.USE_SIP to "@android:string/permlab_use_sip", |
| android.Manifest.permission.PROCESS_OUTGOING_CALLS |
| to "@android:string/permlab_processOutgoingCalls", |
| // Microphone |
| android.Manifest.permission.RECORD_AUDIO |
| to "@android:string/permgrouplab_microphone", |
| // Camera |
| android.Manifest.permission.CAMERA to "@android:string/permgrouplab_camera", |
| // Body sensors |
| android.Manifest.permission.BODY_SENSORS to "@android:string/permgrouplab_sensors" |
| ) |
| } |
| |
| @Before |
| @After |
| fun uninstallApp() { |
| uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false) |
| } |
| |
| protected fun clickPermissionReviewContinue() = |
| click(By.res("com.android.permissioncontroller:id/continue_button")) |
| |
| protected fun clickPermissionReviewCancel() = |
| click(By.res("com.android.permissioncontroller:id/cancel_button")) |
| |
| protected fun approvePermissionReview() { |
| startAppActivityAndAssertResultCode(Activity.RESULT_OK) { |
| clickPermissionReviewContinue() |
| } |
| } |
| |
| protected fun cancelPermissionReview() { |
| startAppActivityAndAssertResultCode(Activity.RESULT_CANCELED) { |
| clickPermissionReviewCancel() |
| } |
| } |
| |
| protected fun assertAppDoesNotNeedPermissionReview() { |
| startAppActivityAndAssertResultCode(Activity.RESULT_OK) {} |
| } |
| |
| protected inline fun startAppActivityAndAssertResultCode( |
| expectedResultCode: Int, |
| block: () -> Unit |
| ) { |
| val future = startActivityForFuture( |
| Intent().apply { |
| component = ComponentName( |
| APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.FinishOnCreateActivity" |
| ) |
| } |
| ) |
| block() |
| assertEquals( |
| expectedResultCode, future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).resultCode |
| ) |
| } |
| |
| protected inline fun requestAppPermissions( |
| vararg permissions: String?, |
| block: () -> Unit |
| ): Instrumentation.ActivityResult { |
| // Request the permissions |
| val future = startActivityForFuture( |
| Intent().apply { |
| component = ComponentName( |
| APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.RequestPermissionsActivity" |
| ) |
| putExtra("$APP_PACKAGE_NAME.PERMISSIONS", permissions) |
| } |
| ) |
| waitForIdle() |
| // Perform the post-request action |
| block() |
| return future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) |
| } |
| |
| protected inline fun requestAppPermissionsAndAssertResult( |
| permissions: Array<out String?>, |
| permissionAndExpectedGrantResults: Array<out Pair<String?, Boolean>>, |
| block: () -> Unit |
| ) { |
| val result = requestAppPermissions(*permissions, block = block) |
| assertEquals(Activity.RESULT_OK, result.resultCode) |
| assertEquals( |
| permissionAndExpectedGrantResults.toList(), |
| result.resultData!!.getStringArrayExtra("$APP_PACKAGE_NAME.PERMISSIONS")!! |
| .zip( |
| result.resultData!!.getIntArrayExtra("$APP_PACKAGE_NAME.GRANT_RESULTS")!! |
| .map { it == PackageManager.PERMISSION_GRANTED } |
| ) |
| ) |
| permissionAndExpectedGrantResults.forEach { |
| it.first?.let { permission -> |
| assertAppHasPermission(permission, it.second) |
| } |
| } |
| } |
| |
| protected inline fun requestAppPermissionsAndAssertResult( |
| vararg permissionAndExpectedGrantResults: Pair<String?, Boolean>, |
| block: () -> Unit |
| ) = requestAppPermissionsAndAssertResult( |
| permissionAndExpectedGrantResults.map { it.first }.toTypedArray(), |
| permissionAndExpectedGrantResults, |
| block |
| ) |
| |
| protected fun clickPermissionRequestAllowButton() = |
| click(By.res("com.android.permissioncontroller:id/permission_allow_button")) |
| |
| protected fun clickPermissionRequestSettingsLinkAndAllowAlways() { |
| clickPermissionRequestSettingsLink() |
| click(By.res("com.android.permissioncontroller:id/allow_always_radio_button")) |
| pressBack() |
| } |
| |
| protected fun clickPermissionRequestAllowForegroundButton() = |
| click(By.res("com.android.permissioncontroller:id/permission_allow_foreground_only_button")) |
| |
| protected fun clickPermissionRequestDenyButton() = |
| click(By.res("com.android.permissioncontroller:id/permission_deny_button")) |
| |
| protected fun clickPermissionRequestSettingsLinkAndDeny() { |
| clickPermissionRequestSettingsLink() |
| click(By.res("com.android.permissioncontroller:id/deny_radio_button")) |
| pressBack() |
| } |
| |
| protected fun clickPermissionRequestSettingsLink() { |
| waitForIdle() |
| // UiObject2 doesn't expose CharSequence. |
| val node = uiAutomation.rootInActiveWindow.findAccessibilityNodeInfosByViewId( |
| "com.android.permissioncontroller:id/detail_message" |
| )[0] |
| assertTrue(node.isVisibleToUser) |
| val text = node.text as Spanned |
| val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] |
| // We could pass in null here in Java, but we need an instance in Kotlin. |
| clickableSpan.onClick(View(context)) |
| waitForIdle() |
| } |
| |
| protected fun clickPermissionRequestDenyAndDontAskAgainButton() = |
| click( |
| By.res("com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button") |
| ) |
| |
| protected fun clickPermissionRequestDontAskAgainButton() = |
| click(By.res("com.android.permissioncontroller:id/permission_deny_dont_ask_again_button")) |
| |
| protected fun clickPermissionRequestNoUpgradeButton() = |
| click(By.res("com.android.permissioncontroller:id/permission_no_upgrade_button")) |
| |
| protected fun clickPermissionRequestNoUpgradeAndDontAskAgainButton() = click( |
| By.res( |
| "com.android.permissioncontroller:id/permission_no_upgrade_and_dont_ask_again_button" |
| ) |
| ) |
| |
| protected fun grantAppPermissions(vararg permissions: String, targetSdk: Int = 30) { |
| setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false, |
| targetSdk = targetSdk) |
| } |
| |
| protected fun revokeAppPermissions( |
| vararg permissions: String, |
| isLegacyApp: Boolean = false, |
| targetSdk: Int = 30 |
| ) { |
| setAppPermissionState(*permissions, state = PermissionState.DENIED, |
| isLegacyApp = isLegacyApp, targetSdk = targetSdk) |
| } |
| |
| private fun setAppPermissionState( |
| vararg permissions: String, |
| state: PermissionState, |
| isLegacyApp: Boolean, |
| targetSdk: Int |
| ) { |
| pressBack() |
| pressBack() |
| pressBack() |
| if (isTv) { |
| pressHome() |
| } |
| // Open the app details settings |
| context.startActivity( |
| Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { |
| data = Uri.fromParts("package", APP_PACKAGE_NAME, null) |
| addCategory(Intent.CATEGORY_DEFAULT) |
| addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| } |
| ) |
| // Open the permissions UI |
| click(byTextRes(R.string.permissions).enabled(true)) |
| for (permission in permissions) { |
| // Find the permission screen |
| val permissionLabel = getPermissionLabel(permission) |
| if (!isTv) { |
| click(By.text(permissionLabel)) |
| } |
| val wasGranted = if (isTv) { |
| false |
| } else { |
| !(waitFindObject(byTextRes(R.string.deny)).isChecked || |
| (!isLegacyApp && hasAskButton(permission) && |
| waitFindObject(byTextRes(R.string.ask)).isChecked)) |
| } |
| var alreadyChecked = false |
| if (isTv) { |
| click(By.text(permissionLabel)) |
| } else { |
| val button = waitFindObject( |
| byTextRes( |
| when (state) { |
| PermissionState.ALLOWED -> |
| if (showsForegroundOnlyButton(permission)) { |
| R.string.allow_foreground |
| } else if (isMediaStorageButton(permission, targetSdk)) { |
| R.string.allow_media_storage |
| } else if (isAllStorageButton(permission, targetSdk)) { |
| R.string.allow_external_storage |
| } else { |
| R.string.allow |
| } |
| PermissionState.DENIED -> |
| if (!isLegacyApp && hasAskButton(permission)) { |
| R.string.ask |
| } else { |
| R.string.deny |
| } |
| PermissionState.DENIED_WITH_PREJUDICE -> R.string.deny |
| } |
| ) |
| ) |
| alreadyChecked = button.isChecked |
| if (!alreadyChecked) { |
| button.click() |
| } |
| } |
| if (!alreadyChecked && isLegacyApp && wasGranted) { |
| scrollToBottom() |
| val resources = context.createPackageContext( |
| packageManager.permissionControllerPackageName, 0 |
| ).resources |
| val confirmTextRes = resources.getIdentifier( |
| "com.android.permissioncontroller:string/grant_dialog_button_deny_anyway", null, |
| null |
| ) |
| val confirmText = resources.getString(confirmTextRes) |
| click(byTextStartsWithCaseInsensitive(confirmText)) |
| } |
| if (!isTv) { |
| pressBack() |
| } |
| } |
| pressBack() |
| pressBack() |
| } |
| |
| private fun getPermissionLabel(permission: String): String { |
| val labelResName = permissionToLabelResNameMap[permission] |
| assertNotNull("Unknown permission $permission", labelResName) |
| val labelRes = platformResources.getIdentifier(labelResName, null, null) |
| return platformResources.getString(labelRes) |
| } |
| |
| private fun hasAskButton(permission: String): Boolean = |
| when (permission) { |
| android.Manifest.permission.CAMERA, |
| android.Manifest.permission.RECORD_AUDIO, |
| android.Manifest.permission.ACCESS_FINE_LOCATION, |
| android.Manifest.permission.ACCESS_COARSE_LOCATION, |
| android.Manifest.permission.ACCESS_BACKGROUND_LOCATION -> true |
| else -> false |
| } |
| |
| private fun showsForegroundOnlyButton(permission: String): Boolean = |
| when (permission) { |
| android.Manifest.permission.CAMERA, |
| android.Manifest.permission.RECORD_AUDIO -> true |
| else -> false |
| } |
| |
| private fun isMediaStorageButton(permission: String, targetSdk: Int): Boolean = |
| when (permission) { |
| android.Manifest.permission.READ_EXTERNAL_STORAGE, |
| android.Manifest.permission.WRITE_EXTERNAL_STORAGE, |
| android.Manifest.permission.ACCESS_MEDIA_LOCATION -> |
| // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set |
| targetSdk >= Build.VERSION_CODES.P |
| else -> false |
| } |
| |
| private fun isAllStorageButton(permission: String, targetSdk: Int): Boolean = |
| when (permission) { |
| android.Manifest.permission.READ_EXTERNAL_STORAGE, |
| android.Manifest.permission.WRITE_EXTERNAL_STORAGE, |
| android.Manifest.permission.ACCESS_MEDIA_LOCATION -> |
| // Default behavior, can cause issues if OPSTR_LEGACY_STORAGE is set |
| targetSdk < Build.VERSION_CODES.P |
| android.Manifest.permission.MANAGE_EXTERNAL_STORAGE -> true |
| else -> false |
| } |
| |
| private fun scrollToBottom() { |
| val scrollable = UiScrollable(UiSelector().scrollable(true)).apply { |
| swipeDeadZonePercentage = 0.25 |
| } |
| waitForIdle() |
| if (scrollable.exists()) { |
| scrollable.flingToEnd(10) |
| } |
| } |
| |
| private fun byTextRes(textRes: Int): BySelector = By.text(context.getString(textRes)) |
| |
| private fun byTextStartsWithCaseInsensitive(prefix: String): BySelector = |
| By.text(Pattern.compile("(?i)^${Pattern.quote(prefix)}.*$")) |
| |
| protected fun assertAppHasPermission(permissionName: String, expectPermission: Boolean) { |
| assertEquals( |
| if (expectPermission) { |
| PackageManager.PERMISSION_GRANTED |
| } else { |
| PackageManager.PERMISSION_DENIED |
| }, |
| packageManager.checkPermission(permissionName, APP_PACKAGE_NAME) |
| ) |
| } |
| |
| protected fun assertAppHasCalendarAccess(expectAccess: Boolean) { |
| val future = startActivityForFuture( |
| Intent().apply { |
| component = ComponentName( |
| APP_PACKAGE_NAME, "$APP_PACKAGE_NAME.CheckCalendarAccessActivity" |
| ) |
| } |
| ) |
| val result = future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) |
| assertEquals(Activity.RESULT_OK, result.resultCode) |
| assertTrue(result.resultData!!.hasExtra("$APP_PACKAGE_NAME.HAS_ACCESS")) |
| assertEquals( |
| expectAccess, |
| result.resultData!!.getBooleanExtra("$APP_PACKAGE_NAME.HAS_ACCESS", false) |
| ) |
| } |
| } |