blob: 7c53349202bbd88a262d3d6a650aabb44a7d9ab4 [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.atomicinstall;
import static com.android.cts.install.lib.InstallUtils.assertStatusSuccess;
import static com.android.cts.install.lib.InstallUtils.getInstalledVersion;
import static com.android.cts.install.lib.InstallUtils.openPackageInstallerSession;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.Manifest;
import android.content.pm.PackageInstaller;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.SystemUtil;
import com.android.cts.install.lib.Install;
import com.android.cts.install.lib.InstallUtils;
import com.android.cts.install.lib.LocalIntentSender;
import com.android.cts.install.lib.TestApp;
import com.android.cts.install.lib.Uninstall;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* Tests for multi-package (a.k.a. atomic) installs.
*/
@RunWith(JUnit4.class)
public class AtomicInstallTest {
/**
* Time between repeated checks in {@link #retry}.
*/
private static final long RETRY_CHECK_INTERVAL_MILLIS = 500;
/**
* Maximum number of checks in {@link #retry} before a timeout occurs.
*/
private static final long RETRY_MAX_INTERVALS = 20;
public static final String TEST_APP_CORRUPT_FILENAME = "corrupt.apk";
private static final TestApp CORRUPT_TESTAPP = new TestApp(
"corrupt", "com.corrupt", 1, false, TEST_APP_CORRUPT_FILENAME);
private void adoptShellPermissions() {
InstrumentationRegistry
.getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES);
}
@Before
public void setup() throws Exception {
adoptShellPermissions();
Uninstall.packages(TestApp.A, TestApp.B, TestApp.C);
}
@After
public void dropShellPermissions() {
InstrumentationRegistry
.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
}
/**
* Cleans up sessions that are not committed during tests.
*/
@After
public void cleanUpSessions() {
InstallUtils.getPackageInstaller().getMySessions().forEach(info -> {
try {
InstallUtils.getPackageInstaller().abandonSession(info.getSessionId());
} catch (Exception ignore) {
}
});
}
private static <T> T retry(Supplier<T> supplier, Predicate<T> predicate, String message)
throws InterruptedException {
for (int i = 0; i < RETRY_MAX_INTERVALS; i++) {
T result = supplier.get();
if (predicate.test(result)) {
return result;
}
Thread.sleep(RETRY_CHECK_INTERVAL_MILLIS);
}
throw new AssertionError(message);
}
/**
* Tests a completed session should be cleaned up.
*/
@Test
public void testSessionCleanUp_Single() throws Exception {
int sessionId = Install.single(TestApp.A1).commit();
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
// The session is cleaned up asynchronously after install completed.
// Retry until the session no longer exists.
retry(() -> InstallUtils.getPackageInstaller().getSessionInfo(sessionId),
info -> info == null,
"Session " + sessionId + " not cleaned up");
}
/**
* Tests a completed session should be cleaned up.
*/
@Test
public void testSessionCleanUp_Multi() throws Exception {
int sessionId = Install.multi(TestApp.A1, TestApp.B1).commit();
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
// The session is cleaned up asynchronously after install completed.
// Retry until the session no longer exists.
retry(() -> InstallUtils.getPackageInstaller().getSessionInfo(sessionId),
info -> info == null,
"Session " + sessionId + " not cleaned up");
}
@Test
public void testInstallTwoApks() throws Exception {
Install.multi(TestApp.A1, TestApp.B1).commit();
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
}
/**
* Tests a removed child shouldn't be installed.
*/
@Test
public void testRemoveChild() throws Exception {
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
assertThat(getInstalledVersion(TestApp.C)).isEqualTo(-1);
int parentId = Install.multi(TestApp.A1).createSession();
int childBId = Install.single(TestApp.B1).createSession();
int childCId = Install.single(TestApp.C1).createSession();
try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
parent.addChildSessionId(childBId);
parent.addChildSessionId(childCId);
parent.removeChildSessionId(childBId);
LocalIntentSender sender = new LocalIntentSender();
parent.commit(sender.getIntentSender());
InstallUtils.assertStatusSuccess(sender.getResult());
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(1);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
assertThat(getInstalledVersion(TestApp.C)).isEqualTo(1);
}
}
@Test
public void testInstallTwoApksDowngradeFail() throws Exception {
Install.multi(TestApp.A2, TestApp.B1).commit();
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(2);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
InstallUtils.commitExpectingFailure(AssertionError.class,
"INSTALL_FAILED_VERSION_DOWNGRADE", Install.multi(TestApp.A1, TestApp.B1));
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(2);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(1);
}
@Test
public void testFailInconsistentMultiPackageCommit() throws Exception {
// Test inconsistency in staged settings
Install parentStaged = Install.multi(Install.single(TestApp.A1)).setStaged();
Install childStaged = Install.multi(Install.single(TestApp.A1).setStaged());
assertInconsistentStagedSettings(parentStaged);
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
assertInconsistentStagedSettings(childStaged);
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
// Test inconsistency in rollback settings
Install parentEnabledRollback = Install.multi(Install.single(TestApp.A1))
.setEnableRollback();
Install childEnabledRollback = Install.multi(
Install.single(TestApp.A1).setEnableRollback());
assertInconsistentRollbackSettings(parentEnabledRollback);
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
assertInconsistentRollbackSettings(childEnabledRollback);
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
}
@Test
public void testChildFailurePropagated() throws Exception {
// Create a child session that "inherits" from a non-existent package. This
// causes the session commit to fail with a PackageManagerException.
Install childInstall = Install.single(TestApp.A1).setSessionMode(
PackageInstaller.SessionParams.MODE_INHERIT_EXISTING);
Install parentInstall = Install.multi(childInstall);
InstallUtils.commitExpectingFailure(AssertionError.class, "Missing existing base package",
parentInstall);
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
}
@Test
public void testEarlyFailureFailsAll() throws Exception {
InstallUtils.commitExpectingFailure(AssertionError.class, "Failed to parse",
Install.multi(TestApp.A1, TestApp.B1, CORRUPT_TESTAPP));
assertThat(getInstalledVersion(TestApp.A)).isEqualTo(-1);
assertThat(getInstalledVersion(TestApp.B)).isEqualTo(-1);
}
@Test
public void testInvalidStateScenario_MultiSessionCantBeApex() throws Exception {
try {
SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check true");
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
params.setMultiPackage();
params.setInstallAsApex();
params.setStaged();
try {
InstallUtils.getPackageInstaller().createSession(params);
fail("Should not be able to create a multi-session set as APEX!");
} catch (Exception ignore) {
}
} finally {
SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check false");
}
}
/**
* Tests a single-session can't have child.
*/
@Test
public void testInvalidStateScenario_AddChildToSingleSessionShouldFail() throws Exception {
int parentId = Install.single(TestApp.A1).createSession();
int childId = Install.single(TestApp.B1).createSession();
try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
try {
parent.addChildSessionId(childId);
fail("Should not be able to add a child session to a single-session!");
} catch (Exception ignore) {
}
}
}
/**
* Tests a multi-session can't be a child.
*/
@Test
public void testInvalidStateScenario_MultiSessionAddedAsChildShouldFail() throws Exception {
int parentId = Install.multi(TestApp.A1).createSession();
int childId = Install.multi(TestApp.B1).createSession();
try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
try {
parent.addChildSessionId(childId);
fail("Should not be able to add a multi-session as a child!");
} catch (Exception ignore) {
}
}
}
/**
* Tests a committed session can't add child.
*/
@Test
public void testInvalidStateScenario_AddChildToCommittedSessionShouldFail() throws Exception {
int parentId = Install.multi(TestApp.A1).createSession();
int childId = Install.single(TestApp.B1).createSession();
try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
LocalIntentSender sender = new LocalIntentSender();
parent.commit(sender.getIntentSender());
try {
parent.addChildSessionId(childId);
fail("Should not be able to add child to a committed session");
} catch (Exception ignore) {
}
}
}
/**
* Tests a committed session can't remove child.
*/
@Test
public void testInvalidStateScenario_RemoveChildFromCommittedSessionShouldFail()
throws Exception {
int parentId = Install.multi(TestApp.A1).createSession();
int childId = Install.single(TestApp.B1).createSession();
try (PackageInstaller.Session parent = openPackageInstallerSession(parentId)) {
parent.addChildSessionId(childId);
LocalIntentSender sender = new LocalIntentSender();
parent.commit(sender.getIntentSender());
try {
parent.removeChildSessionId(childId);
fail("Should not be able to remove child from a committed session");
} catch (Exception ignore) {
}
}
}
/**
* Tests removing a child that is not its own should do nothing.
*/
@Test
public void testInvalidStateScenario_RemoveWrongChildShouldDoNothing() throws Exception {
int parent1Id = Install.multi(TestApp.A1).createSession();
int parent2Id = Install.multi(TestApp.C1).createSession();
int childId = Install.single(TestApp.B1).createSession();
try (PackageInstaller.Session parent1 = openPackageInstallerSession(parent1Id);
PackageInstaller.Session parent2 = openPackageInstallerSession(parent2Id);) {
parent1.addChildSessionId(childId);
// Should do nothing since the child doesn't belong to parent2
parent2.removeChildSessionId(childId);
int currentParentId =
InstallUtils.getPackageInstaller().getSessionInfo(childId).getParentSessionId();
// Check this child still belongs to parent1
assertThat(currentParentId).isEqualTo(parent1Id);
assertThat(parent1.getChildSessionIds()).asList().contains(childId);
assertThat(parent2.getChildSessionIds()).asList().doesNotContain(childId);
}
}
@Test
public void testInvalidStateScenarios() throws Exception {
int parentSessionId = Install.multi(TestApp.A1, TestApp.B1).createSession();
try (PackageInstaller.Session parentSession =
openPackageInstallerSession(parentSessionId)) {
for (int childSessionId : parentSession.getChildSessionIds()) {
try (PackageInstaller.Session childSession =
openPackageInstallerSession(childSessionId)) {
try {
LocalIntentSender sender = new LocalIntentSender();
childSession.commit(sender.getIntentSender());
fail("Should not be able to commit a child session!");
} catch (IllegalStateException e) {
// ignore
}
try {
childSession.abandon();
fail("Should not be able to abandon a child session!");
} catch (IllegalStateException e) {
// ignore
}
}
}
int toAbandonSessionId = Install.single(TestApp.A1).createSession();
try (PackageInstaller.Session toAbandonSession =
openPackageInstallerSession(toAbandonSessionId)) {
toAbandonSession.abandon();
try {
parentSession.addChildSessionId(toAbandonSessionId);
fail("Should not be able to add abandoned child session!");
} catch (RuntimeException e) {
// ignore
}
}
LocalIntentSender sender = new LocalIntentSender();
parentSession.commit(sender.getIntentSender());
assertStatusSuccess(sender.getResult());
}
}
private static void assertInconsistentStagedSettings(Install install) {
assertInconsistentSettings("inconsistent staged settings", install);
}
private static void assertInconsistentRollbackSettings(Install install) {
assertInconsistentSettings("inconsistent rollback settings", install);
}
private static void assertInconsistentSettings(String failMessage, Install install) {
InstallUtils.commitExpectingFailure(IllegalStateException.class, failMessage, install);
}
}