1 /* <lambda>null2 * Copyright (C) 2022 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.ActivityManager 21 import android.content.Context 22 import android.content.Intent 23 import android.os.Build 24 import android.provider.DeviceConfig 25 import android.safetylabel.SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED 26 import android.text.Spanned 27 import android.text.style.ClickableSpan 28 import android.util.Log 29 import android.view.View 30 import androidx.test.filters.FlakyTest 31 import androidx.test.filters.SdkSuppress 32 import androidx.test.uiautomator.By 33 import com.android.compatibility.common.util.DeviceConfigStateChangerRule 34 import com.android.compatibility.common.util.SystemUtil 35 import com.android.compatibility.common.util.SystemUtil.eventually 36 import com.android.modules.utils.build.SdkLevel 37 import org.junit.After 38 import org.junit.Assert.assertEquals 39 import org.junit.Assert.assertFalse 40 import org.junit.Assert.assertTrue 41 import org.junit.Assume 42 import org.junit.Before 43 import org.junit.Ignore 44 import org.junit.Rule 45 import org.junit.Test 46 47 /** Permission rationale activity tests. Permission rationale is only available on U+ */ 48 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake") 49 @FlakyTest 50 class PermissionRationaleTest : BaseUsePermissionTest() { 51 52 private var activityManager: ActivityManager? = null 53 54 @get:Rule 55 val deviceConfigPermissionRationaleEnabled = 56 DeviceConfigStateChangerRule( 57 context, 58 DeviceConfig.NAMESPACE_PRIVACY, 59 PERMISSION_RATIONALE_ENABLED, 60 true.toString() 61 ) 62 63 @Before 64 fun setup() { 65 Assume.assumeTrue("Permission rationale is only available on U+", SdkLevel.isAtLeastU()) 66 Assume.assumeFalse(isAutomotive) 67 Assume.assumeFalse(isTv) 68 Assume.assumeFalse(isWatch) 69 70 activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 71 72 enableComponent(TEST_INSTALLER_ACTIVITY_COMPONENT_NAME) 73 74 installPackageWithInstallSourceAndMetadata(APP_APK_NAME_31) 75 76 assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, false) 77 } 78 79 @After 80 fun disableTestInstallerActivity() { 81 disableComponent(TEST_INSTALLER_ACTIVITY_COMPONENT_NAME) 82 } 83 84 @Test 85 fun startsPermissionRationaleActivity_failedByNullMetadata() { 86 installPackageWithInstallSourceAndNoMetadata(APP_APK_NAME_31) 87 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 88 } 89 90 @Test 91 fun startsPermissionRationaleActivity_failedByEmptyMetadata() { 92 installPackageWithInstallSourceAndEmptyMetadata(APP_APK_NAME_31) 93 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 94 } 95 96 @Test 97 fun startsPermissionRationaleActivity_failedByNoTopLevelVersion() { 98 installPackageWithInstallSourceAndMetadataWithoutTopLevelVersion(APP_APK_NAME_31) 99 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 100 } 101 102 @Test 103 fun startsPermissionRationaleActivity_failedByInvalidTopLevelVersion() { 104 installPackageWithInstallSourceAndMetadataWithInvalidTopLevelVersion(APP_APK_NAME_31) 105 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 106 } 107 108 @Test 109 fun startsPermissionRationaleActivity_failedByNoSafetyLabelVersion() { 110 installPackageWithInstallSourceAndMetadataWithoutSafetyLabelVersion(APP_APK_NAME_31) 111 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 112 } 113 114 @Test 115 fun startsPermissionRationaleActivity_failedByInvalidSafetyLabelVersion() { 116 installPackageWithInstallSourceAndMetadataWithInvalidSafetyLabelVersion(APP_APK_NAME_31) 117 navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() 118 } 119 120 @Test 121 fun startsPermissionRationaleActivity() { 122 navigateToPermissionRationaleActivity() 123 124 assertPermissionRationaleDialogIsVisible(true) 125 } 126 127 @Test 128 fun linksToInstallSource() { 129 navigateToPermissionRationaleActivity() 130 131 assertPermissionRationaleDialogIsVisible(true) 132 133 clickInstallSourceLink() 134 135 eventually { 136 assertStoreLinkClickSuccessful(installerPackageName = TEST_INSTALLER_PACKAGE_NAME) 137 } 138 } 139 140 @Ignore("b/282063206") 141 @Test 142 fun clickLinkToHelpCenter_opensHelpCenter() { 143 Assume.assumeFalse(getPermissionControllerResString(HELP_CENTER_URL_ID).isNullOrEmpty()) 144 145 navigateToPermissionRationaleActivity() 146 147 assertPermissionRationaleActivityTitleIsVisible(true) 148 assertHelpCenterLinkAvailable(true) 149 150 clickHelpCenterLink() 151 152 eventually({ assertHelpCenterLinkClickSuccessful() }, NEW_WINDOW_TIMEOUT_MILLIS) 153 } 154 155 @Test 156 fun noHelpCenterLinkAvailable_noHelpCenterClickAction() { 157 Assume.assumeTrue(getPermissionControllerResString(HELP_CENTER_URL_ID).isNullOrEmpty()) 158 159 navigateToPermissionRationaleActivity() 160 161 assertPermissionRationaleActivityTitleIsVisible(true) 162 assertHelpCenterLinkAvailable(false) 163 } 164 165 @Test 166 fun linksToSettings_noOp_dialogsNotClosed() { 167 navigateToPermissionRationaleActivity() 168 169 assertPermissionRationaleDialogIsVisible(true) 170 171 clicksSettings_doesNothing_leaves() 172 173 eventually { assertPermissionRationaleDialogIsVisible(true) } 174 } 175 176 @Test 177 fun linksToSettings_grants_dialogsClose() { 178 navigateToPermissionRationaleActivity() 179 180 assertPermissionRationaleDialogIsVisible(true) 181 182 clicksSettings_allowsForeground_leaves() 183 184 // Setting, Permission rationale and Grant dialog should be dismissed 185 eventually { 186 assertPermissionSettingsVisible(false) 187 assertPermissionRationaleDialogIsVisible(false) 188 assertPermissionRationaleContainerOnGrantDialogIsVisible(false) 189 } 190 191 assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, true) 192 } 193 194 @Test 195 fun linksToSettings_denies_dialogsClose() { 196 navigateToPermissionRationaleActivity() 197 198 assertPermissionRationaleDialogIsVisible(true) 199 200 clicksSettings_denies_leaves() 201 202 // Setting, Permission rationale and Grant dialog should be dismissed 203 eventually { 204 assertPermissionSettingsVisible(false) 205 assertPermissionRationaleDialogIsVisible(false) 206 assertPermissionRationaleContainerOnGrantDialogIsVisible(false) 207 } 208 209 assertAppHasPermission(Manifest.permission.ACCESS_FINE_LOCATION, false) 210 } 211 212 private fun navigateToPermissionRationaleActivity_failedShowPermissionRationaleContainer() { 213 requestAppPermissionsForNoResult(Manifest.permission.ACCESS_FINE_LOCATION) { 214 assertPermissionRationaleContainerOnGrantDialogIsVisible(false) 215 } 216 } 217 218 private fun navigateToPermissionRationaleActivity() { 219 requestAppPermissionsForNoResult(Manifest.permission.ACCESS_FINE_LOCATION) { 220 assertPermissionRationaleContainerOnGrantDialogIsVisible(true) 221 clickPermissionRationaleViewInGrantDialog() 222 } 223 } 224 225 private fun clickInstallSourceLink() { 226 findView(By.res(DATA_SHARING_SOURCE_MESSAGE_ID), true) 227 228 eventually { 229 // UiObject2 doesn't expose CharSequence. 230 val node = 231 uiAutomation.rootInActiveWindow 232 .findAccessibilityNodeInfosByViewId(DATA_SHARING_SOURCE_MESSAGE_ID)[0] 233 assertTrue(node.isVisibleToUser) 234 val text = node.text as Spanned 235 val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] 236 // We could pass in null here in Java, but we need an instance in Kotlin. 237 doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) } 238 } 239 } 240 241 private fun clickHelpCenterLink() { 242 findView(By.res(LEARN_MORE_MESSAGE_ID), true) 243 244 eventually { 245 // UiObject2 doesn't expose CharSequence. 246 val node = 247 uiAutomation.rootInActiveWindow 248 .findAccessibilityNodeInfosByViewId(LEARN_MORE_MESSAGE_ID)[0] 249 assertTrue(node.isVisibleToUser) 250 val text = node.text as Spanned 251 val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] 252 // We could pass in null here in Java, but we need an instance in Kotlin. 253 doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) } 254 } 255 } 256 257 private fun clickSettingsLink() { 258 findView(By.res(SETTINGS_MESSAGE_ID), true) 259 260 eventually { 261 // UiObject2 doesn't expose CharSequence. 262 val node = 263 uiAutomation.rootInActiveWindow 264 .findAccessibilityNodeInfosByViewId(SETTINGS_MESSAGE_ID)[0] 265 assertTrue(node.isVisibleToUser) 266 val text = node.text as Spanned 267 val clickableSpan = text.getSpans(0, text.length, ClickableSpan::class.java)[0] 268 // We could pass in null here in Java, but we need an instance in Kotlin. 269 doAndWaitForWindowTransition { clickableSpan.onClick(View(context)) } 270 } 271 } 272 273 private fun clicksSettings_doesNothing_leaves() { 274 clickSettingsLink() 275 eventually { assertPermissionSettingsVisible(true) } 276 pressBack() 277 } 278 279 private fun clicksSettings_allowsForeground_leaves() { 280 clickSettingsLink() 281 eventually { clickAllowForegroundInSettings() } 282 pressBack() 283 } 284 285 private fun clicksSettings_denies_leaves() { 286 clickSettingsLink() 287 eventually { clicksDenyInSettings() } 288 pressBack() 289 } 290 291 private fun assertHelpCenterLinkAvailable(expected: Boolean) { 292 // Message should always be visible 293 findView(By.res(LEARN_MORE_MESSAGE_ID), true) 294 295 // Verify the link is (or isn't) in message 296 eventually { 297 // UiObject2 doesn't expose CharSequence. 298 val node = 299 uiAutomation.rootInActiveWindow 300 .findAccessibilityNodeInfosByViewId(LEARN_MORE_MESSAGE_ID)[0] 301 assertTrue(node.isVisibleToUser) 302 val text = node.text as Spanned 303 val clickableSpans = text.getSpans(0, text.length, ClickableSpan::class.java) 304 305 if (expected) { 306 assertFalse("Expected help center link, but none found", clickableSpans.isEmpty()) 307 } else { 308 assertTrue("Expected no links, but found one", clickableSpans.isEmpty()) 309 } 310 } 311 } 312 313 private fun assertPermissionSettingsVisible(expected: Boolean) { 314 findView(By.res(DENY_RADIO_BUTTON), expected = expected) 315 } 316 317 private fun assertStoreLinkClickSuccessful( 318 installerPackageName: String, 319 packageName: String? = null 320 ) { 321 SystemUtil.runWithShellPermissionIdentity { 322 val runningTasks = activityManager!!.getRunningTasks(1) 323 324 assertFalse("Expected runningTasks to not be empty", runningTasks.isEmpty()) 325 326 val taskInfo = runningTasks[0] 327 val observedIntentAction = taskInfo.baseIntent.action 328 val observedPackageName = taskInfo.baseIntent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) 329 val observedInstallerPackageName = taskInfo.topActivity?.packageName 330 331 assertEquals( 332 "Unexpected intent action", 333 Intent.ACTION_SHOW_APP_INFO, 334 observedIntentAction 335 ) 336 assertEquals( 337 "Unexpected installer package name", 338 installerPackageName, 339 observedInstallerPackageName 340 ) 341 assertEquals("Unexpected package name", packageName, observedPackageName) 342 } 343 } 344 345 private fun assertHelpCenterLinkClickSuccessful() { 346 SystemUtil.runWithShellPermissionIdentity { 347 val runningTasks = activityManager!!.getRunningTasks(5) 348 349 Log.v(TAG, "# running tasks: ${runningTasks.size}") 350 assertFalse("Expected runningTasks to not be empty", runningTasks.isEmpty()) 351 352 runningTasks.forEachIndexed { index, runningTaskInfo -> 353 Log.v(TAG, "task $index ${runningTaskInfo.baseIntent}") 354 } 355 356 val taskInfo = runningTasks[0] 357 val observedIntentAction = taskInfo.baseIntent.action 358 val observedIntentDataString = taskInfo.baseIntent.dataString 359 val observedIntentScheme: String? = taskInfo.baseIntent.scheme 360 361 Log.v(TAG, "task base intent: ${taskInfo.baseIntent}") 362 assertEquals("Unexpected intent action", Intent.ACTION_VIEW, observedIntentAction) 363 364 val expectedUrl = getPermissionControllerResString(HELP_CENTER_URL_ID)!! 365 assertFalse(observedIntentDataString.isNullOrEmpty()) 366 assertTrue(observedIntentDataString?.startsWith(expectedUrl) ?: false) 367 368 assertFalse(observedIntentScheme.isNullOrEmpty()) 369 assertEquals("https", observedIntentScheme) 370 } 371 } 372 373 companion object { 374 private val TAG = PermissionRationaleTest::class.java.simpleName 375 376 private const val DATA_SHARING_SOURCE_MESSAGE_ID = 377 "com.android.permissioncontroller:id/data_sharing_source_message" 378 private const val LEARN_MORE_MESSAGE_ID = 379 "com.android.permissioncontroller:id/learn_more_message" 380 private const val SETTINGS_MESSAGE_ID = 381 "com.android.permissioncontroller:id/settings_message" 382 383 private const val HELP_CENTER_URL_ID = "data_sharing_help_center_link" 384 } 385 } 386