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