diff --git a/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt b/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt index 18d82afc5f75e..966d362f1fd19 100644 --- a/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt +++ b/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt @@ -16,6 +16,7 @@ package android.content.pm +import android.Manifest import android.app.Instrumentation import android.app.PendingIntent import android.content.BroadcastReceiver @@ -23,18 +24,23 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller.SessionParams +import android.os.Handler +import android.os.HandlerThread import android.platform.test.annotations.Presubmit +import android.util.Log import androidx.test.InstrumentationRegistry import androidx.test.filters.LargeTest import com.android.compatibility.common.util.ShellIdentityUtils import com.google.common.truth.Truth.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.testng.Assert.assertThrows import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.random.Random +import org.junit.After +import org.testng.Assert.assertThrows +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test /** * For verifying public [PackageInstaller] session APIs. This differs from @@ -65,27 +71,98 @@ class PackageSessionTests { private const val INTENT_ACTION = "com.android.server.pm.test.test_app.action" } + private val TAG = "PackageSessionTests" private val context: Context = InstrumentationRegistry.getContext() private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() - private val installer = context.packageManager.packageInstaller + private var callback: SessionStatusTrackerCallback? = null + private var handlerThread: HandlerThread? = null + private var handler: Handler? = null private val receiver = object : BroadcastReceiver() { private val results = ArrayBlockingQueue(1) override fun onReceive(context: Context, intent: Intent) { + // Added as a safety net. Have observed instances where the Queue isn't empty which + // causes the test suite to crash. + if (results.size != 0) { + clear() + } results.add(intent) } fun makeIntentSender(sessionId: Int) = PendingIntent.getBroadcast(context, sessionId, Intent(INTENT_ACTION), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE_UNAUDITED).intentSender + PendingIntent.FLAG_UPDATE_CURRENT + or PendingIntent.FLAG_MUTABLE_UNAUDITED).intentSender fun getResult(unit: TimeUnit, timeout: Long) = results.poll(timeout, unit) fun clear() = results.clear() } + class SessionStatusTrackerCallback : PackageInstaller.SessionCallback { + private val TAG: String = "SessionStatusTrackerCallback" + private val DEFAULT_TIMEOUT: Long = 30 + private var mSessionActiveLatch: CountDownLatch? = null + private var mSessionInactiveLatch: CountDownLatch? = null + private var mSessionFinishLatch: CountDownLatch? = null + private val mSessionIds = mutableSetOf() + + constructor(sessionActiveCount: Int = 0, sessionInactiveCount: Int = 0) { + this.mSessionActiveLatch = CountDownLatch(sessionActiveCount) + this.mSessionInactiveLatch = CountDownLatch(sessionInactiveCount) + } + + constructor(sessionFinishCount: Int = 0) { + this.mSessionFinishLatch = CountDownLatch(sessionFinishCount) + } + + override fun onCreated(sessionId: Int) { + mSessionIds.add(sessionId) + } + + override fun onBadgingChanged(sessionId: Int) {} + + override fun onActiveChanged(sessionId: Int, active: Boolean) { + if (mSessionIds.contains(sessionId)) { + if (active) { + mSessionActiveLatch?.countDown() + } else { + mSessionInactiveLatch?.countDown() + } + } else { + Log.d(TAG, "Did not find session ID $sessionId which was opened.") + } + } + + override fun onProgressChanged(sessionId: Int, progress: Float) {} + + override fun onFinished(sessionId: Int, success: Boolean) { + if (!success and mSessionIds.contains(sessionId)) { + mSessionFinishLatch?.countDown() + } + } + + fun awaitSessionActiveCallbacks() { + mSessionActiveLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + } + + fun awaitSessionInactiveCallbacks() { + mSessionInactiveLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + } + + fun awaitSessionFinishCallbacks() { + mSessionFinishLatch?.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + } + + fun resetLatches() { + mSessionActiveLatch = null + mSessionInactiveLatch = null + mSessionFinishLatch = null + } + } + @Before fun registerReceiver() { receiver.clear() @@ -101,8 +178,7 @@ class PackageSessionTests { @After fun abandonAllSessions() { instrumentation.uiAutomation - .executeShellCommand("pm uninstall com.android.server.pm.test.test_app") - .close() + .executeShellCommand("pm uninstall $TEST_PKG_NAME") installer.mySessions.asSequence() .map { it.sessionId } @@ -110,11 +186,27 @@ class PackageSessionTests { try { installer.abandonSession(it) } catch (ignored: Exception) { + Log.e(TAG, "Abandon failed: ", ignored) // Querying for sessions checks by calling package name, but abandoning // checks by UID, which won't match if this test failed to clean up // on a previous install + run + uninstall, so ignore these failures. } } + + // Abandoning sessions created by the @LargeTest takes time. We must ensure that all + // sessions are abandoned before proceeding with the next test. If all the sessions are not + // abandoned before starting a new test, we may encounter an IllegalStateException + callback?.apply { + awaitSessionFinishCallbacks() + resetLatches() + installer.unregisterSessionCallback(this) + } + } + + @After + fun revokeShellPermissionsAndCloseThread() { + instrumentation.uiAutomation.dropShellPermissionIdentity() + handlerThread?.quitSafely() } @Test @@ -199,6 +291,13 @@ class PackageSessionTests { @LargeTest @Test fun allocateMaxSessionsWithPermission() { + // Already invoking with shell permissions. Using the function to setup the handler + setupHandlerAndPermissions(/* Need permissions? */false) + + callback = SessionStatusTrackerCallback(sessionFinishCount = 1024) + + installer.registerSessionCallback(callback!!, handler!!) + ShellIdentityUtils.invokeWithShellPermissions { repeat(1024) { createDummySession() } assertThrows(IllegalStateException::class.java) { createDummySession() } @@ -208,10 +307,94 @@ class PackageSessionTests { @LargeTest @Test fun allocateMaxSessionsNoPermission() { + setupHandlerAndPermissions(/* Need permissions? */false) + + callback = SessionStatusTrackerCallback(sessionFinishCount = 50) + + installer.registerSessionCallback(callback!!, handler!!) + repeat(50) { createDummySession() } assertThrows(IllegalStateException::class.java) { createDummySession() } } + @Test + fun whenGrantedInstallPermission_sessionIsActive() { + setupHandlerAndPermissions(true) + + val params = SessionParams(SessionParams.MODE_FULL_INSTALL) + params.setRequireUserAction(SessionParams.USER_ACTION_REQUIRED) + + callback = SessionStatusTrackerCallback(sessionActiveCount = 2, sessionInactiveCount = 1) + + installer.registerSessionCallback(callback!!, handler!!) + + val sessionId = installer.createSession(params) + // sessionActiveLatch counts down once when a session is opened + val session = installer.openSession(sessionId) + + javaClass.classLoader.getResourceAsStream("PackageManagerTestAppVersion1.apk")!! + .use { input -> + session.openWrite("base", 0, -1) + .use { output -> input.copyTo(output) } + } + + session.commit(receiver.makeIntentSender(sessionId)) + session.close() + + // Wait till we get one session change callback to with status 'inactive' + callback?.awaitSessionInactiveCallbacks() + + val installStatus = receiver.getResult(TimeUnit.SECONDS, 30) + .getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + + if (installStatus == PackageInstaller.STATUS_PENDING_USER_ACTION) { + installer.setPermissionsResult(sessionId, true) + } else { + fail("Did not wait for user action") + } + + // sessionActiveLatch counts down the second time when install permission is granted. + // At this time, the latch opens. + callback?.awaitSessionActiveCallbacks() + assertThat(installer.getSessionInfo(sessionId)!!.isActive).isEqualTo(true) + } + + @Test + fun whenWaitingForUserAction_sessionIsInactive() { + setupHandlerAndPermissions(true) + + val params = SessionParams(SessionParams.MODE_FULL_INSTALL) + params.setRequireUserAction(SessionParams.USER_ACTION_REQUIRED) + + callback = SessionStatusTrackerCallback(sessionActiveCount = 1, sessionInactiveCount = 1) + + installer.registerSessionCallback(callback!!, handler!!) + + val sessionId = installer.createSession(params) + val session = installer.openSession(sessionId) + + // Wait till we get one session change callback to with status 'active' + callback?.awaitSessionActiveCallbacks() + + javaClass.classLoader.getResourceAsStream("PackageManagerTestAppVersion1.apk")!! + .use { input -> + session.openWrite("base", 0, -1) + .use { output -> input.copyTo(output) } + } + + session.commit(receiver.makeIntentSender(sessionId)) + session.close() + + // Wait till we get one session change callback to with status 'inactive' + callback?.awaitSessionInactiveCallbacks() + + val installStatus = receiver.getResult(TimeUnit.SECONDS, 30) + .getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + + assertThat(installStatus).isEqualTo(PackageInstaller.STATUS_PENDING_USER_ACTION) + assertThat(installer.getSessionInfo(sessionId)!!.isActive).isEqualTo(false) + } + private fun createDummySession() { installer.createSession(SessionParams(SessionParams.MODE_FULL_INSTALL) .apply { @@ -254,4 +437,15 @@ class PackageSessionTests { installer.abandonSession(sessionId) } } + + private fun setupHandlerAndPermissions(needPermissions: Boolean) { + if (needPermissions) { + instrumentation.uiAutomation.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.REQUEST_INSTALL_PACKAGES, + Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION) + } + handlerThread = HandlerThread("PackageSessionTests") + handlerThread?.start() + handler = Handler(handlerThread?.looper) + } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 3e314a4e0f63b..cd84b4085309b 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -2145,7 +2145,15 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mUserActionRequired = wasUserActionIntentSent; } if (wasUserActionIntentSent) { + // Commit was keeping session marked as active until now; release + // that extra refcount so session appears idle. + deactivate(); return; + } else if (mUserActionRequired) { + // If user action is required, control comes back here when the user allows + // the installation. At this point, the session is marked active once again, + // since installation is in progress. + activate(); } if (params.isStaged) { @@ -2415,10 +2423,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); sendOnUserActionRequired(mContext, target, sessionId, intent); - - // Commit was keeping session marked as active until now; release - // that extra refcount so session appears idle. - closeInternal(false); } @WorkerThread @@ -3479,10 +3483,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } public void open() throws IOException { - if (mActiveCount.getAndIncrement() == 0) { - mCallback.onSessionActiveChanged(this, true); - } - + activate(); boolean wasPrepared; synchronized (mLock) { wasPrepared = mPrepared; @@ -3504,21 +3505,31 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + private void activate() { + if (mActiveCount.getAndIncrement() == 0) { + mCallback.onSessionActiveChanged(this, true); + } + } + @Override public void close() { closeInternal(true); } private void closeInternal(boolean checkCaller) { - int activeCount; synchronized (mLock) { if (checkCaller) { assertCallerIsOwnerOrRoot(); } + } + deactivate(); + } + private void deactivate() { + int activeCount; + synchronized (mLock) { activeCount = mActiveCount.decrementAndGet(); } - if (activeCount == 0) { mCallback.onSessionActiveChanged(this, false); }