Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 359ec4dd authored by Sumedh Sen's avatar Sumedh Sen Committed by Android (Google) Code Review
Browse files

Merge changes from topic "session-is-active"

* changes:
  Testing status of session after requiring user action
  Reactivate install session after user grants install permission
parents 6454d2f2 c1a7baba
Loading
Loading
Loading
Loading
+202 −8
Original line number Diff line number Diff line
@@ -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<Intent>(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<Int>()

        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)
    }
}
+21 −10
Original line number Diff line number Diff line
@@ -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);
        }