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