• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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