1 /*
<lambda>null2  * Copyright (C) 2020 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.content.pm
18 
19 import android.Manifest
20 import android.app.Instrumentation
21 import android.app.PendingIntent
22 import android.content.BroadcastReceiver
23 import android.content.Context
24 import android.content.Intent
25 import android.content.IntentFilter
26 import android.content.pm.PackageInstaller.SessionParams
27 import android.os.Handler
28 import android.os.HandlerThread
29 import android.platform.test.annotations.Presubmit
30 import android.util.Log
31 import androidx.test.InstrumentationRegistry
32 import androidx.test.filters.LargeTest
33 import com.android.compatibility.common.util.ShellIdentityUtils
34 import com.google.common.truth.Truth.assertThat
35 import java.util.concurrent.ArrayBlockingQueue
36 import java.util.concurrent.CountDownLatch
37 import java.util.concurrent.TimeUnit
38 import kotlin.random.Random
39 import org.junit.After
40 import org.testng.Assert.assertThrows
41 import org.junit.Assert.fail
42 import org.junit.Before
43 import org.junit.Test
44 
45 /**
46  * For verifying public [PackageInstaller] session APIs. This differs from
47  * [com.android.server.pm.PackageInstallerSessionTest] in services because that mocks the session,
48  * whereas this test uses the installer on device.
49  */
50 @Presubmit
51 class PackageSessionTests {
52 
53     companion object {
54         /**
55          * Permissions marked "hardRestricted" or "softRestricted" in core/res/AndroidManifest.xml.
56          */
57         private val RESTRICTED_PERMISSIONS = listOf(
58                 "android.permission.SEND_SMS",
59                 "android.permission.RECEIVE_SMS",
60                 "android.permission.READ_SMS",
61                 "android.permission.RECEIVE_WAP_PUSH",
62                 "android.permission.RECEIVE_MMS",
63                 "android.permission.READ_CELL_BROADCASTS",
64                 "android.permission.ACCESS_BACKGROUND_LOCATION",
65                 "android.permission.READ_CALL_LOG",
66                 "android.permission.WRITE_CALL_LOG",
67                 "android.permission.PROCESS_OUTGOING_CALLS"
68         )
69 
70         private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
71         private const val INTENT_ACTION = "com.android.server.pm.test.test_app.action"
72     }
73 
74     private val TAG = "PackageSessionTests"
75     private val context: Context = InstrumentationRegistry.getContext()
76     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
77     private val installer = context.packageManager.packageInstaller
78     private var callback: SessionStatusTrackerCallback? = null
79     private var handlerThread: HandlerThread? = null
80     private var handler: Handler? = null
81 
82     private val receiver = object : BroadcastReceiver() {
83         private val results = ArrayBlockingQueue<Intent>(1)
84 
85         override fun onReceive(context: Context, intent: Intent) {
86             // Added as a safety net. Have observed instances where the Queue isn't empty which
87             // causes the test suite to crash.
88             if (results.size != 0) {
89                 clear()
90             }
91             results.add(intent)
92         }
93 
94         fun makeIntentSender(sessionId: Int) = PendingIntent.getBroadcast(context, sessionId,
95                 Intent(INTENT_ACTION).setPackage(context.packageName),
96                 PendingIntent.FLAG_UPDATE_CURRENT
97                         or PendingIntent.FLAG_MUTABLE).intentSender
98 
99         fun getResult(unit: TimeUnit, timeout: Long) = results.poll(timeout, unit)
100 
101         fun clear() = results.clear()
102     }
103 
104     class SessionStatusTrackerCallback : PackageInstaller.SessionCallback {
105         private val TAG: String = "SessionStatusTrackerCallback"
106         private val DEFAULT_TIMEOUT: Long = 30
107         private var mSessionActiveLatch: CountDownLatch? = null
108         private var mSessionInactiveLatch: CountDownLatch? = null
109         private var mSessionFinishLatch: CountDownLatch? = null
110         private val mSessionIds = mutableSetOf<Int>()
111 
112         constructor(sessionActiveCount: Int = 0, sessionInactiveCount: Int = 0) {
113             this.mSessionActiveLatch = CountDownLatch(sessionActiveCount)
114             this.mSessionInactiveLatch = CountDownLatch(sessionInactiveCount)
115         }
116 
117         constructor(sessionFinishCount: Int = 0) {
118             this.mSessionFinishLatch = CountDownLatch(sessionFinishCount)
119         }
120 
121         override fun onCreated(sessionId: Int) {
122             mSessionIds.add(sessionId)
123         }
124 
125         override fun onBadgingChanged(sessionId: Int) {}
126 
127         override fun onActiveChanged(sessionId: Int, active: Boolean) {
128             if (mSessionIds.contains(sessionId)) {
129                 if (active) {
130                     mSessionActiveLatch?.countDown()
131                 } else {
132                     mSessionInactiveLatch?.countDown()
133                 }
134             } else {
135                 Log.d(TAG, "Did not find session ID $sessionId which was opened.")
136             }
137         }
138 
139         override fun onProgressChanged(sessionId: Int, progress: Float) {}
140 
141         override fun onFinished(sessionId: Int, success: Boolean) {
142             if (!success and mSessionIds.contains(sessionId)) {
143                 mSessionFinishLatch?.countDown()
144             }
145         }
146 
147         fun awaitSessionActiveCallbacks() {
148             mSessionActiveLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
149         }
150 
151         fun awaitSessionInactiveCallbacks() {
152             mSessionInactiveLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
153         }
154 
155         fun awaitSessionFinishCallbacks() {
156             mSessionFinishLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
157         }
158 
159         fun resetLatches() {
160             mSessionActiveLatch = null
161             mSessionInactiveLatch = null
162             mSessionFinishLatch = null
163         }
164     }
165 
166     @Before
167     fun registerReceiver() {
168         receiver.clear()
169         context.registerReceiver(receiver, IntentFilter(INTENT_ACTION))
170     }
171 
172     @After
173     fun unregisterReceiver() {
174         context.unregisterReceiver(receiver)
175     }
176 
177     @Before
178     @After
179     fun abandonAllSessions() {
180         instrumentation.uiAutomation
181                 .executeShellCommand("pm uninstall $TEST_PKG_NAME")
182 
183         installer.mySessions.asSequence()
184                 .map { it.sessionId }
185                 .forEach {
186                     try {
187                         installer.abandonSession(it)
188                     } catch (ignored: Exception) {
189                         Log.e(TAG, "Abandon failed: ", ignored)
190                         // Querying for sessions checks by calling package name, but abandoning
191                         // checks by UID, which won't match if this test failed to clean up
192                         // on a previous install + run + uninstall, so ignore these failures.
193                     }
194                 }
195 
196         // Abandoning sessions created by the @LargeTest takes time. We must ensure that all
197         // sessions are abandoned before proceeding with the next test. If all the sessions are not
198         // abandoned before starting a new test, we may encounter an IllegalStateException
199         callback?.apply {
200             awaitSessionFinishCallbacks()
201             resetLatches()
202             installer.unregisterSessionCallback(this)
203         }
204     }
205 
206     @After
207     fun revokeShellPermissionsAndCloseThread() {
208         instrumentation.uiAutomation.dropShellPermissionIdentity()
209         handlerThread?.quitSafely()
210     }
211 
212     @Test
213     fun truncateAppLabel() {
214         val longLabel = invalidAppLabel()
215         val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
216             setAppLabel(longLabel)
217         }
218 
219         createAndAbandonSession(params) {
220             assertThat(installer.getSessionInfo(it)?.appLabel)
221                     .isEqualTo(longLabel.take(PackageItemInfo.MAX_SAFE_LABEL_LENGTH))
222         }
223     }
224 
225     @Test
226     fun removeInvalidAppPackageName() {
227         val longName = invalidPackageName()
228         val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
229             setAppPackageName(longName)
230         }
231 
232         createAndAbandonSession(params) {
233             assertThat(installer.getSessionInfo(it)?.appPackageName)
234                     .isEqualTo(null)
235         }
236     }
237 
238     @Test
239     fun removeInvalidInstallerPackageName() {
240         val longName = invalidPackageName()
241         val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
242             setInstallerPackageName(longName)
243         }
244 
245         createAndAbandonSession(params) {
246             // If a custom installer name is dropped, it defaults to the caller
247             assertThat(installer.getSessionInfo(it)?.installerPackageName)
248                     .isEqualTo(context.packageName)
249         }
250     }
251 
252     @Test
253     fun truncateWhitelistPermissions() {
254         val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
255             setWhitelistedRestrictedPermissions(invalidPermissions())
256         }
257 
258         createAndAbandonSession(params) {
259             assertThat(installer.getSessionInfo(it)?.whitelistedRestrictedPermissions!!)
260                     .containsExactlyElementsIn(RESTRICTED_PERMISSIONS)
261         }
262     }
263 
264     @Test
265     fun fillPackageNameWithParsedValue() {
266         val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
267         val sessionId = installer.createSession(params)
268         val session = installer.openSession(sessionId)
269 
270         javaClass.classLoader.getResourceAsStream("PackageManagerTestAppVersion1.apk")!!
271                 .use { input ->
272                     session.openWrite("base", 0, -1)
273                             .use { output -> input.copyTo(output) }
274                 }
275 
276         // Test instrumentation doesn't have install permissions, so use shell
277         ShellIdentityUtils.invokeWithShellPermissions {
278             session.commit(receiver.makeIntentSender(sessionId))
279         }
280         session.close()
281 
282         // The actual contents aren't verified as part of this test. Only care about it finishing.
283         val result = receiver.getResult(TimeUnit.SECONDS, 30)
284         assertThat(result).isNotNull()
285 
286         val sessionInfo = installer.getSessionInfo(sessionId)
287         assertThat(sessionInfo).isNotNull()
288         assertThat(sessionInfo!!.getAppPackageName()).isEqualTo(TEST_PKG_NAME)
289     }
290 
291     @LargeTest
292     @Test
293     fun allocateMaxSessionsWithPermission() {
294         // Already invoking with shell permissions. Using the function to setup the handler
295         setupHandlerAndPermissions(/* Need permissions? */false)
296 
297         callback = SessionStatusTrackerCallback(sessionFinishCount = 1024)
298 
299         installer.registerSessionCallback(callback!!, handler!!)
300 
301         ShellIdentityUtils.invokeWithShellPermissions {
302             repeat(1024) { createDummySession() }
303             assertThrows(IllegalStateException::class.java) { createDummySession() }
304         }
305     }
306 
307     @LargeTest
308     @Test
309     fun allocateMaxSessionsNoPermission() {
310         setupHandlerAndPermissions(/* Need permissions? */false)
311 
312         callback = SessionStatusTrackerCallback(sessionFinishCount = 50)
313 
314         installer.registerSessionCallback(callback!!, handler!!)
315 
316         repeat(50) { createDummySession() }
317         assertThrows(IllegalStateException::class.java) { createDummySession() }
318     }
319 
320     @Test
321     fun whenGrantedInstallPermission_sessionIsActive() {
322         setupHandlerAndPermissions(true)
323 
324         val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
325         params.setRequireUserAction(SessionParams.USER_ACTION_REQUIRED)
326 
327         callback = SessionStatusTrackerCallback(sessionActiveCount = 2, sessionInactiveCount = 1)
328 
329         installer.registerSessionCallback(callback!!, handler!!)
330 
331         val sessionId = installer.createSession(params)
332         // sessionActiveLatch counts down once when a session is opened
333         val session = installer.openSession(sessionId)
334 
335         javaClass.classLoader.getResourceAsStream("PackageManagerTestAppVersion1.apk")!!
336                 .use { input ->
337                     session.openWrite("base", 0, -1)
338                             .use { output -> input.copyTo(output) }
339                 }
340 
341         session.commit(receiver.makeIntentSender(sessionId))
342         session.close()
343 
344         // Wait till we get one session change callback to with status 'inactive'
345         callback?.awaitSessionInactiveCallbacks()
346 
347         val installStatus = receiver.getResult(TimeUnit.SECONDS, 30)
348                 .getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
349 
350         if (installStatus == PackageInstaller.STATUS_PENDING_USER_ACTION) {
351             installer.setPermissionsResult(sessionId, true)
352         } else {
353             fail("Did not wait for user action")
354         }
355 
356         // sessionActiveLatch counts down the second time when install permission is granted.
357         // At this time, the latch opens.
358         callback?.awaitSessionActiveCallbacks()
359         assertThat(installer.getSessionInfo(sessionId)!!.isActive).isEqualTo(true)
360     }
361 
362     @Test
363     fun whenWaitingForUserAction_sessionIsInactive() {
364         setupHandlerAndPermissions(true)
365 
366         val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
367         params.setRequireUserAction(SessionParams.USER_ACTION_REQUIRED)
368 
369         callback = SessionStatusTrackerCallback(sessionActiveCount = 1, sessionInactiveCount = 1)
370 
371         installer.registerSessionCallback(callback!!, handler!!)
372 
373         val sessionId = installer.createSession(params)
374         val session = installer.openSession(sessionId)
375 
376         // Wait till we get one session change callback to with status 'active'
377         callback?.awaitSessionActiveCallbacks()
378 
379         javaClass.classLoader.getResourceAsStream("PackageManagerTestAppVersion1.apk")!!
380                 .use { input ->
381                     session.openWrite("base", 0, -1)
382                             .use { output -> input.copyTo(output) }
383                 }
384 
385         session.commit(receiver.makeIntentSender(sessionId))
386         session.close()
387 
388         // Wait till we get one session change callback to with status 'inactive'
389         callback?.awaitSessionInactiveCallbacks()
390 
391         val installStatus = receiver.getResult(TimeUnit.SECONDS, 30)
392                 .getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
393 
394         assertThat(installStatus).isEqualTo(PackageInstaller.STATUS_PENDING_USER_ACTION)
395         assertThat(installer.getSessionInfo(sessionId)!!.isActive).isEqualTo(false)
396     }
397 
398     private fun createDummySession() {
399         installer.createSession(SessionParams(SessionParams.MODE_FULL_INSTALL)
400                 .apply {
401                     setAppPackageName(invalidPackageName())
402                     setAppLabel(invalidAppLabel())
403                     setWhitelistedRestrictedPermissions(invalidPermissions())
404                 })
405     }
406 
407     private fun invalidPackageName(maxLength: Int = SessionParams.MAX_PACKAGE_NAME_LENGTH): String {
408         return (0 until (maxLength + 10))
409                 .asSequence()
410                 .mapIndexed { index, _ ->
411                     // A package name needs at least one separator
412                     if (index == 2) {
413                         '.'
414                     } else {
415                         Random.nextInt('z' - 'a').toChar() + 'a'.toInt()
416                     }
417                 }
418                 .joinToString(separator = "")
419     }
420 
421     private fun invalidAppLabel() = (0 until PackageItemInfo.MAX_SAFE_LABEL_LENGTH + 10)
422             .asSequence()
423             .map { Random.nextInt(Char.MAX_VALUE.toInt()).toChar() }
424             .joinToString(separator = "")
425 
426     private fun invalidPermissions() = RESTRICTED_PERMISSIONS.toMutableSet()
427             .apply {
428                 // Add some invalid permission names
429                 repeat(10) { add(invalidPackageName(300)) }
430             }
431 
432     private fun createAndAbandonSession(params: SessionParams, block: (Int) -> Unit = {}) {
433         val sessionId = installer.createSession(params)
434         try {
435             block(sessionId)
436         } finally {
437             installer.abandonSession(sessionId)
438         }
439     }
440 
441     private fun setupHandlerAndPermissions(needPermissions: Boolean) {
442         if (needPermissions) {
443             instrumentation.uiAutomation.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES,
444                     Manifest.permission.REQUEST_INSTALL_PACKAGES,
445                     Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION)
446         }
447         handlerThread = HandlerThread("PackageSessionTests")
448         handlerThread?.let {
449             it.start()
450             handler = Handler(it.looper)
451         }
452     }
453 }
454