Merge changes from topic "session-is-active"

* changes:
  Testing status of session after requiring user action
  Reactivate install session after user grants install permission
This commit is contained in:
Sumedh Sen
2022-04-25 02:26:57 +00:00
committed by Android (Google) Code Review
2 changed files with 223 additions and 18 deletions

View File

@@ -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)
}
}

View File

@@ -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);
}