1 /* <lambda>null2 * Copyright (C) 2024 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 * https://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.userrestriction.cts 18 19 import android.app.Activity 20 import android.app.PendingIntent 21 import android.content.Context 22 import android.content.Intent 23 import android.content.pm.PackageInstaller.EXTRA_STATUS 24 import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID 25 import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION 26 import android.content.pm.PackageInstaller.Session 27 import android.content.pm.PackageInstaller.SessionParams 28 import android.platform.test.annotations.AppModeFull 29 import android.util.Log 30 import androidx.core.content.FileProvider 31 import androidx.test.InstrumentationRegistry 32 import androidx.test.rule.ActivityTestRule 33 import androidx.test.uiautomator.By 34 import androidx.test.uiautomator.BySelector 35 import androidx.test.uiautomator.UiDevice 36 import androidx.test.uiautomator.UiObject 37 import androidx.test.uiautomator.UiObject2 38 import androidx.test.uiautomator.UiSelector 39 import androidx.test.uiautomator.Until 40 import com.android.bedstead.enterprise.annotations.EnsureDoesNotHaveUserRestriction 41 import com.android.bedstead.enterprise.annotations.EnsureHasUserRestriction 42 import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile 43 import com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile 44 import com.android.bedstead.enterprise.workProfile 45 import com.android.bedstead.harrier.BedsteadJUnit4 46 import com.android.bedstead.harrier.DeviceState 47 import com.android.bedstead.harrier.UserType 48 import com.android.bedstead.harrier.annotations.enterprise.DevicePolicyRelevant 49 import com.android.bedstead.nene.TestApis 50 import com.android.bedstead.nene.exceptions.AdbException 51 import com.android.bedstead.nene.userrestrictions.CommonUserRestrictions.DISALLOW_DEBUGGING_FEATURES 52 import com.android.bedstead.nene.userrestrictions.CommonUserRestrictions.DISALLOW_INSTALL_APPS 53 import com.android.bedstead.nene.users.UserReference 54 import com.android.bedstead.nene.utils.BlockingBroadcastReceiver 55 import com.android.bedstead.nene.utils.ShellCommand 56 import com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS_FULL 57 import com.android.bedstead.permissions.annotations.EnsureHasPermission 58 import com.android.compatibility.common.util.ApiTest 59 import com.android.compatibility.common.util.FutureResultActivity 60 import com.google.common.truth.Truth.assertThat 61 import com.google.common.truth.Truth.assertWithMessage 62 import java.io.File 63 import java.util.concurrent.CompletableFuture 64 import java.util.concurrent.TimeUnit 65 import java.util.regex.Pattern 66 import kotlin.test.assertFailsWith 67 import org.junit.Assert 68 import org.junit.Assert.fail 69 import org.junit.Before 70 import org.junit.ClassRule 71 import org.junit.Rule 72 import org.junit.Test 73 import org.junit.runner.RunWith 74 75 @RunWith(BedsteadJUnit4::class) 76 @AppModeFull(reason = "DEVICE_POLICY_SERVICE is null in instant mode") 77 class UserRestrictionInstallTest { 78 79 companion object { 80 const val INSTALL_BUTTON_ID = "button1" 81 const val CANCEL_BUTTON_ID = "button2" 82 83 const val PACKAGE_INSTALLER_PACKAGE_NAME = "com.android.packageinstaller" 84 const val SYSTEM_PACKAGE_NAME = "android" 85 86 const val TEST_APK_NAME = "CtsEmptyTestApp.apk" 87 const val TEST_APK_PACKAGE_NAME = "android.packageinstaller.emptytestapp.cts" 88 const val TEST_APK_LOCATION = "/data/local/tmp/cts/packageinstaller" 89 90 const val CONTENT_AUTHORITY = "android.packageinstaller.userrestriction.cts.fileprovider" 91 const val APP_INSTALL_ACTION = "android.packageinstaller.userrestriction.cts.action" 92 93 const val TIMEOUT = 60000L 94 95 @JvmField 96 @ClassRule 97 @Rule 98 val sDeviceState = DeviceState() 99 } 100 101 val TAG = UserRestrictionInstallTest::class.java.simpleName 102 103 @get:Rule 104 val installDialogStarter = ActivityTestRule(FutureResultActivity::class.java) 105 106 val context: Context = InstrumentationRegistry.getTargetContext() 107 val apkFile = File(context.filesDir, TEST_APK_NAME) 108 val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 109 110 @Before 111 fun uninstallTestApp() { 112 val cmd = ShellCommand.builder("pm uninstall") 113 cmd.addOperand(TEST_APK_PACKAGE_NAME) 114 try { 115 cmd.execute() 116 } catch (_: AdbException) { 117 fail("Could not uninstall $TEST_APK_PACKAGE_NAME") 118 } 119 } 120 121 @Before 122 fun copyTestApk() { 123 File(TEST_APK_LOCATION, TEST_APK_NAME).copyTo(target = apkFile, overwrite = true) 124 } 125 126 @Test 127 @ApiTest(apis = ["android.os.UserManager#DISALLOW_DEBUGGING_FEATURES"]) 128 @DevicePolicyRelevant 129 @EnsureHasWorkProfile 130 @EnsureHasUserRestriction(value = DISALLOW_DEBUGGING_FEATURES, onUser = UserType.WORK_PROFILE) 131 @EnsureDoesNotHaveUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 132 fun disallowDebuggingFeatures_adbInstallOnAllUsers_installedOnUnrestrictedUser() { 133 val initialUser = sDeviceState.initialUser() 134 val workProfile = sDeviceState.workProfile() 135 136 installPackageViaAdb(apkPath = "$TEST_APK_LOCATION/$TEST_APK_NAME") 137 138 assertWithMessage("Test app should be installed in initial user") 139 .that(TestApis.packages().find(TEST_APK_PACKAGE_NAME).installedOnUser(initialUser)) 140 .isTrue() 141 142 assertWithMessage( 143 "Test app shouldn't be installed in a work profile with " + 144 "$DISALLOW_DEBUGGING_FEATURES set" 145 ) 146 .that(TestApis.packages().find(TEST_APK_PACKAGE_NAME).installedOnUser(workProfile)) 147 .isFalse() 148 } 149 150 @Test 151 @ApiTest(apis = ["android.os.UserManager#DISALLOW_DEBUGGING_FEATURES"]) 152 @DevicePolicyRelevant 153 @EnsureHasWorkProfile 154 @EnsureHasUserRestriction(value = DISALLOW_DEBUGGING_FEATURES, onUser = UserType.WORK_PROFILE) 155 @EnsureDoesNotHaveUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 156 fun disallowDebuggingFeatures_adbInstallOnWorkProfile_fails() { 157 val workProfile = sDeviceState.workProfile() 158 159 installPackageViaAdb(apkPath = "$TEST_APK_LOCATION/$TEST_APK_NAME", user = workProfile) 160 161 assertWithMessage( 162 "Test app shouldn't be installed in a work profile with " + 163 "$DISALLOW_DEBUGGING_FEATURES set" 164 ) 165 .that(TestApis.packages().find(TEST_APK_PACKAGE_NAME).installedOnUser(workProfile)) 166 .isFalse() 167 } 168 169 @Test 170 @ApiTest(apis = ["android.os.UserManager#DISALLOW_DEBUGGING_FEATURES"]) 171 @DevicePolicyRelevant 172 @EnsureHasWorkProfile 173 @EnsureHasUserRestriction(value = DISALLOW_DEBUGGING_FEATURES, onUser = UserType.WORK_PROFILE) 174 @EnsureDoesNotHaveUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 175 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) 176 fun disallowDebuggingFeatures_sessionInstallOnWorkProfile_getInstallRequest() { 177 val workProfile = sDeviceState.workProfile() 178 val (_, session) = createSessionForUser(workProfile) 179 try { 180 writeSessionAsUser(workProfile, session) 181 val result: Intent? = commitSessionAsUser(workProfile, session) 182 assertThat(result).isNotNull() 183 assertThat(result!!.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID)) 184 .isEqualTo(STATUS_PENDING_USER_ACTION) 185 } finally { 186 session.abandon() 187 } 188 } 189 190 @Test 191 @ApiTest(apis = ["android.os.UserManager#DISALLOW_DEBUGGING_FEATURES"]) 192 @DevicePolicyRelevant 193 @EnsureHasUserRestriction(value = DISALLOW_DEBUGGING_FEATURES, onUser = UserType.WORK_PROFILE) 194 @EnsureDoesNotHaveUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 195 @RequireRunOnWorkProfile 196 fun disallowDebuggingFeatures_intentInstallOnWorkProfile_installationSucceeds() { 197 val appInstallIntent = getAppInstallationIntent(apkFile) 198 199 val installation = startInstallationViaIntent(appInstallIntent) 200 clickInstallerUIButton(INSTALL_BUTTON_ID) 201 202 // Install should have succeeded 203 assertThat(installation.get(TIMEOUT, TimeUnit.MILLISECONDS)).isEqualTo(Activity.RESULT_OK) 204 } 205 206 @Test 207 @ApiTest(apis = ["android.os.UserManager#DISALLOW_INSTALL_APPS"]) 208 @DevicePolicyRelevant 209 @EnsureHasWorkProfile 210 @EnsureHasUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 211 @EnsureDoesNotHaveUserRestriction( 212 value = DISALLOW_DEBUGGING_FEATURES, 213 onUser = UserType.WORK_PROFILE 214 ) 215 fun disallowInstallApps_adbInstallOnAllUsers_installedOnUnrestrictedUser() { 216 val initialUser = sDeviceState.initialUser() 217 val workProfile = sDeviceState.workProfile() 218 219 installPackageViaAdb(apkPath = "$TEST_APK_LOCATION/$TEST_APK_NAME") 220 221 var targetPackage = TestApis.packages().installedForUser(initialUser).filter { 222 it.packageName().equals(TEST_APK_PACKAGE_NAME) 223 } 224 assertWithMessage("Test app should be installed in initial user") 225 .that(targetPackage.size) 226 .isNotEqualTo(0) 227 228 targetPackage = TestApis.packages().installedForUser(workProfile).filter { 229 it.packageName().equals(TEST_APK_PACKAGE_NAME) 230 } 231 assertWithMessage( 232 "Test app shouldn't be installed in a work profile with $DISALLOW_INSTALL_APPS set" 233 ) 234 .that(targetPackage.size) 235 .isEqualTo(0) 236 } 237 238 @Test 239 @ApiTest(apis = ["android.os.UserManager#DISALLOW_INSTALL_APPS"]) 240 @DevicePolicyRelevant 241 @EnsureHasWorkProfile 242 @EnsureHasUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 243 @EnsureDoesNotHaveUserRestriction( 244 value = DISALLOW_DEBUGGING_FEATURES, 245 onUser = UserType.WORK_PROFILE 246 ) 247 fun disallowInstallApps_adbInstallOnWorkProfile_fails() { 248 val workProfile = sDeviceState.workProfile() 249 assertThat( 250 TestApis.devicePolicy().userRestrictions(workProfile).isSet(DISALLOW_INSTALL_APPS) 251 ).isTrue() 252 253 installPackageViaAdb(apkPath = "$TEST_APK_LOCATION/$TEST_APK_NAME", user = workProfile) 254 255 val targetPackage = TestApis.packages().installedForUser(workProfile).filter { 256 it.packageName().equals(TEST_APK_PACKAGE_NAME) 257 } 258 assertWithMessage( 259 "Test app shouldn't be installed in a work profile with " + 260 "$DISALLOW_DEBUGGING_FEATURES set" 261 ) 262 .that(targetPackage.size) 263 .isEqualTo(0) 264 } 265 266 @Test 267 @ApiTest(apis = ["android.os.UserManager#DISALLOW_INSTALL_APPS"]) 268 @DevicePolicyRelevant 269 @EnsureHasWorkProfile 270 @EnsureHasUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 271 @EnsureDoesNotHaveUserRestriction( 272 value = DISALLOW_DEBUGGING_FEATURES, 273 onUser = UserType.WORK_PROFILE 274 ) 275 @EnsureHasPermission(INTERACT_ACROSS_USERS_FULL) 276 fun disallowInstallApps_sessionInstallOnWorkProfile_throwsException() { 277 val workProfile = sDeviceState.workProfile() 278 assertFailsWith(SecurityException::class) { 279 createSessionForUser(workProfile) 280 } 281 } 282 283 @Test 284 @ApiTest(apis = ["android.os.UserManager#DISALLOW_INSTALL_APPS"]) 285 @DevicePolicyRelevant 286 @EnsureHasUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 287 @EnsureDoesNotHaveUserRestriction( 288 value = DISALLOW_DEBUGGING_FEATURES, 289 onUser = UserType.WORK_PROFILE 290 ) 291 @RequireRunOnWorkProfile 292 fun disallowInstallApps_intentInstallOnWorkProfile_installationFails() { 293 val appInstallIntent = getAppInstallationIntent(apkFile) 294 295 val installation = startInstallationViaIntent(appInstallIntent) 296 // Dismiss the device policy dialog 297 val closeBtn: UiObject = TestApis.ui().device().findObject( 298 UiSelector().resourceId("android:id/button1") 299 ) 300 closeBtn.click() 301 302 // Install should have failed 303 assertThat(installation.get(TIMEOUT, TimeUnit.MILLISECONDS)) 304 .isEqualTo(Activity.RESULT_CANCELED) 305 } 306 307 @Test 308 @ApiTest( 309 apis = ["android.os.UserManager#DISALLOW_DEBUGGING_FEATURES", 310 "android.os.UserManager#DISALLOW_INSTALL_APPS"] 311 ) 312 @DevicePolicyRelevant 313 @EnsureHasWorkProfile 314 @EnsureDoesNotHaveUserRestriction( 315 value = DISALLOW_DEBUGGING_FEATURES, 316 onUser = UserType.WORK_PROFILE 317 ) 318 @EnsureDoesNotHaveUserRestriction(value = DISALLOW_INSTALL_APPS, onUser = UserType.WORK_PROFILE) 319 fun unrestrictedWorkProfile_adbInstallOnAllUsers_installedOnAllUsers() { 320 val initialUser = sDeviceState.initialUser() 321 val workProfile = sDeviceState.workProfile() 322 assertThat( 323 TestApis.devicePolicy().userRestrictions(workProfile).isSet(DISALLOW_DEBUGGING_FEATURES) 324 ).isFalse() 325 assertThat( 326 TestApis.devicePolicy().userRestrictions(workProfile).isSet(DISALLOW_INSTALL_APPS) 327 ).isFalse() 328 329 installPackageViaAdb(apkPath = "$TEST_APK_LOCATION/$TEST_APK_NAME") 330 331 var targetPackage = TestApis.packages().installedForUser(initialUser).filter { 332 it.packageName().equals(TEST_APK_PACKAGE_NAME) 333 } 334 assertWithMessage("Test app should be installed in initial user") 335 .that(targetPackage.size) 336 .isNotEqualTo(0) 337 338 targetPackage = TestApis.packages().installedForUser(workProfile).filter { 339 it.packageName().equals(TEST_APK_PACKAGE_NAME) 340 } 341 assertWithMessage("Test app should be installed in work profile") 342 .that(targetPackage.size) 343 .isNotEqualTo(0) 344 } 345 346 /** 347 * Start an installation via an Intent 348 */ 349 private fun startInstallationViaIntent(intent: Intent): CompletableFuture<Int> { 350 return installDialogStarter.activity.startActivityForResult(intent) 351 } 352 353 private fun installPackageViaAdb(apkPath: String, user: UserReference? = null): String? { 354 val cmd = ShellCommand.builderForUser(user, "pm install") 355 cmd.addOperand(apkPath) 356 return try { 357 cmd.execute() 358 } catch (e: AdbException) { 359 null 360 } 361 } 362 363 @Throws(SecurityException::class) 364 private fun createSessionForUser(user: UserReference = sDeviceState.initialUser()): 365 Pair<Int, Session> { 366 val context = TestApis.context().androidContextAsUser(user) 367 val pm = context.packageManager 368 val pi = pm.packageInstaller 369 370 val params = SessionParams(SessionParams.MODE_FULL_INSTALL) 371 params.setRequireUserAction(SessionParams.USER_ACTION_REQUIRED) 372 373 val sessionId = pi.createSession(params) 374 val session = pi.openSession(sessionId) 375 376 return Pair(sessionId, session) 377 } 378 379 private fun writeSessionAsUser( 380 user: UserReference = sDeviceState.initialUser(), 381 session: Session 382 ) { 383 val context = TestApis.context().androidContextAsUser(user) 384 // val apkFile = File(context.filesDir, TEST_APK_NAME) 385 // Write data to session 386 apkFile.inputStream().use { fileOnDisk -> 387 session.openWrite(TEST_APK_NAME, 0, -1).use { sessionFile -> 388 fileOnDisk.copyTo(sessionFile) 389 } 390 } 391 } 392 393 private fun commitSessionAsUser( 394 user: UserReference = sDeviceState.initialUser(), 395 session: Session 396 ): Intent? { 397 val context = TestApis.context().androidContextAsUser(user) 398 val receiver: BlockingBroadcastReceiver = 399 sDeviceState.registerBroadcastReceiverForUser(user, APP_INSTALL_ACTION) 400 receiver.register() 401 402 val intent = Intent(APP_INSTALL_ACTION).setPackage(context.packageName) 403 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 404 val pendingIntent = PendingIntent.getBroadcast( 405 context, 406 0 /* requestCode */, 407 intent, 408 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE 409 ) 410 411 session.commit(pendingIntent.intentSender) 412 413 // The system should have asked us to launch the installer 414 return receiver.awaitForBroadcast() 415 } 416 417 private fun getAppInstallationIntent(apkFile: File): Intent { 418 val intent = Intent(Intent.ACTION_INSTALL_PACKAGE) 419 intent.data = FileProvider.getUriForFile(context, CONTENT_AUTHORITY, apkFile) 420 intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION 421 intent.putExtra(Intent.EXTRA_RETURN_RESULT, true) 422 return intent 423 } 424 425 /** 426 * Click a button in the UI of the installer app 427 * 428 * @param resId The resource ID of the button to click 429 */ 430 fun clickInstallerUIButton(resId: String) { 431 clickInstallerUIButton(getBySelector(resId)) 432 } 433 434 fun getBySelector(id: String): BySelector { 435 // Normally, we wouldn't need to look for buttons from 2 different packages. 436 // However, to fix b/297132020, AlertController was replaced with AlertDialog and shared 437 // to selective partners, leading to fragmentation in which button surfaces in an OEM's 438 // installer app. 439 return By.res( 440 Pattern.compile( 441 String.format( 442 "(?:^%s|^%s):id/%s", PACKAGE_INSTALLER_PACKAGE_NAME, SYSTEM_PACKAGE_NAME, id 443 ) 444 ) 445 ) 446 } 447 448 /** 449 * Click a button in the UI of the installer app 450 * 451 * @param bySelector The bySelector of the button to click 452 */ 453 fun clickInstallerUIButton(bySelector: BySelector) { 454 var button: UiObject2? = null 455 val startTime = System.currentTimeMillis() 456 while (startTime + TIMEOUT > System.currentTimeMillis()) { 457 try { 458 button = uiDevice.wait(Until.findObject(bySelector), 1000) 459 if (button != null) { 460 Log.d( 461 TAG, 462 "Found bounds: ${button.getVisibleBounds()} of button $bySelector," + 463 " text: ${button.getText()}," + 464 " package: ${button.getApplicationPackage()}" 465 ) 466 button.click() 467 return 468 } else { 469 // Maybe the screen is small. Swipe down and attempt to click 470 swipeDown() 471 } 472 } catch (ignore: Throwable) { 473 } 474 } 475 Assert.fail("Failed to click the button: $bySelector") 476 } 477 478 private fun swipeDown() { 479 // Perform a swipe from the center of the screen to the top of the screen. 480 // Higher the "steps" value, slower is the swipe 481 val centerX = uiDevice.displayWidth / 2 482 val centerY = uiDevice.displayHeight / 2 483 uiDevice.swipe(centerX, centerY, centerX, 0, 10) 484 } 485 } 486