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.app.Instrumentation 20 import android.app.PendingIntent 21 import android.app.PendingIntent.FLAG_MUTABLE 22 import android.app.PendingIntent.FLAG_UPDATE_CURRENT 23 import android.app.UiAutomation 24 import android.content.BroadcastReceiver 25 import android.content.ComponentName 26 import android.content.Context 27 import android.content.Context.RECEIVER_EXPORTED 28 import android.content.Intent 29 import android.content.IntentFilter 30 import android.content.pm.PackageInstaller 31 import android.content.pm.PackageInstaller.EXTRA_STATUS 32 import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE 33 import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID 34 import android.content.pm.PackageInstaller.STATUS_SUCCESS 35 import android.content.pm.PackageInstaller.SessionParams 36 import android.content.pm.PackageManager 37 import android.content.res.Resources 38 import android.os.PersistableBundle 39 import android.os.SystemClock 40 import android.platform.test.rule.ScreenRecordRule 41 import android.provider.DeviceConfig 42 import android.provider.Settings 43 import android.text.Html 44 import android.util.Log 45 import androidx.test.core.app.ActivityScenario 46 import androidx.test.platform.app.InstrumentationRegistry 47 import androidx.test.uiautomator.By 48 import androidx.test.uiautomator.BySelector 49 import androidx.test.uiautomator.StaleObjectException 50 import androidx.test.uiautomator.UiDevice 51 import androidx.test.uiautomator.UiObject2 52 import androidx.test.uiautomator.UiScrollable 53 import androidx.test.uiautomator.UiSelector 54 import androidx.test.uiautomator.Until 55 import com.android.compatibility.common.util.DisableAnimationRule 56 import com.android.compatibility.common.util.FreezeRotationRule 57 import com.android.compatibility.common.util.SystemUtil.runShellCommand 58 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow 59 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity 60 import com.android.compatibility.common.util.UiAutomatorUtils2 61 import com.android.modules.utils.build.SdkLevel 62 import com.google.common.truth.Truth.assertThat 63 import java.io.File 64 import java.util.concurrent.CompletableFuture 65 import java.util.concurrent.LinkedBlockingQueue 66 import java.util.concurrent.TimeUnit 67 import java.util.regex.Pattern 68 import org.junit.After 69 import org.junit.Assert 70 import org.junit.Assert.assertEquals 71 import org.junit.Assert.assertNotEquals 72 import org.junit.Before 73 import org.junit.Rule 74 75 @ScreenRecordRule.ScreenRecord 76 abstract class BasePermissionTest { 77 companion object { 78 private const val TAG = "BasePermissionTest" 79 80 private const val INSTALL_ACTION_CALLBACK = "BasePermissionTest.install_callback" 81 82 private const val MAX_SWIPES = 5 83 84 const val APK_DIRECTORY = "/data/local/tmp/cts-permissionui" 85 86 const val QUICK_CHECK_TIMEOUT_MILLIS = 100L 87 const val IDLE_TIMEOUT_MILLIS: Long = 1000 88 const val IDLE_LONG_TIMEOUT_MILLIS: Long = 5000 89 const val UNEXPECTED_TIMEOUT_MILLIS = 1000 90 const val TIMEOUT_MILLIS: Long = 20000 91 const val PACKAGE_INSTALLER_TIMEOUT = 60000L 92 const val NEW_WINDOW_TIMEOUT_MILLIS: Long = 20_000 93 94 @JvmStatic 95 protected val instrumentation: Instrumentation = 96 InstrumentationRegistry.getInstrumentation() 97 @JvmStatic protected val context: Context = instrumentation.context 98 @JvmStatic protected val uiAutomation: UiAutomation = instrumentation.uiAutomation 99 @JvmStatic protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) 100 @JvmStatic protected val packageManager: PackageManager = context.packageManager 101 private val packageInstaller = packageManager.packageInstaller 102 @JvmStatic 103 private val mPermissionControllerResources: Resources = 104 context 105 .createPackageContext(context.packageManager.permissionControllerPackageName, 0) 106 .resources 107 108 @JvmStatic 109 protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 110 @JvmStatic 111 protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) 112 @JvmStatic 113 protected val isAutomotive = 114 packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) 115 @JvmStatic 116 protected val isAutomotiveSplitscreen = isAutomotive && 117 packageManager.hasSystemFeature( 118 /* PackageManager.FEATURE_CAR_SPLITSCREEN_MULTITASKING */ 119 "android.software.car.splitscreen_multitasking") 120 } 121 122 @get:Rule val screenRecordRule = ScreenRecordRule(false, false) 123 124 @get:Rule val disableAnimationRule = DisableAnimationRule() 125 126 @get:Rule val freezeRotationRule = FreezeRotationRule() 127 128 var activityScenario: ActivityScenario<StartForFutureActivity>? = null 129 130 data class SessionResult(val status: Int?) 131 132 /** If a status was received the value of the status, otherwise null */ 133 private var installSessionResult = LinkedBlockingQueue<SessionResult>() 134 135 private val installSessionResultReceiver = 136 object : BroadcastReceiver() { 137 override fun onReceive(context: Context, intent: Intent) { 138 val status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID) 139 val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE) 140 Log.d(TAG, "status: $status, msg: $msg") 141 142 installSessionResult.offer(SessionResult(status)) 143 } 144 } 145 146 private var screenTimeoutBeforeTest: Long = 0L 147 148 @Before 149 fun setUp() { 150 runWithShellPermissionIdentity { 151 screenTimeoutBeforeTest = 152 Settings.System.getLong(context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) 153 Settings.System.putLong( 154 context.contentResolver, 155 Settings.System.SCREEN_OFF_TIMEOUT, 156 1800000L 157 ) 158 } 159 160 uiDevice.wakeUp() 161 runShellCommand(instrumentation, "wm dismiss-keyguard") 162 163 uiDevice.findObject(By.text("Close"))?.click() 164 } 165 166 @Before 167 fun registerInstallSessionResultReceiver() { 168 context.registerReceiver( 169 installSessionResultReceiver, 170 IntentFilter(INSTALL_ACTION_CALLBACK), 171 RECEIVER_EXPORTED 172 ) 173 } 174 175 @After 176 fun unregisterInstallSessionResultReceiver() { 177 try { 178 context.unregisterReceiver(installSessionResultReceiver) 179 } catch (ignored: IllegalArgumentException) {} 180 } 181 182 @After 183 fun tearDown() { 184 runWithShellPermissionIdentity { 185 Settings.System.putLong( 186 context.contentResolver, 187 Settings.System.SCREEN_OFF_TIMEOUT, 188 screenTimeoutBeforeTest 189 ) 190 } 191 192 try { 193 activityScenario?.close() 194 } catch (e: NullPointerException) { 195 // ignore 196 } 197 198 pressHome() 199 } 200 201 protected fun setDeviceConfigPrivacyProperty( 202 propertyName: String, 203 value: String, 204 ) { 205 runWithShellPermissionIdentity(instrumentation.uiAutomation) { 206 val valueWasSet = 207 DeviceConfig.setProperty( 208 DeviceConfig.NAMESPACE_PRIVACY, 209 /* name = */ propertyName, 210 /* value = */ value, 211 /* makeDefault = */ false 212 ) 213 check(valueWasSet) { "Could not set $propertyName to $value" } 214 } 215 } 216 217 protected fun getPermissionControllerString(res: String, vararg formatArgs: Any): Pattern { 218 val textWithHtml = 219 mPermissionControllerResources.getString( 220 mPermissionControllerResources.getIdentifier( 221 res, 222 "string", 223 "com.android.permissioncontroller" 224 ), 225 *formatArgs 226 ) 227 val textWithoutHtml = Html.fromHtml(textWithHtml, 0).toString() 228 return Pattern.compile( 229 Pattern.quote(textWithoutHtml), 230 Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE 231 ) 232 } 233 234 protected fun getPermissionControllerResString(res: String): String? { 235 try { 236 return mPermissionControllerResources.getString( 237 mPermissionControllerResources.getIdentifier( 238 res, 239 "string", 240 "com.android.permissioncontroller" 241 ) 242 ) 243 } catch (e: Resources.NotFoundException) { 244 return null 245 } 246 } 247 248 protected fun byAnyText(vararg texts: String?): BySelector { 249 var regex = "" 250 for (text in texts) { 251 if (text != null) { 252 regex = regex + Pattern.quote(text) + "|" 253 } 254 } 255 if (regex.endsWith("|")) { 256 regex = regex.dropLast(1) 257 } 258 return By.text(Pattern.compile(regex, Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE)) 259 } 260 261 protected open fun installPackage( 262 apkPath: String, 263 reinstall: Boolean = false, 264 grantRuntimePermissions: Boolean = false, 265 expectSuccess: Boolean = true, 266 installSource: String? = null 267 ) { 268 val output = 269 runShellCommandOrThrow( 270 "pm install${if (SdkLevel.isAtLeastU()) " --bypass-low-target-sdk-block" else ""} " + 271 "${if (reinstall) " -r" else ""}${if (grantRuntimePermissions) " -g" 272 else ""}${if (installSource != null) " -i $installSource" else ""} $apkPath" 273 ) 274 .trim() 275 if (expectSuccess) { 276 assertEquals("Success", output) 277 } else { 278 assertNotEquals("Success", output) 279 } 280 } 281 282 protected fun installPackageViaSession( 283 apkName: String, 284 appMetadata: PersistableBundle? = null, 285 packageSource: Int? = null, 286 allowlistedRestrictedPermissions: Set<String>? = null 287 ) { 288 val (sessionId, session) = createPackageInstallerSession( 289 packageSource, 290 allowlistedRestrictedPermissions 291 ) 292 runWithShellPermissionIdentity { 293 writePackageInstallerSession(session, apkName) 294 if (appMetadata != null) { 295 setAppMetadata(session, appMetadata) 296 } 297 commitPackageInstallerSession(session) 298 299 // No need to click installer UI here due to running in shell permission identity and 300 // not needing user interaciton to complete install. Install should have succeeded. 301 val result = getInstallSessionResult() 302 assertThat(result.status).isEqualTo(STATUS_SUCCESS) 303 } 304 } 305 306 protected fun uninstallPackage(packageName: String, requireSuccess: Boolean = true) { 307 val output = runShellCommand("pm uninstall $packageName").trim() 308 if (requireSuccess) { 309 assertEquals("Success", output) 310 } 311 } 312 313 protected fun waitFindObject(selector: BySelector): UiObject2 { 314 return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObject(selector, t) })!! 315 } 316 317 protected fun waitFindObject(selector: BySelector, timeoutMillis: Long): UiObject2 { 318 return findObjectWithRetry( 319 { t -> UiAutomatorUtils2.waitFindObject(selector, t) }, 320 timeoutMillis 321 )!! 322 } 323 324 protected fun waitFindObjectOrNull(selector: BySelector): UiObject2? { 325 return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) }) 326 } 327 328 protected fun waitFindObjectOrNull(selector: BySelector, timeoutMillis: Long): UiObject2? { 329 return findObjectWithRetry( 330 { t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) }, 331 timeoutMillis 332 ) 333 } 334 335 private fun findObjectWithRetry( 336 automatorMethod: (timeoutMillis: Long) -> UiObject2?, 337 timeoutMillis: Long = 20_000L 338 ): UiObject2? { 339 val startTime = SystemClock.elapsedRealtime() 340 return try { 341 automatorMethod(timeoutMillis) 342 } catch (e: StaleObjectException) { 343 val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime) 344 if (remainingTime <= 0) { 345 throw e 346 } 347 automatorMethod(remainingTime) 348 } 349 } 350 351 protected fun click(selector: BySelector, timeoutMillis: Long = 20_000) { 352 waitFindObject(selector, timeoutMillis).click() 353 } 354 355 protected fun clickAndWaitForWindowTransition( 356 selector: BySelector, 357 timeoutMillis: Long = 20_000 358 ) { 359 waitFindObject(selector, timeoutMillis) 360 .clickAndWait(Until.newWindow(), NEW_WINDOW_TIMEOUT_MILLIS) 361 } 362 363 protected fun findView(selector: BySelector, expected: Boolean) { 364 val timeoutMs = 365 if (expected) { 366 10000L 367 } else { 368 1000L 369 } 370 371 val exception = 372 try { 373 waitFindObject(selector, timeoutMs) 374 null 375 } catch (e: Exception) { 376 e 377 } 378 Assert.assertTrue("Expected to find view: $expected", (exception == null) == expected) 379 } 380 381 protected fun clickPermissionControllerUi(selector: BySelector, timeoutMillis: Long = 20_000) { 382 click(selector.pkg(context.packageManager.permissionControllerPackageName), timeoutMillis) 383 } 384 385 /** 386 * Clicks Permission Controller UI with a swipe based timeout instead of a time based one 387 * 388 * Use this if finding some Permission Controller UI isn't time bound. 389 * 390 * @param text The text to search for 391 * @param maxSearchSwipes See {@link UiScrollable#setMaxSearchSwipes} 392 */ 393 protected fun clickPermissionControllerUi(text: String, maxSearchSwipes: Int = 5) { 394 scrollToText(text, maxSearchSwipes).click() 395 } 396 397 private fun scrollToText(text: String, maxSearchSwipes: Int = MAX_SWIPES): UiObject2 { 398 val scrollable = 399 UiScrollable(UiSelector().scrollable(true)).apply { 400 this.maxSearchSwipes = maxSearchSwipes 401 } 402 403 scrollable.scrollTextIntoView(text) 404 405 val foundObject = 406 uiDevice.findObject( 407 By.text(text).pkg(context.packageManager.permissionControllerPackageName) 408 ) 409 Assert.assertNotNull("View not found after scrolling", foundObject) 410 411 return foundObject 412 } 413 414 protected fun pressBack() { 415 uiDevice.pressBack() 416 } 417 418 protected fun pressHome() { 419 uiDevice.pressHome() 420 } 421 422 protected fun pressDPadDown() { 423 uiDevice.pressDPadDown() 424 waitForIdle() 425 } 426 427 protected fun waitForIdle() = uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS) 428 429 protected fun waitForIdleLong() = 430 uiAutomation.waitForIdle(IDLE_LONG_TIMEOUT_MILLIS, TIMEOUT_MILLIS) 431 432 protected fun startActivityForFuture( 433 intent: Intent 434 ): CompletableFuture<Instrumentation.ActivityResult> = 435 CompletableFuture<Instrumentation.ActivityResult>().also { 436 activityScenario = 437 ActivityScenario.launch(StartForFutureActivity::class.java).onActivity { activity -> 438 activity.startActivityForFuture(intent, it) 439 } 440 } 441 442 open fun enableComponent(component: ComponentName) { 443 packageManager.setComponentEnabledSetting( 444 component, 445 PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 446 PackageManager.DONT_KILL_APP 447 ) 448 } 449 450 open fun disableComponent(component: ComponentName) { 451 packageManager.setComponentEnabledSetting( 452 component, 453 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 454 PackageManager.DONT_KILL_APP 455 ) 456 } 457 458 private fun createPackageInstallerSession( 459 packageSource: Int? = null, 460 allowlistedRestrictedPermissions: Set<String>? = null 461 ): Pair<Int, PackageInstaller.Session> { 462 // Create session 463 val sessionParam = SessionParams(SessionParams.MODE_FULL_INSTALL) 464 allowlistedRestrictedPermissions?.let { 465 sessionParam.setWhitelistedRestrictedPermissions(it) 466 } 467 468 if (packageSource != null) { 469 sessionParam.setPackageSource(packageSource) 470 } 471 472 val sessionId = packageInstaller.createSession(sessionParam) 473 val session = packageInstaller.openSession(sessionId)!! 474 475 return Pair(sessionId, session) 476 } 477 478 private fun writePackageInstallerSession(session: PackageInstaller.Session, apkName: String) { 479 val apkFile = File(APK_DIRECTORY, apkName) 480 // Write data to session 481 apkFile.inputStream().use { fileOnDisk -> 482 session 483 .openWrite(/* name= */ apkName, /* offsetBytes= */ 0, /* lengthBytes= */ -1) 484 .use { sessionFile -> fileOnDisk.copyTo(sessionFile) } 485 } 486 } 487 488 private fun commitPackageInstallerSession(session: PackageInstaller.Session) { 489 // PendingIntent that triggers a INSTALL_ACTION_CALLBACK broadcast that gets received by 490 // installSessionResultReceiver when install actions occur with this session 491 val installActionPendingIntent = 492 PendingIntent.getBroadcast( 493 context, 494 0, 495 Intent(INSTALL_ACTION_CALLBACK).setPackage(context.packageName), 496 FLAG_UPDATE_CURRENT or FLAG_MUTABLE 497 ) 498 session.commit(installActionPendingIntent.intentSender) 499 } 500 501 private fun setAppMetadata(session: PackageInstaller.Session, data: PersistableBundle) { 502 try { 503 session.setAppMetadata(data) 504 } catch (e: Exception) { 505 session.abandon() 506 throw e 507 } 508 } 509 510 /** Wait for session's install result and return it */ 511 private fun getInstallSessionResult(timeout: Long = PACKAGE_INSTALLER_TIMEOUT): SessionResult { 512 return installSessionResult.poll(timeout, TimeUnit.MILLISECONDS) 513 ?: SessionResult(null /* status */) 514 } 515 } 516