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