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