blob: f07ae9f65b1b0ec2744a50ac24019f1ecec198ec [file] [log] [blame]
/*
* Copyright (C) 2018 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.rollback;
import android.Manifest;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.VersionedPackage;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.RollbackInfo;
import android.content.rollback.RollbackManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.test.InstrumentationRegistry;
import android.util.Log;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* Test system Rollback APIs.
* TODO: Should this be a cts test instead? Where should it live?
*/
@RunWith(JUnit4.class)
public class RollbackTest {
private static final String TAG = "RollbackTest";
private static final String TEST_APP_A = "com.android.tests.rollback.testapp.A";
private static final String TEST_APP_B = "com.android.tests.rollback.testapp.B";
private static final String INSTRUMENTED_APP = "com.android.tests.rollback";
/**
* Test basic rollbacks.
*/
@Test
public void testBasic() throws Exception {
// Make sure an app can't listen to or disturb the internal
// ACTION_PACKAGE_ENABLE_ROLLBACK broadcast.
Context context = InstrumentationRegistry.getContext();
IntentFilter enableRollbackFilter = new IntentFilter();
enableRollbackFilter.addAction("android.intent.action.PACKAGE_ENABLE_ROLLBACK");
enableRollbackFilter.addDataType("application/vnd.android.package-archive");
enableRollbackFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
BroadcastReceiver enableRollbackReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
abortBroadcast();
}
};
context.registerReceiver(enableRollbackReceiver, enableRollbackFilter);
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
// Register a broadcast receiver for notification when the
// rollback has been committed.
RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver();
RollbackManager rm = RollbackTestUtils.getRollbackManager();
// Uninstall TEST_APP_A
RollbackTestUtils.uninstall(TEST_APP_A);
assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// TODO: There is currently a race condition between when the app is
// uninstalled and when rollback manager deletes the rollback. Fix it
// so that's not the case!
for (int i = 0; i < 5; ++i) {
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A);
if (rollback != null) {
Log.i(TAG, "Sleeping 1 second to wait for uninstall to take effect.");
Thread.sleep(1000);
}
}
// The app should not be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is uninstalled and when the previously
// available rollback, if any, is removed.
Thread.sleep(1000);
assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A));
// There should be no recently committed rollbacks for this package.
assertNull(getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A));
// Install v1 of the app (without rollbacks enabled).
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// Upgrade from v1 to v2, with rollbacks enabled.
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback);
// We should not have received any rollback requests yet.
// TODO: Possibly flaky if, by chance, some other app on device
// happens to be rolled back at the same time?
assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS));
// Roll back the app.
RollbackTestUtils.rollback(rollback.getRollbackId());
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// Verify we received a broadcast for the rollback.
// TODO: Race condition between the timeout and when the broadcast is
// received could lead to test flakiness.
Intent broadcast = broadcastReceiver.poll(5, TimeUnit.SECONDS);
assertNotNull(broadcast);
assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS));
// Verify the recent rollback has been recorded.
rollback = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback);
broadcastReceiver.unregister();
context.unregisterReceiver(enableRollbackReceiver);
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test that multiple available rollbacks are properly persisted.
*/
@Test
public void testAvailableRollbackPersistence() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
RollbackTestUtils.uninstall(TEST_APP_B);
RollbackTestUtils.install("RollbackTestAppBv1.apk", false);
RollbackTestUtils.install("RollbackTestAppBv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// Both test apps should now be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA);
RollbackInfo rollbackB = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB);
// Reload the persisted data.
rm.reloadPersistedData();
// The apps should still be available for rollback.
rollbackA = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA);
rollbackB = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB);
// Rollback of B should not rollback A
RollbackTestUtils.rollback(rollbackB.getRollbackId());
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test that available multi-package rollbacks are properly persisted.
*/
@Test
public void testAvailableMultiPackageRollbackPersistence() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.uninstall(TEST_APP_B);
RollbackTestUtils.installMultiPackage(false,
"RollbackTestAppAv1.apk",
"RollbackTestAppBv1.apk");
RollbackTestUtils.installMultiPackage(true,
"RollbackTestAppAv2.apk",
"RollbackTestAppBv2.apk");
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// The app should now be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoForAandB(rollbackA);
RollbackInfo rollbackB = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoForAandB(rollbackB);
// Reload the persisted data.
rm.reloadPersistedData();
// The apps should still be available for rollback.
rollbackA = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoForAandB(rollbackA);
rollbackB = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoForAandB(rollbackB);
// Rollback of B should rollback A as well
RollbackTestUtils.rollback(rollbackB.getRollbackId());
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test that recently committed rollback data is properly persisted.
*/
@Test
public void testRecentlyCommittedRollbackPersistence() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
// Roll back the app.
VersionedPackage cause = new VersionedPackage(
"com.android.tests.rollback.testapp.Foo", 42);
RollbackTestUtils.rollback(rollback.getRollbackId(), cause);
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// Verify the recent rollback has been recorded.
rollback = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback, cause);
// Reload the persisted data.
rm.reloadPersistedData();
// Verify the recent rollback is still recorded.
rollback = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback, cause);
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test explicit expiration of rollbacks.
* Does not test the scheduling aspects of rollback expiration.
*/
@Test
public void testRollbackExpiration() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback);
// Expire the rollback.
rm.expireRollbackForPackage(TEST_APP_A);
// The rollback should no longer be available.
assertNull(getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A));
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
private static final String NO_RESPONSE = "NO RESPONSE";
// Calls into the test app to process user data.
// Asserts if the user data could not be processed or was version
// incompatible with the previously processed user data.
private void processUserData(String packageName) throws Exception {
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName,
"com.android.tests.rollback.testapp.ProcessUserData"));
Context context = InstrumentationRegistry.getContext();
HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
handlerThread.start();
// It can sometimes take a while after rollback before the app will
// receive this broadcast, so try a few times in a loop.
String result = NO_RESPONSE;
for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) {
BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>();
context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getResultCode() == 1) {
resultQueue.add("OK");
} else {
// If the test app doesn't receive the broadcast or
// fails to set the result data, then getResultData
// here returns the initial NO_RESPONSE data passed to
// the sendOrderedBroadcast call.
resultQueue.add(getResultData());
}
}
}, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null);
result = resultQueue.poll(10, TimeUnit.SECONDS);
if (result == null) {
result = "ProcessUserData broadcast timed out";
}
}
handlerThread.quit();
if (!"OK".equals(result)) {
fail(result);
}
}
/**
* Test that app user data is rolled back.
*/
@Test
public void testUserDataRollback() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
processUserData(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
processUserData(TEST_APP_A);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
RollbackTestUtils.rollback(rollback.getRollbackId());
processUserData(TEST_APP_A);
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test restrictions on rollback broadcast sender.
* A random app should not be able to send a ROLLBACK_COMMITTED broadcast.
*/
@Test
public void testRollbackBroadcastRestrictions() throws Exception {
RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver();
Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED);
try {
InstrumentationRegistry.getContext().sendBroadcast(broadcast);
fail("Succeeded in sending restricted broadcast from app context.");
} catch (SecurityException se) {
// Expected behavior.
}
// Confirm that we really haven't received the broadcast.
// TODO: How long to wait for the expected timeout?
assertNull(broadcastReceiver.poll(5, TimeUnit.SECONDS));
// TODO: Do we need to do this? Do we need to ensure this is always
// called, even when the test fails?
broadcastReceiver.unregister();
}
/**
* Regression test for rollback in the case when multiple apps are
* available for rollback at the same time.
*/
@Test
public void testMultipleRollbackAvailable() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
// Prep installation of the test apps.
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
RollbackTestUtils.uninstall(TEST_APP_B);
RollbackTestUtils.install("RollbackTestAppBv1.apk", false);
RollbackTestUtils.install("RollbackTestAppBv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// Both test apps should now be available for rollback, and the
// RollbackInfo returned for the rollbacks should be correct.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA);
RollbackInfo rollbackB = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB);
// Executing rollback should roll back the correct package.
RollbackTestUtils.rollback(rollbackA.getRollbackId());
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppAv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
RollbackTestUtils.rollback(rollbackB.getRollbackId());
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
/**
* Test that the MANAGE_ROLLBACKS permission is required to call
* RollbackManager APIs.
*/
@Test
public void testManageRollbacksPermission() throws Exception {
// We shouldn't be allowed to call any of the RollbackManager APIs
// without the MANAGE_ROLLBACKS permission.
RollbackManager rm = RollbackTestUtils.getRollbackManager();
try {
rm.getAvailableRollbacks();
fail("expected SecurityException");
} catch (SecurityException e) {
// Expected.
}
try {
rm.getRecentlyCommittedRollbacks();
fail("expected SecurityException");
} catch (SecurityException e) {
// Expected.
}
try {
// TODO: What if the implementation checks arguments for non-null
// first? Then this test isn't valid.
rm.commitRollback(0, Collections.emptyList(), null);
fail("expected SecurityException");
} catch (SecurityException e) {
// Expected.
}
try {
rm.reloadPersistedData();
fail("expected SecurityException");
} catch (SecurityException e) {
// Expected.
}
try {
rm.expireRollbackForPackage(TEST_APP_A);
fail("expected SecurityException");
} catch (SecurityException e) {
// Expected.
}
}
/**
* Test rollback of multi-package installs is implemented.
*/
@Test
public void testMultiPackage() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
// Prep installation of the test apps.
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.uninstall(TEST_APP_B);
RollbackTestUtils.installMultiPackage(false,
"RollbackTestAppAv1.apk",
"RollbackTestAppBv1.apk");
RollbackTestUtils.installMultiPackage(true,
"RollbackTestAppAv2.apk",
"RollbackTestAppBv2.apk");
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// TEST_APP_A should now be available for rollback.
RollbackInfo rollback = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoForAandB(rollback);
// Rollback the app. It should cause both test apps to be rolled
// back.
RollbackTestUtils.rollback(rollback.getRollbackId());
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// We should see recent rollbacks listed for both A and B.
Thread.sleep(1000);
RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_A);
RollbackInfo rollbackB = getUniqueRollbackInfoForPackage(
rm.getRecentlyCommittedRollbacks(), TEST_APP_B);
assertRollbackInfoForAandB(rollbackB);
assertEquals(rollbackA.getRollbackId(), rollbackB.getRollbackId());
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
}
}
// Helper function to test the value of a PackageRollbackInfo
private void assertPackageRollbackInfoEquals(String packageName,
long versionRolledBackFrom, long versionRolledBackTo,
PackageRollbackInfo info) {
assertEquals(packageName, info.getPackageName());
assertEquals(packageName, info.getVersionRolledBackFrom().getPackageName());
assertEquals(versionRolledBackFrom, info.getVersionRolledBackFrom().getLongVersionCode());
assertEquals(packageName, info.getVersionRolledBackTo().getPackageName());
assertEquals(versionRolledBackTo, info.getVersionRolledBackTo().getLongVersionCode());
}
/**
* Test bad update automatic rollback.
*/
@Test
public void testBadUpdateRollback() throws Exception {
BroadcastReceiver crashCountReceiver = null;
Context context = InstrumentationRegistry.getContext();
try {
RollbackTestUtils.adoptShellPermissionIdentity(
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.DELETE_PACKAGES,
Manifest.permission.MANAGE_ROLLBACKS,
Manifest.permission.KILL_BACKGROUND_PROCESSES,
Manifest.permission.RESTART_PACKAGES);
RollbackManager rm = RollbackTestUtils.getRollbackManager();
// Prep installation of the test apps.
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
RollbackTestUtils.install("RollbackTestAppACrashingV2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
RollbackTestUtils.uninstall(TEST_APP_B);
RollbackTestUtils.install("RollbackTestAppBv1.apk", false);
RollbackTestUtils.install("RollbackTestAppBv2.apk", true);
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
// Both test apps should now be available for rollback, and the
// targetPackage returned for rollback should be correct.
// TODO: See if there is a way to remove this race condition
// between when the app is installed and when the rollback
// is made available.
Thread.sleep(1000);
RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_A);
assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA);
RollbackInfo rollbackB = getUniqueRollbackInfoForPackage(
rm.getAvailableRollbacks(), TEST_APP_B);
assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB);
BlockingQueue<Integer> crashQueue = new SynchronousQueue<>();
IntentFilter crashCountFilter = new IntentFilter();
crashCountFilter.addAction("com.android.tests.rollback.CRASH");
crashCountFilter.addCategory(Intent.CATEGORY_DEFAULT);
crashCountReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
try {
// Sleep long enough for packagewatchdog to be notified of crash
Thread.sleep(1000);
// Kill app and close AppErrorDialog
ActivityManager am = context.getSystemService(ActivityManager.class);
am.killBackgroundProcesses(TEST_APP_A);
// Allow another package launch
crashQueue.put(intent.getIntExtra("count", 0));
} catch (InterruptedException e) {
fail("Failed to communicate with test app");
}
}
};
context.registerReceiver(crashCountReceiver, crashCountFilter);
// Start apps PackageWatchdog#TRIGGER_FAILURE_COUNT times so TEST_APP_A crashes
do {
RollbackTestUtils.launchPackage(TEST_APP_A);
} while(crashQueue.take() < 5);
// TEST_APP_A is automatically rolled back by the RollbackPackageHealthObserver
assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// Instrumented app is still the package installer
String installer = context.getPackageManager().getInstallerPackageName(TEST_APP_A);
assertEquals(INSTRUMENTED_APP, installer);
// TEST_APP_B is untouched
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B));
} finally {
RollbackTestUtils.dropShellPermissionIdentity();
if (crashCountReceiver != null) {
context.unregisterReceiver(crashCountReceiver);
}
}
}
// Helper function to test the value of a RollbackInfo with single package
private void assertRollbackInfoEquals(String packageName,
long versionRolledBackFrom, long versionRolledBackTo,
RollbackInfo info, VersionedPackage... causePackages) {
assertNotNull(info);
assertEquals(1, info.getPackages().size());
assertPackageRollbackInfoEquals(packageName, versionRolledBackFrom, versionRolledBackTo,
info.getPackages().get(0));
assertEquals(causePackages.length, info.getCausePackages().size());
for (int i = 0; i < causePackages.length; ++i) {
assertEquals(causePackages[i].getPackageName(),
info.getCausePackages().get(i).getPackageName());
assertEquals(causePackages[i].getLongVersionCode(),
info.getCausePackages().get(i).getLongVersionCode());
}
}
// Helper function to test that the given rollback info is a rollback for
// the atomic set {A2, B2} -> {A1, B1}.
private void assertRollbackInfoForAandB(RollbackInfo rollback) {
assertNotNull(rollback);
assertEquals(2, rollback.getPackages().size());
if (TEST_APP_A.equals(rollback.getPackages().get(0).getPackageName())) {
assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.getPackages().get(0));
assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollback.getPackages().get(1));
} else {
assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollback.getPackages().get(0));
assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.getPackages().get(1));
}
}
// Helper function to return the RollbackInfo with a given package in the
// list of rollbacks. Throws an assertion failure if there is more than
// one such rollback info. Returns null if there are no such rollback
// infos.
private RollbackInfo getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks,
String packageName) {
RollbackInfo found = null;
for (RollbackInfo rollback : rollbacks) {
for (PackageRollbackInfo info : rollback.getPackages()) {
if (packageName.equals(info.getPackageName())) {
assertNull(found);
found = rollback;
break;
}
}
}
return found;
}
}