blob: 968960adf29621e40bc173d94a64aaca4957a77a [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tests.stagedinstall;
import static com.android.tests.stagedinstall.PackageInstallerSessionInfoSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Pair;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* This series of tests are meant to be driven by a host, since some of the interactions being
* tested require the device to be rebooted, and some assertions to be verified post-reboot.
* The convention used here (not enforced) is that the test methods in this file will be named
* the same way as the test methods in the "host" class (see e.g.
* {@code com.android.test.stagedinstall.host.StagedInstallTest}), with an optional suffix preceded
* by an underscore, in case of multiple phases.
* Example:
* - In {@code com.android.test.stagedinstall.host.StagedInstallTest}:
*
* @Test
* public void testInstallStagedApk() throws Exception {
* runPhase("testInstallStagedApk_Commit");
* getDevice().reboot();
* runPhase("testInstallStagedApk_VerifyPostReboot");
* }
* - In this class:
* @Test public void testInstallStagedApk_Commit() throws Exception;
* @Test public void testInstallStagedApk_VerifyPostReboot() throws Exception;
*/
@RunWith(JUnit4.class)
public class StagedInstallTest {
private static final String TAG = "StagedInstallTest";
private static final String TEST_APP_A = "com.android.tests.stagedinstall.testapp.A";
private static final String TEST_APP_B = "com.android.tests.stagedinstall.testapp.B";
private static final String SHIM_APEX_PACKAGE_NAME = "com.android.apex.cts.shim";
private static final String NOT_PRE_INSTALLED_SHIM_APEX_PACKAGE_NAME =
"com.android.apex.cts.shim_not_pre_installed";
private File mTestStateFile = new File(
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
"ctsstagedinstall_state");
private static final Duration WAIT_FOR_SESSION_REMOVED_TTL = Duration.ofSeconds(10);
private static final Duration SLEEP_DURATION = Duration.ofMillis(200);
@Before
public void adoptShellPermissions() {
InstrumentationRegistry
.getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES);
}
@After
public void dropShellPermissions() {
InstrumentationRegistry
.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
}
@Before
public void clearBroadcastReceiver() {
SessionUpdateBroadcastReceiver.sessionBroadcasts.clear();
}
// This is marked as @Test to take advantage of @Before/@After methods of this class. Actual
// purpose of this method to be called before and after each test case of
// com.android.test.stagedinstall.host.StagedInstallTest to reduce tests flakiness.
@Test
public void cleanUp() throws Exception {
PackageInstaller packageInstaller = getPackageInstaller();
List<PackageInstaller.SessionInfo> stagedSessions = packageInstaller.getStagedSessions();
for (PackageInstaller.SessionInfo sessionInfo : stagedSessions) {
try {
Log.i(TAG, "abandoning session " + sessionInfo.getSessionId());
packageInstaller.abandonSession(sessionInfo.getSessionId());
} catch (Exception e) {
Log.e(TAG, "Failed to abandon session " + sessionInfo.getSessionId(), e);
}
}
uninstall(TEST_APP_A);
uninstall(TEST_APP_B);
Files.deleteIfExists(mTestStateFile.toPath());
}
@Test
public void testFailInstallIfNoPermission() throws Exception {
dropShellPermissions();
try {
createStagedSession();
fail(); // Should have thrown SecurityException.
} catch (SecurityException e) {
// This would be a better version, but it requires a version of truth not present in the
// tree yet.
// assertThat(e).hasMessageThat().containsMatch(...);
assertThat(e.getMessage()).containsMatch(
"Neither user [0-9]+ nor current process has "
+ "android.permission.INSTALL_PACKAGES");
}
}
@Test
public void testInstallStagedApk_Commit() throws Exception {
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
}
@Test
public void testInstallStagedApk_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(1);
}
@Test
public void testInstallStagedApk_AbandonSessionIsNoop() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
// Session is in a final state. Test that abandoning the session doesn't remove it from the
// session database.
getPackageInstaller().abandonSession(sessionId);
assertSessionApplied(sessionId);
}
@Test
public void testInstallMultipleStagedApks_Commit() throws Exception {
int sessionId = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk")
.assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
assertThat(getInstalledVersion(TEST_APP_B)).isEqualTo(-1);
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
assertThat(getInstalledVersion(TEST_APP_B)).isEqualTo(-1);
}
@Test
public void testInstallMultipleStagedApks_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(1);
assertThat(getInstalledVersion(TEST_APP_B)).isEqualTo(1);
}
@Test
public void testFailInstallAnotherSessionAlreadyInProgress_BothSinglePackage()
throws Exception {
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
StageSessionResult failedSessionResult = stageSingleApk("StagedInstallTestAppAv1.apk");
assertThat(failedSessionResult.getErrorMessage()).contains(
"There is already in-progress committed staged session");
getPackageInstaller().abandonSession(sessionId);
}
@Test
public void testFailInstallAnotherSessionAlreadyInProgress_SinglePackageMultiPackage()
throws Exception {
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
StageSessionResult failedSessionResult = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk");
assertThat(failedSessionResult.getErrorMessage()).contains(
"There is already in-progress committed staged session");
getPackageInstaller().abandonSession(sessionId);
}
@Test
public void testFailInstallAnotherSessionAlreadyInProgress_MultiPackageSinglePackage()
throws Exception {
int sessionId = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk").assertSuccessful().getSessionId();
StageSessionResult failedSessionResult = stageSingleApk(
"StagedInstallTestAppAv1.apk");
assertThat(failedSessionResult.getErrorMessage()).contains(
"There is already in-progress committed staged session");
getPackageInstaller().abandonSession(sessionId);
}
@Test
public void testFailInstallAnotherSessionAlreadyInProgress_BothMultiPackage()
throws Exception {
int sessionId = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk").assertSuccessful().getSessionId();
StageSessionResult failedSessionResult = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk");
assertThat(failedSessionResult.getErrorMessage()).contains(
"There is already in-progress committed staged session");
getPackageInstaller().abandonSession(sessionId);
}
@Test
public void testAbandonStagedApkBeforeReboot_CommitAndAbandon() throws Exception {
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
waitForIsReadyBroadcast(sessionId);
PackageInstaller.SessionInfo session = getStagedSessionInfo(sessionId);
assertSessionReady(sessionId);
abandonSession(sessionId);
assertThat(getStagedSessionInfo(sessionId)).isNull();
// Allow the session to be removed from PackageInstaller
Duration spentWaiting = Duration.ZERO;
while (spentWaiting.compareTo(WAIT_FOR_SESSION_REMOVED_TTL) < 0) {
session = getSessionInfo(sessionId);
if (session == null) {
Log.i(TAG, "Done waiting after " + spentWaiting);
break;
}
try {
Thread.sleep(SLEEP_DURATION.toMillis());
spentWaiting = spentWaiting.plus(SLEEP_DURATION);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
assertThat(session).isNull();
}
@Test
public void testAbandonStagedApkBeforeReboot_VerifyPostReboot() throws Exception {
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
}
@Test
public void testGetActiveStagedSession() throws Exception {
PackageInstaller packageInstaller = getPackageInstaller();
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
PackageInstaller.SessionInfo session = packageInstaller.getActiveStagedSession();
assertThat(session.getSessionId()).isEqualTo(sessionId);
}
@Test
public void testGetActiveStagedSessionNoSessionActive() throws Exception {
PackageInstaller packageInstaller = getPackageInstaller();
PackageInstaller.SessionInfo session = packageInstaller.getActiveStagedSession();
assertThat(session).isNull();
}
@Test
public void testGetGetActiveStagedSession_MultiApkSession() throws Exception {
int sessionId = stageMultipleApks(
"StagedInstallTestAppAv1.apk",
"StagedInstallTestAppBv1.apk")
.assertSuccessful().getSessionId();
PackageInstaller.SessionInfo session = getPackageInstaller().getActiveStagedSession();
assertThat(session.getSessionId()).isEqualTo(sessionId);
}
@Test
public void testStagedInstallDowngrade_DowngradeNotRequested_Fails_Commit() throws Exception {
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
installNonStaged("StagedInstallTestAppAv2.apk");
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(2);
PackageInstaller.SessionInfo sessionInfo = waitForBroadcast(sessionId);
assertThat(sessionInfo).isStagedSessionFailed();
}
@Test
public void testStagedInstallDowngrade_DowngradeRequested_Commit() throws Exception {
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
installNonStaged("StagedInstallTestAppAv2.apk");
int sessionId = stageDowngradeSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(2);
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
}
@Test
public void testStagedInstallDowngrade_DowngradeRequested_Fails_Commit() throws Exception {
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
installNonStaged("StagedInstallTestAppAv2.apk");
int sessionId = stageDowngradeSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(2);
PackageInstaller.SessionInfo sessionInfo = waitForBroadcast(sessionId);
assertThat(sessionInfo).isStagedSessionFailed();
}
@Test
public void testStagedInstallDowngrade_DowngradeRequested_DebugBuild_VerifyPostReboot()
throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
// App should be downgraded.
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(1);
}
@Test
public void testInstallStagedApex_Commit() throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
int sessionId = stageSingleApk(
"com.android.apex.cts.shim.v2.apex").assertSuccessful().getSessionId();
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
// Version shouldn't change before reboot.
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
}
@Test
public void testInstallStagedApex_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
}
@Test
public void testInstallStagedApexAndApk_Commit() throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
int sessionId = stageMultipleApks(
"com.android.apex.cts.shim.v2.apex",
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
// Version shouldn't change before reboot.
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
}
@Test
public void testInstallStagedApexAndApk_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(1);
}
@Test
public void testsFailsNonStagedApexInstall() throws Exception {
PackageInstaller installer = getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
params.setInstallAsApex();
try {
installer.createSession(params);
fail("IllegalArgumentException expected");
} catch (IllegalArgumentException expected) {
assertThat(expected.getMessage()).contains(
"APEX files can only be installed as part of a staged session");
}
}
@Test
public void testInstallStagedNonPreInstalledApex_Fails() throws Exception {
assertThat(getInstalledVersion(NOT_PRE_INSTALLED_SHIM_APEX_PACKAGE_NAME)).isEqualTo(-1);
int sessionId = stageSingleApk(
"com.android.apex.cts.shim_not_pre_installed.apex")
.assertSuccessful().getSessionId();
PackageInstaller.SessionInfo sessionInfo = waitForBroadcast(sessionId);
assertThat(sessionInfo).isStagedSessionFailed();
}
@Test
public void testStageApkWithSameNameAsApexShouldFail_Commit() throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
int sessionId = stageSingleApk(
"StagedInstallTestAppSamePackageNameAsApex.apk").assertSuccessful().getSessionId();
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
}
@Test
public void testStageApkWithSameNameAsApexShouldFail_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionFailed(sessionId);
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
}
@Test
public void testNonStagedInstallApkWithSameNameAsApexShouldFail() throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
PackageInstaller packageInstaller = getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = packageInstaller.createSession(sessionParams);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
writeApk(session, "StagedInstallTestAppSamePackageNameAsApex.apk");
session.commit(LocalIntentSender.getIntentSender());
final String errorMessage = extractErrorMessage(LocalIntentSender.getIntentSenderResult());
assertThat(errorMessage).contains("is an APEX package and can't be installed as an APK");
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
}
@Test
public void testInstallV3Apex_Commit() throws Exception {
int sessionId = stageSingleApk(
"com.android.apex.cts.shim.v3.apex").assertSuccessful().getSessionId();
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
}
@Test
public void testInstallV3Apex_VerifyPostReboot() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeNotRequested_Fails_Commit()
throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
int sessionId = stageSingleApk(
"com.android.apex.cts.shim.v2.apex").assertSuccessful().getSessionId();
PackageInstaller.SessionInfo sessionInfo = waitForBroadcast(sessionId);
assertThat(sessionInfo).isStagedSessionFailed();
// Also verify that correct session info is reported by PackageManager.
assertSessionFailed(sessionId);
storeSessionId(sessionId);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeNotRequested_Fails_VerifyPostReboot()
throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionFailed(sessionId);
// INSTALL_REQUEST_DOWNGRADE wasn't set, so apex shouldn't be downgraded.
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeRequested_DebugBuild_Commit()
throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
int sessionId = stageDowngradeSingleApk(
"com.android.apex.cts.shim.v2.apex").assertSuccessful().getSessionId();
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
storeSessionId(sessionId);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeRequested_DebugBuild_VerifyPostReboot()
throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionApplied(sessionId);
// Apex should be downgraded.
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(2);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeRequested_UserBuild_Fails_Commit()
throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
int sessionId = stageDowngradeSingleApk(
"com.android.apex.cts.shim.v2.apex").assertSuccessful().getSessionId();
PackageInstaller.SessionInfo sessionInfo = waitForBroadcast(sessionId);
assertThat(sessionInfo).isStagedSessionFailed();
// Also verify that correct session info is reported by PackageManager.
assertSessionFailed(sessionId);
storeSessionId(sessionId);
}
@Test
public void testStagedInstallDowngradeApex_DowngradeRequested_UserBuild_Fails_VerifyPostReboot()
throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionFailed(sessionId);
// Apex shouldn't be downgraded.
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(3);
}
@Test
public void testInstallApex_DeviceDoesNotSupportApex_Fails() throws Exception {
try {
stageSingleApk("com.android.apex.cts.shim.v2.apex");
fail("IllegalArgumentException expected");
} catch (IllegalArgumentException expected) {
assertThat(expected.getMessage()).contains(
"This device doesn't support the installation of APEX files");
}
}
@Test
public void testFailsInvalidApexInstall_Commit() throws Exception {
assertThat(getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
int sessionId = stageSingleApk(
"com.android.apex.cts.shim.v2_wrong_sha.apex").assertSuccessful()
.getSessionId();
waitForIsFailedBroadcast(sessionId);
assertSessionFailed(sessionId);
storeSessionId(sessionId);
}
@Test
public void testFailsInvalidApexInstall_AbandonSessionIsNoop() throws Exception {
int sessionId = retrieveLastSessionId();
assertSessionFailed(sessionId);
// Session is in a final state. Test that abandoning the session doesn't remove it from the
// session database.
getPackageInstaller().abandonSession(sessionId);
assertSessionFailed(sessionId);
}
@Test
public void testStagedApkSessionCallbacks() throws Exception {
List<Integer> created = new ArrayList<Integer>();
List<Integer> finished = new ArrayList<Integer>();
HandlerThread handlerThread = new HandlerThread(
"StagedApkSessionCallbacksTestHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
PackageInstaller.SessionCallback callback = new PackageInstaller.SessionCallback() {
@Override
public void onCreated(int sessionId) {
synchronized (created) {
created.add(sessionId);
}
}
@Override public void onBadgingChanged(int sessionId) { }
@Override public void onActiveChanged(int sessionId, boolean active) { }
@Override public void onProgressChanged(int sessionId, float progress) { }
@Override
public void onFinished(int sessionId, boolean success) {
synchronized (finished) {
finished.add(sessionId);
}
}
};
Context context = InstrumentationRegistry.getInstrumentation().getContext();
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
packageInstaller.registerSessionCallback(callback, handler);
int sessionId = stageSingleApk(
"StagedInstallTestAppAv1.apk").assertSuccessful().getSessionId();
assertThat(getInstalledVersion(TEST_APP_A)).isEqualTo(-1);
waitForIsReadyBroadcast(sessionId);
assertSessionReady(sessionId);
packageInstaller.unregisterSessionCallback(callback);
handlerThread.quitSafely();
handlerThread.join();
synchronized (created) {
assertThat(created).containsExactly(sessionId);
}
synchronized (finished) {
assertThat(finished).containsExactly(sessionId);
}
packageInstaller.abandonSession(sessionId);
}
private static PackageInstaller getPackageInstaller() {
return InstrumentationRegistry.getInstrumentation().getContext().getPackageManager()
.getPackageInstaller();
}
private static long getInstalledVersion(String packageName) {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
PackageManager pm = context.getPackageManager();
try {
PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
return info.getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
return -1;
}
}
// It becomes harder to maintain this variety of install-related helper methods.
// TODO(ioffe): refactor install-related helper methods into a separate utility.
private static int createStagedSession() throws Exception {
return createStagedSession(getPackageInstaller(), false, false, false);
}
private static int createStagedSession(
PackageInstaller packageInstaller,
boolean multiPackage, boolean isDowngrade, boolean isApexSession) throws Exception {
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
if (multiPackage) {
sessionParams.setMultiPackage();
}
sessionParams.setStaged();
sessionParams.setRequestDowngrade(isDowngrade);
if (isApexSession) {
sessionParams.setInstallAsApex();
}
return packageInstaller.createSession(sessionParams);
}
private static StageSessionResult stageDowngradeSingleApk(String apkFileName) throws Exception {
Log.i(TAG, "Staging a downgrade of " + apkFileName);
PackageInstaller packageInstaller = getPackageInstaller();
Pair<Integer, PackageInstaller.Session> sessionPair =
prepareSingleApkStagedSession(packageInstaller, apkFileName, true);
// Commit the session (this will start the installation workflow).
Log.i(TAG, "Committing downgrade session for apk: " + apkFileName);
sessionPair.second.commit(LocalIntentSender.getIntentSender());
return new StageSessionResult(sessionPair.first, LocalIntentSender.getIntentSenderResult());
}
private static StageSessionResult stageSingleApk(String apkFileName) throws Exception {
Log.i(TAG, "Staging an install of " + apkFileName);
PackageInstaller packageInstaller = getPackageInstaller();
Pair<Integer, PackageInstaller.Session> sessionPair =
prepareSingleApkStagedSession(packageInstaller, apkFileName, false);
// Commit the session (this will start the installation workflow).
Log.i(TAG, "Committing session for apk: " + apkFileName);
sessionPair.second.commit(LocalIntentSender.getIntentSender());
return new StageSessionResult(sessionPair.first, LocalIntentSender.getIntentSenderResult());
}
private static Pair<Integer, PackageInstaller.Session>
prepareSingleApkStagedSession(PackageInstaller packageInstaller, String apkFileName,
boolean isDowngrade)
throws Exception {
int sessionId = createStagedSession(packageInstaller, false, isDowngrade,
apkFileName.endsWith(".apex"));
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
writeApk(session, apkFileName);
return new Pair<>(sessionId, session);
}
private static StageSessionResult stageMultipleApks(String... apkFileNames) throws Exception {
Log.i(TAG, "Staging an install of " + Arrays.toString(apkFileNames));
PackageInstaller packageInstaller = getPackageInstaller();
int multiPackageSessionId = createStagedSession(packageInstaller, true, false, false);
PackageInstaller.Session multiPackageSession = packageInstaller.openSession(
multiPackageSessionId);
for (String apkFileName : apkFileNames) {
Pair<Integer, PackageInstaller.Session> sessionPair =
prepareSingleApkStagedSession(packageInstaller, apkFileName, false);
multiPackageSession.addChildSessionId(sessionPair.first);
}
multiPackageSession.commit(LocalIntentSender.getIntentSender());
return new StageSessionResult(
multiPackageSessionId, LocalIntentSender.getIntentSenderResult());
}
private static void assertSessionApplied(int sessionId) {
assertSessionState(sessionId,
(session) -> assertThat(session).isStagedSessionApplied());
}
private static void assertSessionReady(int sessionId) {
assertSessionState(sessionId,
(session) -> assertThat(session).isStagedSessionReady());
}
private static void assertSessionFailed(int sessionId) {
assertSessionState(sessionId,
(session) -> assertThat(session).isStagedSessionFailed());
}
private static void assertSessionState(
int sessionId, Consumer<PackageInstaller.SessionInfo> assertion) {
PackageInstaller packageInstaller = getPackageInstaller();
List<PackageInstaller.SessionInfo> sessions = packageInstaller.getStagedSessions();
boolean found = false;
for (PackageInstaller.SessionInfo session : sessions) {
if (session.getSessionId() == sessionId) {
assertion.accept(session);
found = true;
}
}
assertWithMessage("Expecting to find session in getStagedSession()")
.that(found).isTrue();
// Test also that getSessionInfo correctly returns the session.
PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
assertion.accept(sessionInfo);
}
private void storeSessionId(int sessionId) throws Exception {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(mTestStateFile))) {
writer.write("" + sessionId);
}
}
private int retrieveLastSessionId() throws Exception {
try (BufferedReader reader = new BufferedReader(new FileReader(mTestStateFile))) {
return Integer.parseInt(reader.readLine());
}
}
private static void installNonStaged(String apkFileName) throws Exception {
PackageInstaller packageInstaller = getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = packageInstaller.createSession(sessionParams);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
writeApk(session, apkFileName);
session.commit(LocalIntentSender.getIntentSender());
assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
}
private static void writeApk(PackageInstaller.Session session, String apkFileName)
throws Exception {
try (OutputStream packageInSession = session.openWrite(apkFileName, 0, -1);
InputStream is =
StagedInstallTest.class.getClassLoader().getResourceAsStream(apkFileName)) {
byte[] buffer = new byte[4096];
int n;
while ((n = is.read(buffer)) >= 0) {
packageInSession.write(buffer, 0, n);
}
}
}
// TODO(ioffe): not really-tailored to staged install, rename to InstallResult?
private static final class StageSessionResult {
private final int sessionId;
private final Intent result;
private StageSessionResult(int sessionId, Intent result) {
this.sessionId = sessionId;
this.result = result;
}
public int getSessionId() {
return sessionId;
}
public String getErrorMessage() {
return extractErrorMessage(result);
}
public StageSessionResult assertSuccessful() {
assertStatusSuccess(result);
return this;
}
}
private static String extractErrorMessage(Intent result) {
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status == -1) {
throw new AssertionError("PENDING USER ACTION");
}
if (status == 0) {
throw new AssertionError("Result was successful");
}
return result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
}
private static void abandonSession(int sessionId) {
getPackageInstaller().abandonSession(sessionId);
}
/**
* Returns the session by session Id, or null if no session is found.
*/
private static PackageInstaller.SessionInfo getStagedSessionInfo(int sessionId) {
PackageInstaller packageInstaller = getPackageInstaller();
for (PackageInstaller.SessionInfo session : packageInstaller.getStagedSessions()) {
if (session.getSessionId() == sessionId) {
return session;
}
}
return null;
}
private static PackageInstaller.SessionInfo getSessionInfo(int sessionId) {
return getPackageInstaller().getSessionInfo(sessionId);
}
private static void uninstall(String packageName) throws Exception {
// No need to uninstall if the package isn't installed.
if (getInstalledVersion(packageName) == -1) {
return;
}
Context context = InstrumentationRegistry.getInstrumentation().getContext();
PackageManager packageManager = context.getPackageManager();
PackageInstaller packageInstaller = packageManager.getPackageInstaller();
packageInstaller.uninstall(packageName, LocalIntentSender.getIntentSender());
assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
}
private static void assertStatusSuccess(Intent result) {
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status == -1) {
throw new AssertionError("PENDING USER ACTION");
} else if (status > 0) {
String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
}
}
private void waitForIsFailedBroadcast(int sessionId) {
Log.i(TAG, "Waiting for session " + sessionId + " to be marked as failed");
try {
PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
assertThat(info).isStagedSessionFailed();
} catch (Exception e) {
throw new AssertionError(e);
}
}
private void waitForIsReadyBroadcast(int sessionId) {
Log.i(TAG, "Waiting for session " + sessionId + " to be ready");
try {
PackageInstaller.SessionInfo info = waitForBroadcast(sessionId);
assertThat(info).isStagedSessionReady();
} catch (Exception e) {
throw new AssertionError(e);
}
}
private PackageInstaller.SessionInfo waitForBroadcast(int sessionId) throws Exception {
PackageInstaller.SessionInfo info =
SessionUpdateBroadcastReceiver.sessionBroadcasts.poll(60, TimeUnit.SECONDS);
assertThat(info).isNotNull();
assertThat(info.getSessionId()).isEqualTo(sessionId);
return info;
}
}