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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user