1 /* <lambda>null2 * Copyright (C) 2018 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.packageinstaller.install.cts 18 19 import android.app.PendingIntent 20 import android.app.PendingIntent.FLAG_MUTABLE 21 import android.app.PendingIntent.FLAG_UPDATE_CURRENT 22 import android.content.BroadcastReceiver 23 import android.content.Context 24 import android.content.Intent 25 import android.content.Intent.EXTRA_INTENT 26 import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK 27 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK 28 import android.content.IntentFilter 29 import android.content.pm.PackageInfo 30 import android.content.pm.PackageInstaller 31 import android.content.pm.PackageInstaller.EXTRA_PRE_APPROVAL 32 import android.content.pm.PackageInstaller.EXTRA_STATUS 33 import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE 34 import android.content.pm.PackageInstaller.PreapprovalDetails 35 import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID 36 import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION 37 import android.content.pm.PackageInstaller.Session 38 import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL 39 import android.content.pm.PackageManager 40 import android.platform.test.rule.ScreenRecordRule 41 import android.provider.DeviceConfig 42 import android.provider.Settings 43 import android.util.Log 44 import androidx.core.content.FileProvider 45 import androidx.test.InstrumentationRegistry 46 import androidx.test.rule.ActivityTestRule 47 import androidx.test.uiautomator.By 48 import androidx.test.uiautomator.BySelector 49 import androidx.test.uiautomator.UiDevice 50 import androidx.test.uiautomator.UiObject2 51 import androidx.test.uiautomator.UiScrollable 52 import androidx.test.uiautomator.UiSelector 53 import androidx.test.uiautomator.Until 54 import com.android.compatibility.common.util.DisableAnimationRule 55 import com.android.compatibility.common.util.FutureResultActivity 56 import com.android.compatibility.common.util.SystemUtil 57 import java.io.File 58 import java.util.concurrent.CompletableFuture 59 import java.util.concurrent.LinkedBlockingQueue 60 import java.util.concurrent.TimeUnit 61 import java.util.regex.Pattern 62 import org.junit.After 63 import org.junit.Assert 64 import org.junit.Before 65 import org.junit.Rule 66 67 open class PackageInstallerTestBase { 68 69 companion object { 70 const val TAG = "PackageInstallerTest" 71 72 const val INSTALL_BUTTON_ID = "button1" 73 const val CANCEL_BUTTON_ID = "button2" 74 75 const val TEST_APK_NAME = "CtsEmptyTestApp.apk" 76 const val TEST_APK_PACKAGE_NAME = "android.packageinstaller.emptytestapp.cts" 77 const val TEST_APK_LOCATION = "/data/local/tmp/cts/packageinstaller" 78 79 const val TEST_LOW_TARGET_SDK_APK_NAME = "CtsEmptyTestApp_LowTargetSdk.apk" 80 const val TEST_LOW_TARGET_SDK_APK_PACKAGE_NAME = 81 "android.packageinstaller.emptytestapp.lowtargetsdk.cts" 82 83 const val INSTALL_ACTION_CB = "PackageInstallerTestBase.install_cb" 84 85 const val CONTENT_AUTHORITY = "android.packageinstaller.install.cts.fileprovider" 86 87 const val PACKAGE_INSTALLER_PACKAGE_NAME = "com.android.packageinstaller" 88 const val SYSTEM_PACKAGE_NAME = "android" 89 const val SHELL_PACKAGE_NAME = "com.android.shell" 90 const val APP_OP_STR = "REQUEST_INSTALL_PACKAGES" 91 92 const val PROPERTY_IS_PRE_APPROVAL_REQUEST_AVAILABLE = "is_preapproval_available" 93 const val PROPERTY_IS_UPDATE_OWNERSHIP_ENFORCEMENT_AVAILABLE = 94 "is_update_ownership_enforcement_available" 95 96 const val GLOBAL_TIMEOUT = 60000L 97 const val FIND_OBJECT_TIMEOUT = 1000L 98 const val INSTALL_INSTANT_APP = 0x00000800 99 const val INSTALL_REQUEST_UPDATE_OWNERSHIP = 0x02000000 100 101 val context: Context = InstrumentationRegistry.getTargetContext() 102 val testUserId: Int = context.user.identifier 103 } 104 105 @get:Rule 106 val disableAnimationsRule = DisableAnimationRule() 107 108 @get:Rule 109 val installDialogStarter = ActivityTestRule(FutureResultActivity::class.java) 110 111 @get:Rule 112 val screenRecordRule = ScreenRecordRule( 113 keepTestLevelRecordingOnSuccess = false, 114 waitExtraAfterEnd = false 115 ) 116 117 protected val pm: PackageManager = context.packageManager 118 protected val pi = pm.packageInstaller 119 protected val instrumentation = InstrumentationRegistry.getInstrumentation() 120 protected val uiDevice = UiDevice.getInstance(instrumentation) 121 122 data class SessionResult(val status: Int?, val preapproval: Boolean?, val message: String?) 123 124 /** If a status was received the value of the status, otherwise null */ 125 private var installSessionResult = LinkedBlockingQueue<SessionResult>() 126 127 private val receiver = object : BroadcastReceiver() { 128 override fun onReceive(context: Context, intent: Intent) { 129 val status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID) 130 val preapproval = intent.getBooleanExtra(EXTRA_PRE_APPROVAL, false) 131 val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE) 132 Log.d(TAG, "status: $status, msg: $msg") 133 134 if (status == STATUS_PENDING_USER_ACTION) { 135 val activityIntent = intent.getParcelableExtra(EXTRA_INTENT, Intent::class.java) 136 Assert.assertEquals(activityIntent!!.extras!!.keySet().size, 1) 137 activityIntent.addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) 138 installDialogStarter.activity.startActivityForResult(activityIntent) 139 } 140 141 installSessionResult.offer(SessionResult(status, preapproval, msg)) 142 } 143 } 144 145 @Before 146 fun wakeUpScreen() { 147 if (!uiDevice.isScreenOn) { 148 uiDevice.wakeUp() 149 } 150 uiDevice.executeShellCommand("wm dismiss-keyguard") 151 } 152 153 @Before 154 fun assertTestPackageNotInstalled() { 155 try { 156 context.packageManager.getPackageInfo(TEST_APK_PACKAGE_NAME, 0) 157 Assert.fail("Package should not be installed") 158 } catch (expected: PackageManager.NameNotFoundException) { 159 } 160 } 161 162 @Before 163 fun registerInstallResultReceiver() { 164 context.registerReceiver( 165 receiver, 166 IntentFilter(INSTALL_ACTION_CB), 167 Context.RECEIVER_EXPORTED 168 ) 169 } 170 171 @Before 172 fun waitForUIIdle() { 173 uiDevice.waitForIdle() 174 } 175 176 @After 177 fun pressBack() { 178 uiDevice.pressBack() 179 } 180 181 /** 182 * Wait for session's install result and return it 183 */ 184 protected fun getInstallSessionResult(timeout: Long = GLOBAL_TIMEOUT): SessionResult { 185 return getInstallSessionResult(installSessionResult, timeout) 186 } 187 188 protected fun getInstallSessionResult( 189 installResult: LinkedBlockingQueue<SessionResult>, 190 timeout: Long = GLOBAL_TIMEOUT, 191 ): SessionResult { 192 return installResult.poll(timeout, TimeUnit.MILLISECONDS) 193 ?: SessionResult(null, null, "Fail to poll result") 194 } 195 196 protected fun startInstallationViaSessionNoPrompt() { 197 startInstallationViaSession(0, TEST_APK_NAME, null, false) 198 } 199 200 protected fun startInstallationViaSessionWithPackageSource(packageSource: Int?) { 201 startInstallationViaSession(0, TEST_APK_NAME, packageSource) 202 } 203 204 protected fun createSession( 205 installFlags: Int, 206 isMultiPackage: Boolean, 207 packageSource: Int?, 208 paramsBlock: (PackageInstaller.SessionParams) -> Unit = {}, 209 ): Pair<Int, Session> { 210 // Create session 211 val sessionParam = PackageInstaller.SessionParams(MODE_FULL_INSTALL) 212 // Handle additional install flags 213 if (installFlags and INSTALL_INSTANT_APP != 0) { 214 sessionParam.setInstallAsInstantApp(true) 215 } 216 if (installFlags and INSTALL_REQUEST_UPDATE_OWNERSHIP != 0) { 217 sessionParam.setRequestUpdateOwnership(true) 218 } 219 if (isMultiPackage) { 220 sessionParam.setMultiPackage() 221 } 222 if (packageSource != null) { 223 sessionParam.setPackageSource(packageSource) 224 } 225 226 paramsBlock(sessionParam) 227 228 val sessionId = pi.createSession(sessionParam) 229 val session = pi.openSession(sessionId)!! 230 231 return Pair(sessionId, session) 232 } 233 234 protected fun writeSession(session: Session, apkName: String) { 235 // Write data to session 236 File(TEST_APK_LOCATION, apkName).inputStream().use { fileOnDisk -> 237 session.openWrite(apkName, 0, -1).use { sessionFile -> 238 fileOnDisk.copyTo(sessionFile) 239 } 240 } 241 } 242 243 protected fun commitSession( 244 session: Session, 245 expectedPrompt: Boolean = true, 246 needFuture: Boolean = false, 247 ): CompletableFuture<Int>? { 248 var intent = Intent(INSTALL_ACTION_CB) 249 .setPackage(context.getPackageName()) 250 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 251 val pendingIntent = PendingIntent.getBroadcast( 252 context, 253 0, 254 intent, 255 FLAG_UPDATE_CURRENT or FLAG_MUTABLE 256 ) 257 258 var dialog: CompletableFuture<Int>? = null 259 260 if (!expectedPrompt) { 261 session.commit(pendingIntent.intentSender) 262 return dialog 263 } 264 265 // Commit session 266 if (needFuture) { 267 dialog = FutureResultActivity.doAndAwaitStart { 268 session.commit(pendingIntent.intentSender) 269 } 270 } else { 271 session.commit(pendingIntent.intentSender) 272 } 273 274 // The system should have asked us to launch the installer 275 val result = getInstallSessionResult() 276 Assert.assertEquals(STATUS_PENDING_USER_ACTION, result.status) 277 Assert.assertEquals(false, result.preapproval) 278 279 return dialog 280 } 281 282 protected fun startRequestUserPreapproval( 283 session: Session, 284 details: PreapprovalDetails, 285 expectedPrompt: Boolean = true, 286 ) { 287 // In some abnormal cases, passing expectedPrompt as false to return immediately without 288 // waiting for timeout (60 secs). 289 if (!expectedPrompt) { requestSession(session, details); return } 290 291 FutureResultActivity.doAndAwaitStart { 292 requestSession(session, details) 293 } 294 295 // The system should have asked us to launch the installer 296 val result = getInstallSessionResult() 297 Assert.assertEquals(STATUS_PENDING_USER_ACTION, result.status) 298 Assert.assertEquals(true, result.preapproval) 299 } 300 301 private fun requestSession(session: Session, details: PreapprovalDetails) { 302 val pendingIntent = PendingIntent.getBroadcast( 303 context, 304 0, 305 Intent(INSTALL_ACTION_CB).setPackage(context.packageName), 306 FLAG_UPDATE_CURRENT or FLAG_MUTABLE 307 ) 308 session.requestUserPreapproval(details, pendingIntent.intentSender) 309 } 310 311 protected fun startInstallationViaSession( 312 installFlags: Int = 0, 313 apkName: String = TEST_APK_NAME, 314 packageSource: Int? = null, 315 expectedPrompt: Boolean = true, 316 needFuture: Boolean = false, 317 paramsBlock: (PackageInstaller.SessionParams) -> Unit = {}, 318 ): CompletableFuture<Int>? { 319 val (_, session) = createSession(installFlags, false, packageSource, paramsBlock) 320 writeSession(session, apkName) 321 return commitSession(session, expectedPrompt, needFuture) 322 } 323 324 protected fun writeAndCommitSession( 325 apkName: String, 326 session: Session, 327 expectedPrompt: Boolean = true, 328 ) { 329 writeSession(session, apkName) 330 commitSession(session, expectedPrompt) 331 } 332 333 protected fun startInstallationViaMultiPackageSession( 334 installFlags: Int, 335 vararg apkNames: String, 336 needFuture: Boolean = false, 337 ): CompletableFuture<Int>? { 338 val (sessionId, session) = createSession(installFlags, true, null) 339 for (apkName in apkNames) { 340 val (childSessionId, childSession) = createSession(installFlags, false, null) 341 writeSession(childSession, apkName) 342 session.addChildSessionId(childSessionId) 343 } 344 return commitSession(session, needFuture = needFuture) 345 } 346 347 /** 348 * Start an installation via an Intent. By default, it uses an intent to install 349 * the `CtsEmptyTestApp` 350 */ 351 protected fun startInstallationViaIntent( 352 intent: Intent = getInstallationIntent(), 353 ): CompletableFuture<Int> { 354 return installDialogStarter.activity.startActivityForResult(intent) 355 } 356 357 protected fun getInstallationIntent(apkName: String = TEST_APK_NAME): Intent { 358 val apkFile = File(context.filesDir, apkName) 359 if (!apkFile.exists()) { 360 File(TEST_APK_LOCATION, apkName).copyTo(target = apkFile, overwrite = true) 361 } 362 val intent = Intent(Intent.ACTION_INSTALL_PACKAGE) 363 intent.data = FileProvider.getUriForFile(context, CONTENT_AUTHORITY, apkFile) 364 intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION 365 intent.putExtra(Intent.EXTRA_RETURN_RESULT, true) 366 367 return intent 368 } 369 370 protected fun startInstallationViaPreapprovalSession(session: Session) { 371 val pendingIntent = PendingIntent.getBroadcast( 372 context, 373 0, 374 Intent(INSTALL_ACTION_CB).setPackage(context.packageName), 375 FLAG_UPDATE_CURRENT or FLAG_MUTABLE 376 ) 377 session.commit(pendingIntent.intentSender) 378 } 379 380 fun assertInstalled( 381 flags: PackageManager.PackageInfoFlags = PackageManager.PackageInfoFlags.of(0), 382 ): PackageInfo { 383 // Throws exception if package is not installed. 384 return pm.getPackageInfo(TEST_APK_PACKAGE_NAME, flags) 385 } 386 387 fun assertInstalled( 388 packageName: String, 389 flags: PackageManager.PackageInfoFlags = PackageManager.PackageInfoFlags.of(0), 390 ): PackageInfo { 391 // Throws exception if package is not installed. 392 return pm.getPackageInfo(packageName, flags) 393 } 394 395 fun assertNotInstalled( 396 packageName: String = TEST_APK_PACKAGE_NAME, 397 flags: PackageManager.PackageInfoFlags = PackageManager.PackageInfoFlags.of(0), 398 ) { 399 try { 400 pm.getPackageInfo(packageName, flags) 401 Assert.fail("Package should not be installed") 402 } catch (expected: PackageManager.NameNotFoundException) { 403 } 404 } 405 406 /** 407 * Click a button in the UI of the installer app 408 * 409 * @param resId The resource ID of the button to click 410 */ 411 fun clickInstallerUIButton(resId: String) { 412 clickInstallerUIButton(getBySelector(resId)) 413 } 414 415 fun getBySelector(id: String): BySelector { 416 // Normally, we wouldn't need to look for buttons from 2 different packages. 417 // However, to fix b/297132020, AlertController was replaced with AlertDialog and shared 418 // to selective partners, leading to fragmentation in which button surfaces in an OEM's 419 // installer app. 420 return By.res( 421 Pattern.compile( 422 String.format( 423 "(?:^%s|^%s):id/%s", 424 PACKAGE_INSTALLER_PACKAGE_NAME, 425 SYSTEM_PACKAGE_NAME, 426 id 427 ) 428 ) 429 ) 430 } 431 432 /** 433 * Click a button in the UI of the installer app 434 * 435 * @param bySelector The bySelector of the button to click 436 */ 437 fun clickInstallerUIButton(bySelector: BySelector) { 438 // Wait for a minimum 2000ms and maximum 10000ms for the UI to become idle. 439 instrumentation.uiAutomation.waitForIdle( 440 (2 * FIND_OBJECT_TIMEOUT), 441 (10 * FIND_OBJECT_TIMEOUT) 442 ) 443 444 var button: UiObject2? = null 445 val startTime = System.currentTimeMillis() 446 while (startTime + GLOBAL_TIMEOUT > System.currentTimeMillis()) { 447 try { 448 button = uiDevice.wait(Until.findObject(bySelector), FIND_OBJECT_TIMEOUT) 449 if (button != null) { 450 Log.d( 451 TAG, 452 "Found bounds: ${button.getVisibleBounds()} of button $bySelector," + 453 " text: ${button.getText()}," + 454 " package: ${button.getApplicationPackage()}" 455 ) 456 button.click() 457 return 458 } else { 459 // Maybe the screen is small. Scroll forward and attempt to click 460 scroll() 461 } 462 } catch (ignore: Throwable) { 463 } 464 } 465 Assert.fail("Failed to click the button: $bySelector") 466 } 467 468 private fun scroll() { 469 UiScrollable(UiSelector().scrollable(true)).scrollForward() 470 } 471 472 /** 473 * Sets the given secure setting to the provided value. 474 */ 475 fun setSecureSetting(secureSetting: String, value: Int) { 476 uiDevice.executeShellCommand("settings put --user $testUserId secure $secureSetting $value") 477 } 478 479 fun setSecureFrp(secureFrp: Boolean) { 480 uiDevice.executeShellCommand( 481 "settings " + 482 "put global secure_frp_mode ${if (secureFrp) 1 else 0}" 483 ) 484 Assert.assertEquals( 485 if (secureFrp) 1 else 0, 486 Settings.Global.getInt(context.contentResolver, Settings.Global.SECURE_FRP_MODE) 487 ) 488 } 489 490 @After 491 fun unregisterInstallResultReceiver() { 492 try { 493 context.unregisterReceiver(receiver) 494 } catch (ignored: IllegalArgumentException) { 495 } 496 } 497 498 @After 499 @Before 500 fun uninstallTestPackage() { 501 uninstallPackage(TEST_APK_PACKAGE_NAME) 502 } 503 504 fun uninstallPackage(packageName: String) { 505 uiDevice.executeShellCommand("pm uninstall $packageName") 506 } 507 508 fun installTestPackage(extraArgs: String = "") { 509 installPackage(TEST_APK_NAME, extraArgs) 510 } 511 512 fun installPackage(apkName: String, extraArgs: String = "") { 513 Log.d(TAG, "installPackage(): apkName=$apkName, extraArgs='$extraArgs'") 514 uiDevice.executeShellCommand( 515 "pm install $extraArgs " + 516 File(TEST_APK_LOCATION, apkName).canonicalPath 517 ) 518 } 519 520 fun getDeviceProperty(name: String): String? { 521 return SystemUtil.callWithShellPermissionIdentity { 522 DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name) 523 } 524 } 525 526 fun setDeviceProperty(name: String, value: String?) { 527 SystemUtil.callWithShellPermissionIdentity { 528 DeviceConfig.setProperty( 529 DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, 530 name, 531 value, 532 false 533 ) 534 } 535 } 536 } 537