/*
|
* 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.server.rollback;
|
|
import android.Manifest;
|
import android.annotation.NonNull;
|
import android.annotation.UserIdInt;
|
import android.app.AppOpsManager;
|
import android.content.BroadcastReceiver;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.IntentFilter;
|
import android.content.IntentSender;
|
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ModuleInfo;
|
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInstaller;
|
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManagerInternal;
|
import android.content.pm.PackageParser;
|
import android.content.pm.ParceledListSlice;
|
import android.content.pm.UserInfo;
|
import android.content.pm.VersionedPackage;
|
import android.content.rollback.IRollbackManager;
|
import android.content.rollback.PackageRollbackInfo;
|
import android.content.rollback.RollbackInfo;
|
import android.content.rollback.RollbackManager;
|
import android.os.Binder;
|
import android.os.Environment;
|
import android.os.Handler;
|
import android.os.HandlerThread;
|
import android.os.ParcelFileDescriptor;
|
import android.os.Process;
|
import android.os.UserHandle; // duped to avoid merge conflict
|
import android.os.UserManager; // out of order to avoid merge conflict
|
import android.os.SystemClock;
|
import android.os.UserHandle;
|
import android.provider.DeviceConfig;
|
import android.util.ArraySet;
|
import android.util.IntArray;
|
import android.util.Log;
|
import android.util.SparseBooleanArray;
|
import android.util.SparseLongArray;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.util.ArrayUtils;
|
import com.android.internal.util.DumpUtils;
|
import com.android.internal.util.IndentingPrintWriter;
|
import com.android.server.LocalServices;
|
import com.android.server.Watchdog;
|
import com.android.server.pm.Installer;
|
|
import java.io.File;
|
import java.io.FileDescriptor;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.security.SecureRandom;
|
import java.time.Instant;
|
import java.time.temporal.ChronoUnit;
|
import java.util.ArrayList;
|
import java.util.HashSet;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.Random;
|
import java.util.Set;
|
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* Implementation of service that manages APK level rollbacks.
|
*/
|
class RollbackManagerServiceImpl extends IRollbackManager.Stub {
|
|
private static final String TAG = "RollbackManager";
|
|
// Rollbacks expire after 14 days.
|
private static final long DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS =
|
TimeUnit.DAYS.toMillis(14);
|
|
// Lock used to synchronize accesses to in-memory rollback data
|
// structures. By convention, methods with the suffix "Locked" require
|
// mLock is held when they are called.
|
private final Object mLock = new Object();
|
|
// No need for guarding with lock because value is only accessed in handler thread
|
// and the value will be written on boot complete. Initialization here happens before
|
// handler threads are running so that's fine.
|
private long mRollbackLifetimeDurationInMillis = DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS;
|
|
private static final long HANDLER_THREAD_TIMEOUT_DURATION_MILLIS =
|
TimeUnit.MINUTES.toMillis(10);
|
|
// Used for generating rollback IDs.
|
private final Random mRandom = new SecureRandom();
|
|
// Set of allocated rollback ids
|
@GuardedBy("mLock")
|
private final SparseBooleanArray mAllocatedRollbackIds = new SparseBooleanArray();
|
|
// Package rollback data for rollbacks we are in the process of enabling.
|
@GuardedBy("mLock")
|
private final Set<NewRollback> mNewRollbacks = new ArraySet<>();
|
|
// The list of all rollbacks, including available and committed rollbacks.
|
// This list is null until the rollback data has been loaded.
|
@GuardedBy("mLock")
|
private List<RollbackData> mRollbacks;
|
|
private final RollbackStore mRollbackStore;
|
|
private final Context mContext;
|
private final HandlerThread mHandlerThread;
|
private final Installer mInstaller;
|
private final RollbackPackageHealthObserver mPackageHealthObserver;
|
private final AppDataRollbackHelper mAppDataRollbackHelper;
|
|
// This field stores the difference in Millis between the uptime (millis since device
|
// has booted) and current time (device wall clock) - it's used to update rollback data
|
// timestamps when the time is changed, by the user or by change of timezone.
|
// No need for guarding with lock because value is only accessed in handler thread.
|
private long mRelativeBootTime = calculateRelativeBootTime();
|
|
RollbackManagerServiceImpl(Context context) {
|
mContext = context;
|
// Note that we're calling onStart here because this object is only constructed on
|
// SystemService#onStart.
|
mInstaller = new Installer(mContext);
|
mInstaller.onStart();
|
mHandlerThread = new HandlerThread("RollbackManagerServiceHandler");
|
mHandlerThread.start();
|
|
// Monitor the handler thread
|
Watchdog.getInstance().addThread(getHandler(), HANDLER_THREAD_TIMEOUT_DURATION_MILLIS);
|
|
mRollbackStore = new RollbackStore(new File(Environment.getDataDirectory(), "rollback"));
|
|
mPackageHealthObserver = new RollbackPackageHealthObserver(mContext);
|
mAppDataRollbackHelper = new AppDataRollbackHelper(mInstaller);
|
|
// Kick off loading of the rollback data from strorage in a background
|
// thread.
|
// TODO: Consider loading the rollback data directly here instead, to
|
// avoid the need to call ensureRollbackDataLoaded every time before
|
// accessing the rollback data?
|
// TODO: Test that this kicks off initial scheduling of rollback
|
// expiration.
|
getHandler().post(() -> ensureRollbackDataLoaded());
|
|
// TODO: Make sure to register these call backs when a new user is
|
// added too.
|
SessionCallback sessionCallback = new SessionCallback();
|
for (UserInfo userInfo : UserManager.get(mContext).getUsers(true)) {
|
registerUserCallbacks(userInfo.getUserHandle());
|
}
|
|
IntentFilter enableRollbackFilter = new IntentFilter();
|
enableRollbackFilter.addAction(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK);
|
try {
|
enableRollbackFilter.addDataType("application/vnd.android.package-archive");
|
} catch (IntentFilter.MalformedMimeTypeException e) {
|
Log.e(TAG, "addDataType", e);
|
}
|
|
mContext.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
if (Intent.ACTION_PACKAGE_ENABLE_ROLLBACK.equals(intent.getAction())) {
|
int token = intent.getIntExtra(
|
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN, -1);
|
int installFlags = intent.getIntExtra(
|
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS, 0);
|
int[] installedUsers = intent.getIntArrayExtra(
|
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALLED_USERS);
|
int user = intent.getIntExtra(
|
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_USER, 0);
|
|
File newPackageCodePath = new File(intent.getData().getPath());
|
|
getHandler().post(() -> {
|
boolean success = enableRollback(installFlags, newPackageCodePath,
|
installedUsers, user, token);
|
int ret = PackageManagerInternal.ENABLE_ROLLBACK_SUCCEEDED;
|
if (!success) {
|
ret = PackageManagerInternal.ENABLE_ROLLBACK_FAILED;
|
}
|
|
PackageManagerInternal pm = LocalServices.getService(
|
PackageManagerInternal.class);
|
pm.setEnableRollbackCode(token, ret);
|
});
|
|
// We're handling the ordered broadcast. Abort the
|
// broadcast because there is no need for it to go to
|
// anyone else.
|
abortBroadcast();
|
}
|
}
|
}, enableRollbackFilter, null, getHandler());
|
|
IntentFilter enableRollbackTimedOutFilter = new IntentFilter();
|
enableRollbackTimedOutFilter.addAction(Intent.ACTION_CANCEL_ENABLE_ROLLBACK);
|
|
mContext.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
if (Intent.ACTION_CANCEL_ENABLE_ROLLBACK.equals(intent.getAction())) {
|
int token = intent.getIntExtra(
|
PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN, -1);
|
synchronized (mLock) {
|
for (NewRollback rollback : mNewRollbacks) {
|
if (rollback.hasToken(token)) {
|
rollback.isCancelled = true;
|
return;
|
}
|
}
|
}
|
}
|
}
|
}, enableRollbackTimedOutFilter, null, getHandler());
|
|
IntentFilter userAddedIntentFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
|
mContext.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) {
|
final int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
|
if (newUserId == -1) {
|
return;
|
}
|
registerUserCallbacks(UserHandle.of(newUserId));
|
}
|
}
|
}, userAddedIntentFilter, null, getHandler());
|
|
registerTimeChangeReceiver();
|
}
|
|
private void registerUserCallbacks(UserHandle user) {
|
Context context = getContextAsUser(user);
|
if (context == null) {
|
Log.e(TAG, "Unable to register user callbacks for user " + user);
|
return;
|
}
|
|
// TODO: Reuse the same SessionCallback and broadcast receiver
|
// instances, rather than creating new instances for each user.
|
|
context.getPackageManager().getPackageInstaller()
|
.registerSessionCallback(new SessionCallback(), getHandler());
|
|
IntentFilter filter = new IntentFilter();
|
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
|
filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
|
filter.addDataScheme("package");
|
context.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
String action = intent.getAction();
|
if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
|
String packageName = intent.getData().getSchemeSpecificPart();
|
onPackageReplaced(packageName);
|
}
|
if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
|
String packageName = intent.getData().getSchemeSpecificPart();
|
onPackageFullyRemoved(packageName);
|
}
|
}
|
}, filter, null, getHandler());
|
}
|
|
/**
|
* This method posts a blocking call to the handler thread, so it should not be called from
|
* that same thread.
|
* @throws {@link IllegalStateException} if called from {@link #mHandlerThread}
|
*/
|
@Override
|
public ParceledListSlice getAvailableRollbacks() {
|
enforceManageRollbacks("getAvailableRollbacks");
|
if (Thread.currentThread().equals(mHandlerThread)) {
|
Log.wtf(TAG, "Calling getAvailableRollbacks from mHandlerThread "
|
+ "causes a deadlock");
|
throw new IllegalStateException("Cannot call RollbackManager#getAvailableRollbacks "
|
+ "from the handler thread!");
|
}
|
|
// Wait for the handler thread to get the list of available rollbacks
|
// to get the most up-to-date results. This is intended to reduce test
|
// flakiness when checking available rollbacks immediately after
|
// installing a package with rollback enabled.
|
final LinkedBlockingQueue<Boolean> result = new LinkedBlockingQueue<>();
|
getHandler().post(() -> result.offer(true));
|
|
try {
|
result.take();
|
} catch (InterruptedException ie) {
|
// We may not get the most up-to-date information, but whatever we
|
// can get now is better than nothing, so log but otherwise ignore
|
// the exception.
|
Log.w(TAG, "Interrupted while waiting for handler thread in getAvailableRollbacks");
|
}
|
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
List<RollbackInfo> rollbacks = new ArrayList<>();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE) {
|
rollbacks.add(data.info);
|
}
|
}
|
return new ParceledListSlice<>(rollbacks);
|
}
|
}
|
|
@Override
|
public ParceledListSlice<RollbackInfo> getRecentlyExecutedRollbacks() {
|
enforceManageRollbacks("getRecentlyCommittedRollbacks");
|
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
List<RollbackInfo> rollbacks = new ArrayList<>();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.state == RollbackData.ROLLBACK_STATE_COMMITTED) {
|
rollbacks.add(data.info);
|
}
|
}
|
return new ParceledListSlice<>(rollbacks);
|
}
|
}
|
|
@Override
|
public void commitRollback(int rollbackId, ParceledListSlice causePackages,
|
String callerPackageName, IntentSender statusReceiver) {
|
enforceManageRollbacks("executeRollback");
|
|
final int callingUid = Binder.getCallingUid();
|
AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
|
appOps.checkPackage(callingUid, callerPackageName);
|
|
getHandler().post(() ->
|
commitRollbackInternal(rollbackId, causePackages.getList(),
|
callerPackageName, statusReceiver));
|
}
|
|
private void registerTimeChangeReceiver() {
|
final BroadcastReceiver timeChangeIntentReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
final long oldRelativeBootTime = mRelativeBootTime;
|
mRelativeBootTime = calculateRelativeBootTime();
|
final long timeDifference = mRelativeBootTime - oldRelativeBootTime;
|
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
|
Iterator<RollbackData> iter = mRollbacks.iterator();
|
while (iter.hasNext()) {
|
RollbackData data = iter.next();
|
data.timestamp = data.timestamp.plusMillis(timeDifference);
|
saveRollbackData(data);
|
}
|
}
|
}
|
};
|
final IntentFilter filter = new IntentFilter();
|
filter.addAction(Intent.ACTION_TIME_CHANGED);
|
mContext.registerReceiver(timeChangeIntentReceiver, filter,
|
null /* broadcastPermission */, getHandler());
|
}
|
|
private static long calculateRelativeBootTime() {
|
return System.currentTimeMillis() - SystemClock.elapsedRealtime();
|
}
|
|
/**
|
* Performs the actual work to commit a rollback.
|
* The work is done on the current thread. This may be a long running
|
* operation.
|
*/
|
private void commitRollbackInternal(int rollbackId, List<VersionedPackage> causePackages,
|
String callerPackageName, IntentSender statusReceiver) {
|
Log.i(TAG, "Initiating rollback");
|
|
RollbackData data = getRollbackForId(rollbackId);
|
if (data == null || data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) {
|
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
|
"Rollback unavailable");
|
return;
|
}
|
|
// Get a context for the caller to use to install the downgraded
|
// version of the package.
|
Context context = null;
|
try {
|
context = mContext.createPackageContext(callerPackageName, 0);
|
} catch (PackageManager.NameNotFoundException e) {
|
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
|
"Invalid callerPackageName");
|
return;
|
}
|
|
PackageManager pm = context.getPackageManager();
|
try {
|
PackageInstaller packageInstaller = pm.getPackageInstaller();
|
PackageInstaller.SessionParams parentParams = new PackageInstaller.SessionParams(
|
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
|
parentParams.setRequestDowngrade(true);
|
parentParams.setMultiPackage();
|
if (data.isStaged()) {
|
parentParams.setStaged();
|
}
|
|
int parentSessionId = packageInstaller.createSession(parentParams);
|
PackageInstaller.Session parentSession = packageInstaller.openSession(parentSessionId);
|
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
|
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
|
// TODO: We can't get the installerPackageName for apex
|
// (b/123920130). Is it okay to ignore the installer package
|
// for apex?
|
if (!info.isApex()) {
|
String installerPackageName = pm.getInstallerPackageName(info.getPackageName());
|
if (installerPackageName != null) {
|
params.setInstallerPackageName(installerPackageName);
|
}
|
}
|
params.setRequestDowngrade(true);
|
params.setRequiredInstalledVersionCode(
|
info.getVersionRolledBackFrom().getLongVersionCode());
|
if (data.isStaged()) {
|
params.setStaged();
|
}
|
if (info.isApex()) {
|
params.setInstallAsApex();
|
}
|
int sessionId = packageInstaller.createSession(params);
|
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
|
|
File[] packageCodePaths = RollbackStore.getPackageCodePaths(
|
data, info.getPackageName());
|
if (packageCodePaths == null) {
|
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
|
"Backup copy of package inaccessible");
|
return;
|
}
|
|
for (File packageCodePath : packageCodePaths) {
|
try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(packageCodePath,
|
ParcelFileDescriptor.MODE_READ_ONLY)) {
|
final long token = Binder.clearCallingIdentity();
|
try {
|
session.write(packageCodePath.getName(), 0, packageCodePath.length(),
|
fd);
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
}
|
parentSession.addChildSessionId(sessionId);
|
}
|
|
final LocalIntentReceiver receiver = new LocalIntentReceiver(
|
(Intent result) -> {
|
getHandler().post(() -> {
|
|
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
|
PackageInstaller.STATUS_FAILURE);
|
if (status != PackageInstaller.STATUS_SUCCESS) {
|
// Committing the rollback failed, but we
|
// still have all the info we need to try
|
// rolling back again, so restore the rollback
|
// state to how it was before we tried
|
// committing.
|
// TODO: Should we just kill this rollback if
|
// commit failed? Why would we expect commit
|
// not to fail again?
|
synchronized (mLock) {
|
// TODO: Could this cause a rollback to be
|
// resurrected if it should otherwise have
|
// expired by now?
|
data.state = RollbackData.ROLLBACK_STATE_AVAILABLE;
|
data.restoreUserDataInProgress = false;
|
}
|
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_INSTALL,
|
"Rollback downgrade install failed: "
|
+ result.getStringExtra(
|
PackageInstaller.EXTRA_STATUS_MESSAGE));
|
return;
|
}
|
|
synchronized (mLock) {
|
if (!data.isStaged()) {
|
// All calls to restoreUserData should have
|
// completed by now for a non-staged install.
|
data.restoreUserDataInProgress = false;
|
}
|
|
data.info.setCommittedSessionId(parentSessionId);
|
data.info.getCausePackages().addAll(causePackages);
|
}
|
mRollbackStore.deletePackageCodePaths(data);
|
saveRollbackData(data);
|
|
sendSuccess(statusReceiver);
|
|
Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED);
|
|
mContext.sendBroadcastAsUser(broadcast, UserHandle.SYSTEM,
|
Manifest.permission.MANAGE_ROLLBACKS);
|
});
|
}
|
);
|
|
synchronized (mLock) {
|
data.state = RollbackData.ROLLBACK_STATE_COMMITTED;
|
data.restoreUserDataInProgress = true;
|
}
|
parentSession.commit(receiver.getIntentSender());
|
} catch (IOException e) {
|
Log.e(TAG, "Rollback failed", e);
|
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
|
"IOException: " + e.toString());
|
return;
|
}
|
}
|
|
@Override
|
public void reloadPersistedData() {
|
mContext.enforceCallingOrSelfPermission(
|
Manifest.permission.TEST_MANAGE_ROLLBACKS,
|
"reloadPersistedData");
|
|
synchronized (mLock) {
|
mRollbacks = null;
|
}
|
getHandler().post(() -> {
|
updateRollbackLifetimeDurationInMillis();
|
ensureRollbackDataLoaded();
|
});
|
}
|
|
@Override
|
public void expireRollbackForPackage(String packageName) {
|
mContext.enforceCallingOrSelfPermission(
|
Manifest.permission.TEST_MANAGE_ROLLBACKS,
|
"expireRollbackForPackage");
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
Iterator<RollbackData> iter = mRollbacks.iterator();
|
while (iter.hasNext()) {
|
RollbackData data = iter.next();
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
if (info.getPackageName().equals(packageName)) {
|
iter.remove();
|
deleteRollback(data);
|
break;
|
}
|
}
|
}
|
}
|
}
|
|
void onUnlockUser(int userId) {
|
getHandler().post(() -> {
|
final List<RollbackData> rollbacks;
|
synchronized (mLock) {
|
rollbacks = new ArrayList<>(mRollbacks);
|
}
|
|
final Set<RollbackData> changed =
|
mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId, rollbacks);
|
|
for (RollbackData rd : changed) {
|
saveRollbackData(rd);
|
}
|
});
|
}
|
|
private void updateRollbackLifetimeDurationInMillis() {
|
mRollbackLifetimeDurationInMillis = DeviceConfig.getLong(
|
DeviceConfig.NAMESPACE_ROLLBACK_BOOT,
|
RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS,
|
DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS);
|
if (mRollbackLifetimeDurationInMillis < 0) {
|
mRollbackLifetimeDurationInMillis = DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS;
|
}
|
}
|
|
void onBootCompleted() {
|
getHandler().post(() -> updateRollbackLifetimeDurationInMillis());
|
// Also posts to handler thread
|
scheduleExpiration(0);
|
|
getHandler().post(() -> {
|
// Check to see if any rollback-enabled staged sessions or staged
|
// rollback sessions been applied.
|
List<RollbackData> enabling = new ArrayList<>();
|
List<RollbackData> restoreInProgress = new ArrayList<>();
|
Set<String> apexPackageNames = new HashSet<>();
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
for (RollbackData data : mRollbacks) {
|
if (data.isStaged()) {
|
if (data.state == RollbackData.ROLLBACK_STATE_ENABLING) {
|
enabling.add(data);
|
} else if (data.restoreUserDataInProgress) {
|
restoreInProgress.add(data);
|
}
|
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
if (info.isApex()) {
|
apexPackageNames.add(info.getPackageName());
|
}
|
}
|
}
|
}
|
}
|
|
for (RollbackData data : enabling) {
|
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
|
PackageInstaller.SessionInfo session = installer.getSessionInfo(
|
data.stagedSessionId);
|
// TODO: What if session is null?
|
if (session != null) {
|
if (session.isStagedSessionApplied()) {
|
makeRollbackAvailable(data);
|
} else if (session.isStagedSessionFailed()) {
|
// TODO: Do we need to remove this from
|
// mRollbacks, or is it okay to leave as
|
// unavailable until the next reboot when it will go
|
// away on its own?
|
deleteRollback(data);
|
}
|
}
|
}
|
|
for (RollbackData data : restoreInProgress) {
|
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
|
PackageInstaller.SessionInfo session = installer.getSessionInfo(
|
data.stagedSessionId);
|
// TODO: What if session is null?
|
if (session != null) {
|
if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) {
|
synchronized (mLock) {
|
data.restoreUserDataInProgress = false;
|
}
|
saveRollbackData(data);
|
}
|
}
|
}
|
|
for (String apexPackageName : apexPackageNames) {
|
// We will not recieve notifications when an apex is updated,
|
// so check now in case any rollbacks ought to be expired. The
|
// onPackagedReplace function is safe to call if the package
|
// hasn't actually been updated.
|
onPackageReplaced(apexPackageName);
|
}
|
mPackageHealthObserver.onBootCompletedAsync();
|
});
|
}
|
|
/**
|
* Load rollback data from storage if it has not already been loaded.
|
* After calling this funciton, mAvailableRollbacks and
|
* mRecentlyExecutedRollbacks will be non-null.
|
*/
|
private void ensureRollbackDataLoaded() {
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
}
|
}
|
|
/**
|
* Load rollback data from storage if it has not already been loaded.
|
* After calling this function, mRollbacks will be non-null.
|
*/
|
@GuardedBy("mLock")
|
private void ensureRollbackDataLoadedLocked() {
|
if (mRollbacks == null) {
|
loadAllRollbackDataLocked();
|
}
|
}
|
|
/**
|
* Load all rollback data from storage.
|
* Note: We do potentially heavy IO here while holding mLock, because we
|
* have to have the rollback data loaded before we can do anything else
|
* meaningful.
|
*/
|
@GuardedBy("mLock")
|
private void loadAllRollbackDataLocked() {
|
mRollbacks = mRollbackStore.loadAllRollbackData();
|
for (RollbackData data : mRollbacks) {
|
mAllocatedRollbackIds.put(data.info.getRollbackId(), true);
|
}
|
}
|
|
/**
|
* Called when a package has been replaced with a different version.
|
* Removes all backups for the package not matching the currently
|
* installed package version.
|
*/
|
private void onPackageReplaced(String packageName) {
|
// TODO: Could this end up incorrectly deleting a rollback for a
|
// package that is about to be installed?
|
VersionedPackage installedVersion = getInstalledPackageVersion(packageName);
|
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
Iterator<RollbackData> iter = mRollbacks.iterator();
|
while (iter.hasNext()) {
|
RollbackData data = iter.next();
|
// TODO: Should we remove rollbacks in the ENABLING state here?
|
if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE
|
|| data.state == RollbackData.ROLLBACK_STATE_ENABLING) {
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
if (info.getPackageName().equals(packageName)
|
&& !packageVersionsEqual(
|
info.getVersionRolledBackFrom(),
|
installedVersion)) {
|
iter.remove();
|
deleteRollback(data);
|
break;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
/**
|
* Called when a package has been completely removed from the device.
|
* Removes all backups and rollback history for the given package.
|
*/
|
private void onPackageFullyRemoved(String packageName) {
|
expireRollbackForPackage(packageName);
|
}
|
|
/**
|
* Notifies an IntentSender of failure.
|
*
|
* @param statusReceiver where to send the failure
|
* @param status the RollbackManager.STATUS_* code with the failure.
|
* @param message the failure message.
|
*/
|
private void sendFailure(IntentSender statusReceiver, @RollbackManager.Status int status,
|
String message) {
|
Log.e(TAG, message);
|
try {
|
final Intent fillIn = new Intent();
|
fillIn.putExtra(RollbackManager.EXTRA_STATUS, status);
|
fillIn.putExtra(RollbackManager.EXTRA_STATUS_MESSAGE, message);
|
statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
|
} catch (IntentSender.SendIntentException e) {
|
// Nowhere to send the result back to, so don't bother.
|
}
|
}
|
|
/**
|
* Notifies an IntentSender of success.
|
*/
|
private void sendSuccess(IntentSender statusReceiver) {
|
try {
|
final Intent fillIn = new Intent();
|
fillIn.putExtra(RollbackManager.EXTRA_STATUS, RollbackManager.STATUS_SUCCESS);
|
statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
|
} catch (IntentSender.SendIntentException e) {
|
// Nowhere to send the result back to, so don't bother.
|
}
|
}
|
|
// Check to see if anything needs expiration, and if so, expire it.
|
// Schedules future expiration as appropriate.
|
private void runExpiration() {
|
Instant now = Instant.now();
|
Instant oldest = null;
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
|
Iterator<RollbackData> iter = mRollbacks.iterator();
|
while (iter.hasNext()) {
|
RollbackData data = iter.next();
|
if (data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) {
|
continue;
|
}
|
if (!now.isBefore(data.timestamp.plusMillis(mRollbackLifetimeDurationInMillis))) {
|
iter.remove();
|
deleteRollback(data);
|
} else if (oldest == null || oldest.isAfter(data.timestamp)) {
|
oldest = data.timestamp;
|
}
|
}
|
}
|
|
if (oldest != null) {
|
scheduleExpiration(now.until(oldest.plusMillis(mRollbackLifetimeDurationInMillis),
|
ChronoUnit.MILLIS));
|
}
|
}
|
|
/**
|
* Schedules an expiration check to be run after the given duration in
|
* milliseconds has gone by.
|
*/
|
private void scheduleExpiration(long duration) {
|
getHandler().postDelayed(() -> runExpiration(), duration);
|
}
|
|
private Handler getHandler() {
|
return mHandlerThread.getThreadHandler();
|
}
|
|
// Returns true if <code>session</code> has installFlags and code path
|
// matching the installFlags and new package code path given to
|
// enableRollback.
|
private boolean sessionMatchesForEnableRollback(PackageInstaller.SessionInfo session,
|
int installFlags, File newPackageCodePath) {
|
if (session == null || session.resolvedBaseCodePath == null) {
|
return false;
|
}
|
|
File packageCodePath = new File(session.resolvedBaseCodePath).getParentFile();
|
if (newPackageCodePath.equals(packageCodePath) && installFlags == session.installFlags) {
|
return true;
|
}
|
|
return false;
|
}
|
|
private Context getContextAsUser(UserHandle user) {
|
try {
|
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
|
} catch (PackageManager.NameNotFoundException e) {
|
return null;
|
}
|
}
|
|
/**
|
* Called via broadcast by the package manager when a package is being
|
* staged for install with rollback enabled. Called before the package has
|
* been installed.
|
*
|
* @param installFlags information about what is being installed.
|
* @param newPackageCodePath path to the package about to be installed.
|
* @param installedUsers the set of users for which a given package is installed.
|
* @param user the user that owns the install session to enable rollback on.
|
* @param token the distinct rollback token sent by package manager.
|
* @return true if enabling the rollback succeeds, false otherwise.
|
*/
|
private boolean enableRollback(int installFlags, File newPackageCodePath,
|
int[] installedUsers, @UserIdInt int user, int token) {
|
|
// Find the session id associated with this install.
|
// TODO: It would be nice if package manager or package installer told
|
// us the session directly, rather than have to search for it
|
// ourselves.
|
|
// getAllSessions only returns sessions for the associated user.
|
// Create a context with the right user so we can find the matching
|
// session.
|
final Context context = getContextAsUser(UserHandle.of(user));
|
if (context == null) {
|
Log.e(TAG, "Unable to create context for install session user.");
|
return false;
|
}
|
|
PackageInstaller.SessionInfo parentSession = null;
|
PackageInstaller.SessionInfo packageSession = null;
|
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
|
for (PackageInstaller.SessionInfo info : installer.getAllSessions()) {
|
if (info.isMultiPackage()) {
|
for (int childId : info.getChildSessionIds()) {
|
PackageInstaller.SessionInfo child = installer.getSessionInfo(childId);
|
if (sessionMatchesForEnableRollback(child, installFlags, newPackageCodePath)) {
|
// TODO: Check we only have one matching session?
|
parentSession = info;
|
packageSession = child;
|
break;
|
}
|
}
|
} else if (sessionMatchesForEnableRollback(info, installFlags, newPackageCodePath)) {
|
// TODO: Check we only have one matching session?
|
parentSession = info;
|
packageSession = info;
|
break;
|
}
|
}
|
|
if (parentSession == null || packageSession == null) {
|
Log.e(TAG, "Unable to find session for enabled rollback.");
|
return false;
|
}
|
|
// Check to see if this is the apk session for a staged session with
|
// rollback enabled.
|
// TODO: This check could be made more efficient.
|
RollbackData rd = null;
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.apkSessionId == parentSession.getSessionId()) {
|
rd = data;
|
break;
|
}
|
}
|
}
|
|
if (rd != null) {
|
// This is the apk session for a staged session. We do not need to create a new rollback
|
// for this session.
|
PackageParser.PackageLite newPackage = null;
|
try {
|
newPackage = PackageParser.parsePackageLite(
|
new File(packageSession.resolvedBaseCodePath), 0);
|
} catch (PackageParser.PackageParserException e) {
|
Log.e(TAG, "Unable to parse new package", e);
|
return false;
|
}
|
String packageName = newPackage.packageName;
|
for (PackageRollbackInfo info : rd.info.getPackages()) {
|
if (info.getPackageName().equals(packageName)) {
|
info.getInstalledUsers().addAll(IntArray.wrap(installedUsers));
|
return true;
|
}
|
}
|
Log.e(TAG, "Unable to find package in apk session");
|
return false;
|
}
|
|
NewRollback newRollback;
|
synchronized (mLock) {
|
// See if we already have a NewRollback that contains this package
|
// session. If not, create a NewRollback for the parent session
|
// that we will use for all the packages in the session.
|
newRollback = getNewRollbackForPackageSessionLocked(packageSession.getSessionId());
|
if (newRollback == null) {
|
newRollback = createNewRollbackLocked(parentSession);
|
mNewRollbacks.add(newRollback);
|
}
|
}
|
newRollback.addToken(token);
|
|
return enableRollbackForPackageSession(newRollback.data, packageSession, installedUsers);
|
}
|
|
/**
|
* Do code and userdata backups to enable rollback of the given session.
|
* In case of multiPackage sessions, <code>session</code> should be one of
|
* the child sessions, not the parent session.
|
*
|
* @return true on success, false on failure.
|
*/
|
private boolean enableRollbackForPackageSession(RollbackData data,
|
PackageInstaller.SessionInfo session, @NonNull int[] installedUsers) {
|
// TODO: Don't attempt to enable rollback for split installs.
|
final int installFlags = session.installFlags;
|
if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
|
Log.e(TAG, "Rollback is not enabled.");
|
return false;
|
}
|
if ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
|
Log.e(TAG, "Rollbacks not supported for instant app install");
|
return false;
|
}
|
|
if (session.resolvedBaseCodePath == null) {
|
Log.e(TAG, "Session code path has not been resolved.");
|
return false;
|
}
|
|
// Get information about the package to be installed.
|
PackageParser.PackageLite newPackage = null;
|
try {
|
newPackage = PackageParser.parsePackageLite(new File(session.resolvedBaseCodePath), 0);
|
} catch (PackageParser.PackageParserException e) {
|
Log.e(TAG, "Unable to parse new package", e);
|
return false;
|
}
|
|
String packageName = newPackage.packageName;
|
Log.i(TAG, "Enabling rollback for install of " + packageName
|
+ ", session:" + session.sessionId);
|
|
String installerPackageName = session.getInstallerPackageName();
|
if (!enableRollbackAllowed(installerPackageName, packageName)) {
|
Log.e(TAG, "Installer " + installerPackageName
|
+ " is not allowed to enable rollback on " + packageName);
|
return false;
|
}
|
|
VersionedPackage newVersion = new VersionedPackage(packageName, newPackage.versionCode);
|
final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0);
|
|
// Get information about the currently installed package.
|
PackageManager pm = mContext.getPackageManager();
|
final PackageInfo pkgInfo;
|
try {
|
pkgInfo = getPackageInfo(packageName);
|
} catch (PackageManager.NameNotFoundException e) {
|
// TODO: Support rolling back fresh package installs rather than
|
// fail here. Test this case.
|
Log.e(TAG, packageName + " is not installed");
|
return false;
|
}
|
|
VersionedPackage installedVersion = new VersionedPackage(packageName,
|
pkgInfo.getLongVersionCode());
|
|
PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(
|
newVersion, installedVersion,
|
new IntArray() /* pendingBackups */, new ArrayList<>() /* pendingRestores */,
|
isApex, IntArray.wrap(installedUsers),
|
new SparseLongArray() /* ceSnapshotInodes */);
|
|
try {
|
ApplicationInfo appInfo = pkgInfo.applicationInfo;
|
RollbackStore.backupPackageCodePath(data, packageName, appInfo.sourceDir);
|
if (!ArrayUtils.isEmpty(appInfo.splitSourceDirs)) {
|
for (String sourceDir : appInfo.splitSourceDirs) {
|
RollbackStore.backupPackageCodePath(data, packageName, sourceDir);
|
}
|
}
|
} catch (IOException e) {
|
Log.e(TAG, "Unable to copy package for rollback for " + packageName, e);
|
return false;
|
}
|
|
synchronized (mLock) {
|
data.info.getPackages().add(packageRollbackInfo);
|
}
|
return true;
|
}
|
|
@Override
|
public void snapshotAndRestoreUserData(String packageName, int[] userIds, int appId,
|
long ceDataInode, String seInfo, int token) {
|
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
|
throw new SecurityException(
|
"snapshotAndRestoreUserData may only be called by the system.");
|
}
|
|
getHandler().post(() -> {
|
snapshotUserDataInternal(packageName);
|
restoreUserDataInternal(packageName, userIds, appId, ceDataInode, seInfo, token);
|
final PackageManagerInternal pmi = LocalServices.getService(
|
PackageManagerInternal.class);
|
pmi.finishPackageInstall(token, false);
|
});
|
}
|
|
private void snapshotUserDataInternal(String packageName) {
|
synchronized (mLock) {
|
// staged installs
|
ensureRollbackDataLoadedLocked();
|
for (int i = 0; i < mRollbacks.size(); i++) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.state != RollbackData.ROLLBACK_STATE_ENABLING) {
|
continue;
|
}
|
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
if (info.getPackageName().equals(packageName)) {
|
mAppDataRollbackHelper.snapshotAppData(data.info.getRollbackId(), info);
|
saveRollbackData(data);
|
break;
|
}
|
}
|
}
|
// non-staged installs
|
PackageRollbackInfo info;
|
for (NewRollback rollback : mNewRollbacks) {
|
info = getPackageRollbackInfo(rollback.data, packageName);
|
if (info != null) {
|
mAppDataRollbackHelper.snapshotAppData(rollback.data.info.getRollbackId(),
|
info);
|
saveRollbackData(rollback.data);
|
}
|
}
|
}
|
}
|
|
private void restoreUserDataInternal(String packageName, int[] userIds, int appId,
|
long ceDataInode, String seInfo, int token) {
|
PackageRollbackInfo info = null;
|
RollbackData rollbackData = null;
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.restoreUserDataInProgress) {
|
info = getPackageRollbackInfo(data, packageName);
|
if (info != null) {
|
rollbackData = data;
|
break;
|
}
|
}
|
}
|
}
|
|
if (rollbackData == null) {
|
return;
|
}
|
|
for (int userId : userIds) {
|
final boolean changedRollbackData = mAppDataRollbackHelper.restoreAppData(
|
rollbackData.info.getRollbackId(), info, userId, appId, seInfo);
|
|
// We've updated metadata about this rollback, so save it to flash.
|
if (changedRollbackData) {
|
saveRollbackData(rollbackData);
|
}
|
}
|
}
|
|
@Override
|
public boolean notifyStagedSession(int sessionId) {
|
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
|
throw new SecurityException("notifyStagedSession may only be called by the system.");
|
}
|
final LinkedBlockingQueue<Boolean> result = new LinkedBlockingQueue<>();
|
|
// NOTE: We post this runnable on the RollbackManager's binder thread because we'd prefer
|
// to preserve the invariant that all operations that modify state happen there.
|
getHandler().post(() -> {
|
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
|
|
final PackageInstaller.SessionInfo session = installer.getSessionInfo(sessionId);
|
if (session == null) {
|
Log.e(TAG, "No matching install session for: " + sessionId);
|
result.offer(false);
|
return;
|
}
|
|
NewRollback newRollback;
|
synchronized (mLock) {
|
newRollback = createNewRollbackLocked(session);
|
}
|
|
if (!session.isMultiPackage()) {
|
if (!enableRollbackForPackageSession(newRollback.data, session,
|
new int[0])) {
|
Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
|
result.offer(false);
|
return;
|
}
|
} else {
|
for (int childSessionId : session.getChildSessionIds()) {
|
final PackageInstaller.SessionInfo childSession =
|
installer.getSessionInfo(childSessionId);
|
if (childSession == null) {
|
Log.e(TAG, "No matching child install session for: " + childSessionId);
|
result.offer(false);
|
return;
|
}
|
if (!enableRollbackForPackageSession(newRollback.data, childSession,
|
new int[0])) {
|
Log.e(TAG, "Unable to enable rollback for session: " + sessionId);
|
result.offer(false);
|
return;
|
}
|
}
|
}
|
|
result.offer(completeEnableRollback(newRollback, true) != null);
|
});
|
|
try {
|
return result.take();
|
} catch (InterruptedException ie) {
|
Log.e(TAG, "Interrupted while waiting for notifyStagedSession response");
|
return false;
|
}
|
}
|
|
@Override
|
public void notifyStagedApkSession(int originalSessionId, int apkSessionId) {
|
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
|
throw new SecurityException("notifyStagedApkSession may only be called by the system.");
|
}
|
getHandler().post(() -> {
|
RollbackData rd = null;
|
synchronized (mLock) {
|
ensureRollbackDataLoadedLocked();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.stagedSessionId == originalSessionId) {
|
data.apkSessionId = apkSessionId;
|
rd = data;
|
break;
|
}
|
}
|
}
|
|
if (rd != null) {
|
saveRollbackData(rd);
|
}
|
});
|
}
|
|
/**
|
* Returns true if the installer is allowed to enable rollback for the
|
* given named package, false otherwise.
|
*/
|
private boolean enableRollbackAllowed(String installerPackageName, String packageName) {
|
if (installerPackageName == null) {
|
return false;
|
}
|
|
PackageManager pm = mContext.getPackageManager();
|
boolean manageRollbacksGranted = pm.checkPermission(
|
Manifest.permission.MANAGE_ROLLBACKS,
|
installerPackageName) == PackageManager.PERMISSION_GRANTED;
|
|
boolean testManageRollbacksGranted = pm.checkPermission(
|
Manifest.permission.TEST_MANAGE_ROLLBACKS,
|
installerPackageName) == PackageManager.PERMISSION_GRANTED;
|
|
// For now only allow rollbacks for modules or for testing.
|
return (isModule(packageName) && manageRollbacksGranted)
|
|| testManageRollbacksGranted;
|
}
|
|
/**
|
* Returns true if the package name is the name of a module.
|
*/
|
private boolean isModule(String packageName) {
|
PackageManager pm = mContext.getPackageManager();
|
final ModuleInfo moduleInfo;
|
try {
|
moduleInfo = pm.getModuleInfo(packageName, 0);
|
} catch (PackageManager.NameNotFoundException e) {
|
return false;
|
}
|
|
return moduleInfo != null;
|
}
|
|
/**
|
* Gets the version of the package currently installed.
|
* Returns null if the package is not currently installed.
|
*/
|
private VersionedPackage getInstalledPackageVersion(String packageName) {
|
PackageManager pm = mContext.getPackageManager();
|
PackageInfo pkgInfo = null;
|
try {
|
pkgInfo = getPackageInfo(packageName);
|
} catch (PackageManager.NameNotFoundException e) {
|
return null;
|
}
|
|
return new VersionedPackage(packageName, pkgInfo.getLongVersionCode());
|
}
|
|
/**
|
* Gets PackageInfo for the given package.
|
* Matches any user and apex. Returns null if no such package is
|
* installed.
|
*/
|
private PackageInfo getPackageInfo(String packageName)
|
throws PackageManager.NameNotFoundException {
|
PackageManager pm = mContext.getPackageManager();
|
try {
|
// The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX
|
// flag, so make two separate attempts to get the package info.
|
// We don't need both flags at the same time because we assume
|
// apex files are always installed for all users.
|
return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER);
|
} catch (PackageManager.NameNotFoundException e) {
|
return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
|
}
|
}
|
|
|
private boolean packageVersionsEqual(VersionedPackage a, VersionedPackage b) {
|
return a != null && b != null
|
&& a.getPackageName().equals(b.getPackageName())
|
&& a.getLongVersionCode() == b.getLongVersionCode();
|
}
|
|
private class SessionCallback extends PackageInstaller.SessionCallback {
|
|
@Override
|
public void onCreated(int 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) {
|
NewRollback newRollback;
|
synchronized (mLock) {
|
newRollback = getNewRollbackForPackageSessionLocked(sessionId);
|
if (newRollback != null) {
|
mNewRollbacks.remove(newRollback);
|
}
|
}
|
|
if (newRollback != null) {
|
RollbackData rollback = completeEnableRollback(newRollback, success);
|
if (rollback != null && !rollback.isStaged()) {
|
makeRollbackAvailable(rollback);
|
}
|
}
|
}
|
}
|
|
/**
|
* Add a rollback to the list of rollbacks.
|
* This should be called after rollback has been enabled for all packages
|
* in the rollback. It does not make the rollback available yet.
|
*
|
* @return the rollback data for a successfully enable-completed rollback,
|
* or null on error.
|
*/
|
private RollbackData completeEnableRollback(NewRollback newRollback, boolean success) {
|
RollbackData data = newRollback.data;
|
if (!success) {
|
// The install session was aborted, clean up the pending install.
|
deleteRollback(data);
|
return null;
|
}
|
if (newRollback.isCancelled) {
|
Log.e(TAG, "Rollback has been cancelled by PackageManager");
|
deleteRollback(data);
|
return null;
|
}
|
|
// It's safe to access data.info outside a synchronized block because
|
// this is running on the handler thread and all changes to the
|
// data.info occur on the handler thread.
|
if (data.info.getPackages().size() != newRollback.packageSessionIds.length) {
|
Log.e(TAG, "Failed to enable rollback for all packages in session.");
|
deleteRollback(data);
|
return null;
|
}
|
|
saveRollbackData(data);
|
synchronized (mLock) {
|
// Note: There is a small window of time between when
|
// the session has been committed by the package
|
// manager and when we make the rollback available
|
// here. Presumably the window is small enough that
|
// nobody will want to roll back the newly installed
|
// package before we make the rollback available.
|
// TODO: We'll lose the rollback data if the
|
// device reboots between when the session is
|
// committed and this point. Revisit this after
|
// adding support for rollback of staged installs.
|
ensureRollbackDataLoadedLocked();
|
mRollbacks.add(data);
|
}
|
|
return data;
|
}
|
|
private void makeRollbackAvailable(RollbackData data) {
|
// TODO: What if the rollback has since been expired, for example due
|
// to a new package being installed. Won't this revive an expired
|
// rollback? Consider adding a ROLLBACK_STATE_EXPIRED to address this.
|
synchronized (mLock) {
|
data.state = RollbackData.ROLLBACK_STATE_AVAILABLE;
|
data.timestamp = Instant.now();
|
}
|
saveRollbackData(data);
|
|
// TODO(zezeozue): Provide API to explicitly start observing instead
|
// of doing this for all rollbacks. If we do this for all rollbacks,
|
// should document in PackageInstaller.SessionParams#setEnableRollback
|
// After enabling and commiting any rollback, observe packages and
|
// prepare to rollback if packages crashes too frequently.
|
List<String> packages = new ArrayList<>();
|
for (int i = 0; i < data.info.getPackages().size(); i++) {
|
packages.add(data.info.getPackages().get(i).getPackageName());
|
}
|
mPackageHealthObserver.startObservingHealth(packages,
|
mRollbackLifetimeDurationInMillis);
|
scheduleExpiration(mRollbackLifetimeDurationInMillis);
|
}
|
|
/*
|
* Returns the RollbackData, if any, for a rollback with the given
|
* rollbackId.
|
*/
|
private RollbackData getRollbackForId(int rollbackId) {
|
synchronized (mLock) {
|
// TODO: Have ensureRollbackDataLoadedLocked return the list of
|
// available rollbacks, to hopefully avoid forgetting to call it?
|
ensureRollbackDataLoadedLocked();
|
for (int i = 0; i < mRollbacks.size(); ++i) {
|
RollbackData data = mRollbacks.get(i);
|
if (data.info.getRollbackId() == rollbackId) {
|
return data;
|
}
|
}
|
}
|
|
return null;
|
}
|
|
/**
|
* Returns the {@code PackageRollbackInfo} associated with {@code packageName} from
|
* a specified {@code RollbackData}.
|
*/
|
private static PackageRollbackInfo getPackageRollbackInfo(RollbackData data,
|
String packageName) {
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
if (info.getPackageName().equals(packageName)) {
|
return info;
|
}
|
}
|
|
return null;
|
}
|
|
@GuardedBy("mLock")
|
private int allocateRollbackIdLocked() {
|
int n = 0;
|
int rollbackId;
|
do {
|
rollbackId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
|
if (!mAllocatedRollbackIds.get(rollbackId, false)) {
|
mAllocatedRollbackIds.put(rollbackId, true);
|
return rollbackId;
|
}
|
} while (n++ < 32);
|
|
throw new IllegalStateException("Failed to allocate rollback ID");
|
}
|
|
private void deleteRollback(RollbackData rollbackData) {
|
for (PackageRollbackInfo info : rollbackData.info.getPackages()) {
|
IntArray installedUsers = info.getInstalledUsers();
|
for (int i = 0; i < installedUsers.size(); i++) {
|
int userId = installedUsers.get(i);
|
mAppDataRollbackHelper.destroyAppDataSnapshot(rollbackData.info.getRollbackId(),
|
info, userId);
|
}
|
}
|
mRollbackStore.deleteRollbackData(rollbackData);
|
}
|
|
/**
|
* Saves rollback data, swallowing any IOExceptions.
|
* For those times when it's not obvious what to do about the IOException.
|
* TODO: Double check we can't do a better job handling the IOException in
|
* a cases where this method is called.
|
*/
|
private void saveRollbackData(RollbackData rollbackData) {
|
try {
|
mRollbackStore.saveRollbackData(rollbackData);
|
} catch (IOException ioe) {
|
Log.e(TAG, "Unable to save rollback info for: "
|
+ rollbackData.info.getRollbackId(), ioe);
|
}
|
}
|
|
@Override
|
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
|
|
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
|
synchronized (mLock) {
|
for (RollbackData data : mRollbacks) {
|
RollbackInfo info = data.info;
|
ipw.println(info.getRollbackId() + ":");
|
ipw.increaseIndent();
|
ipw.println("-state: " + data.getStateAsString());
|
ipw.println("-timestamp: " + data.timestamp);
|
if (data.stagedSessionId != -1) {
|
ipw.println("-stagedSessionId: " + data.stagedSessionId);
|
}
|
ipw.println("-packages:");
|
ipw.increaseIndent();
|
for (PackageRollbackInfo pkg : info.getPackages()) {
|
ipw.println(pkg.getPackageName()
|
+ " " + pkg.getVersionRolledBackFrom().getLongVersionCode()
|
+ " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
|
}
|
ipw.decreaseIndent();
|
if (data.state == RollbackData.ROLLBACK_STATE_COMMITTED) {
|
ipw.println("-causePackages:");
|
ipw.increaseIndent();
|
for (VersionedPackage cPkg : info.getCausePackages()) {
|
ipw.println(cPkg.getPackageName() + " " + cPkg.getLongVersionCode());
|
}
|
ipw.decreaseIndent();
|
ipw.println("-committedSessionId: " + info.getCommittedSessionId());
|
}
|
ipw.decreaseIndent();
|
}
|
}
|
}
|
|
private void enforceManageRollbacks(@NonNull String message) {
|
if ((PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
|
Manifest.permission.MANAGE_ROLLBACKS))
|
&& (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
|
Manifest.permission.TEST_MANAGE_ROLLBACKS))) {
|
throw new SecurityException(message + " requires "
|
+ Manifest.permission.MANAGE_ROLLBACKS + " or "
|
+ Manifest.permission.TEST_MANAGE_ROLLBACKS);
|
}
|
}
|
|
private static class NewRollback {
|
public final RollbackData data;
|
|
/**
|
* This array holds all of the rollback tokens associated with package sessions included
|
* in this rollback. This is used to identify which rollback should be cancelled in case
|
* {@link PackageManager} sends an {@link Intent#ACTION_CANCEL_ENABLE_ROLLBACK} intent.
|
*/
|
private final IntArray mTokens = new IntArray();
|
|
/**
|
* Session ids for all packages in the install.
|
* For multi-package sessions, this is the list of child session ids.
|
* For normal sessions, this list is a single element with the normal
|
* session id.
|
*/
|
public final int[] packageSessionIds;
|
|
/**
|
* Flag to determine whether the RollbackData has been cancelled.
|
*
|
* <p>RollbackData could be invalidated and cancelled if RollbackManager receives
|
* {@link Intent#ACTION_CANCEL_ENABLE_ROLLBACK} from {@link PackageManager}.
|
*
|
* <p>The main underlying assumption here is that if enabling the rollback times out, then
|
* {@link PackageManager} will NOT send
|
* {@link PackageInstaller.SessionCallback#onFinished(int, boolean)} before it broadcasts
|
* {@link Intent#ACTION_CANCEL_ENABLE_ROLLBACK}.
|
*/
|
public boolean isCancelled = false;
|
|
NewRollback(RollbackData data, int[] packageSessionIds) {
|
this.data = data;
|
this.packageSessionIds = packageSessionIds;
|
}
|
|
public void addToken(int token) {
|
mTokens.add(token);
|
}
|
|
public boolean hasToken(int token) {
|
return mTokens.indexOf(token) != -1;
|
}
|
}
|
|
NewRollback createNewRollbackLocked(PackageInstaller.SessionInfo parentSession) {
|
int rollbackId = allocateRollbackIdLocked();
|
final RollbackData data;
|
int parentSessionId = parentSession.getSessionId();
|
|
if (parentSession.isStaged()) {
|
data = mRollbackStore.createStagedRollback(rollbackId, parentSessionId);
|
} else {
|
data = mRollbackStore.createNonStagedRollback(rollbackId);
|
}
|
|
int[] packageSessionIds;
|
if (parentSession.isMultiPackage()) {
|
packageSessionIds = parentSession.getChildSessionIds();
|
} else {
|
packageSessionIds = new int[]{parentSessionId};
|
}
|
|
return new NewRollback(data, packageSessionIds);
|
}
|
|
/**
|
* Returns the NewRollback associated with the given package session.
|
* Returns null if no NewRollback is found for the given package
|
* session.
|
*/
|
NewRollback getNewRollbackForPackageSessionLocked(int packageSessionId) {
|
// We expect mNewRollbacks to be a very small list; linear search
|
// should be plenty fast.
|
for (NewRollback newRollbackData : mNewRollbacks) {
|
for (int id : newRollbackData.packageSessionIds) {
|
if (id == packageSessionId) {
|
return newRollbackData;
|
}
|
}
|
}
|
return null;
|
}
|
}
|