blob: 5e10916c4491a39d4be9014ded020e8d5206a0be [file] [log] [blame]
/*
* Copyright (C) 2017 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.backup.keyvalue;
import static android.app.ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL;
import static android.os.ParcelFileDescriptor.MODE_CREATE;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupManager;
import android.app.backup.BackupTransport;
import android.app.backup.IBackupCallback;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.ConditionVariable;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.SELinux;
import android.os.UserHandle;
import android.os.WorkSource;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.Preconditions;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.backup.BackupAgentTimeoutParameters;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.DataChangedJournal;
import com.android.server.backup.KeyValueBackupJob;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.fullbackup.PerformFullTransportBackupTask;
import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.internal.Operation;
import com.android.server.backup.remote.RemoteCall;
import com.android.server.backup.remote.RemoteCallable;
import com.android.server.backup.remote.RemoteResult;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.transport.TransportNotAvailableException;
import com.android.server.backup.utils.AppBackupUtils;
import libcore.io.IoUtils;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents the task of performing a sequence of key-value backups for a given list of packages.
* Method {@link #run()} executes the backups to the transport specified via the {@code
* transportClient} parameter in the constructor.
*
* <p>A few definitions:
*
* <ul>
* <li>State directory: {@link UserBackupManagerService#getBaseStateDir()}/&lt;transport&gt;
* <li>State file: {@link
* UserBackupManagerService#getBaseStateDir()}/&lt;transport&gt;/&lt;package&gt;<br>
* Represents the state of the backup data for a specific package in the current dataset.
* <li>Stage directory: {@link UserBackupManagerService#getDataDir()}
* <li>Stage file: {@link UserBackupManagerService#getDataDir()}/&lt;package&gt;.data<br>
* Contains staged data that the agents wrote via {@link BackupDataOutput}, to be transmitted
* to the transport.
* </ul>
*
* If there is no PackageManager (PM) pseudo-package state file in the state directory, the
* specified transport will be initialized with {@link IBackupTransport#initializeDevice()}.
*
* <p>The PM pseudo-package is the first package to be backed-up and sent to the transport in case
* of incremental choice. If non-incremental, PM will only be backed-up if specified in the queue,
* and if it's the case it will be re-positioned at the head of the queue.
*
* <p>Before starting, this task will register itself in {@link UserBackupManagerService} current
* operations.
*
* <p>In summary, this task will for each package:
*
* <ul>
* <li>Bind to its {@link IBackupAgent}.
* <li>Request transport quota and flags.
* <li>Call {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor,
* ParcelFileDescriptor, long, IBackupCallback, int)} via {@link RemoteCall} passing the
* old state file descriptor (read), the backup data file descriptor (write), the new state
* file descriptor (write), the quota and the transport flags. This will call {@link
* BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)} with
* the old state file to be read, a {@link BackupDataOutput} object to write the backup data
* and the new state file to write. By writing to {@link BackupDataOutput}, the agent will
* write data to the stage file. The task will block waiting for either:
* <ul>
* <li>Agent response.
* <li>Agent time-out (specified via {@link
* UserBackupManagerService#getAgentTimeoutParameters()}.
* <li>External cancellation or thread interrupt.
* </ul>
* <li>Unbind the agent.
* <li>Assuming agent response, send the staged data that the agent wrote to disk to the transport
* via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}.
* <li>Call {@link IBackupTransport#finishBackup()} if previous call was successful.
* <li>Save the new state in the state file. During the agent call it was being written to
* &lt;state file&gt;.new, here we rename it and replace the old one.
* <li>Delete the stage file.
* </ul>
*
* In the end, this task will:
*
* <ul>
* <li>Mark data-changed for the remaining packages in the queue (skipped packages).
* <li>Delete the {@link DataChangedJournal} provided. Note that this should not be the current
* journal.
* <li>Set {@link UserBackupManagerService} current token as {@link
* IBackupTransport#getCurrentRestoreSet()}, if applicable.
* <li>Add the transport to the list of transports pending initialization ({@link
* UserBackupManagerService#getPendingInits()}) and kick-off initialization if the transport
* ever returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}.
* <li>Unregister the task in current operations.
* <li>Release the wakelock.
* <li>Kick-off {@link PerformFullTransportBackupTask} if a list of full-backup packages was
* provided.
* </ul>
*
* The caller can specify whether this should be an incremental or non-incremental backup. In the
* case of non-incremental the agents will be passed an empty old state file, which signals that a
* complete backup should be performed.
*
* <p>This task is designed to run on a dedicated thread, with the exception of the {@link
* #handleCancel(boolean)} method, which can be called from any thread.
*/
// TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks)
// TODO: Consider having the caller responsible for some clean-up (like resetting state)
// TODO: Distinguish between cancel and time-out where possible for logging/monitoring/observing
public class KeyValueBackupTask implements BackupRestoreTask, Runnable {
private static final String TAG = "KVBT";
private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND;
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final String BLANK_STATE_FILE_NAME = "blank_state";
private static final String PM_PACKAGE = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
private static final String SUCCESS_STATE_SUBDIR = "backing-up";
@VisibleForTesting static final String NO_DATA_END_SENTINEL = "@end@";
@VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data";
@VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new";
/**
* Creates a new {@link KeyValueBackupTask} for key-value backup operation, spins up a new
* dedicated thread and kicks off the operation in it.
*
* @param backupManagerService The {@link UserBackupManagerService} instance.
* @param transportClient The {@link TransportClient} that contains the transport used for the
* operation.
* @param transportDirName The value of {@link IBackupTransport#transportDirName()} for the
* transport whose {@link TransportClient} was provided above.
* @param queue The list of package names that will be backed-up.
* @param dataChangedJournal The old data-changed journal file that will be deleted when the
* operation finishes (successfully or not) or {@code null}.
* @param observer A {@link IBackupObserver}.
* @param monitor A {@link IBackupManagerMonitor}.
* @param listener A {@link OnTaskFinishedListener} or {@code null}.
* @param pendingFullBackups The list of packages that will be passed for a new {@link
* PerformFullTransportBackupTask} operation, which will be started when this finishes.
* @param userInitiated Whether this was user-initiated or not.
* @param nonIncremental If {@code true}, this will be a complete backup for each package,
* otherwise it will be just an incremental one over the current dataset.
* @return The {@link KeyValueBackupTask} that was started.
*/
public static KeyValueBackupTask start(
UserBackupManagerService backupManagerService,
TransportClient transportClient,
String transportDirName,
List<String> queue,
@Nullable DataChangedJournal dataChangedJournal,
IBackupObserver observer,
@Nullable IBackupManagerMonitor monitor,
OnTaskFinishedListener listener,
List<String> pendingFullBackups,
boolean userInitiated,
boolean nonIncremental) {
KeyValueBackupReporter reporter =
new KeyValueBackupReporter(backupManagerService, observer, monitor);
KeyValueBackupTask task =
new KeyValueBackupTask(
backupManagerService,
transportClient,
transportDirName,
queue,
dataChangedJournal,
reporter,
listener,
pendingFullBackups,
userInitiated,
nonIncremental);
Thread thread = new Thread(task, "key-value-backup-" + THREAD_COUNT.incrementAndGet());
thread.start();
KeyValueBackupReporter.onNewThread(thread.getName());
return task;
}
private final UserBackupManagerService mBackupManagerService;
private final PackageManager mPackageManager;
private final TransportClient mTransportClient;
private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
private final KeyValueBackupReporter mReporter;
private final OnTaskFinishedListener mTaskFinishedListener;
private final boolean mUserInitiated;
private final int mCurrentOpToken;
private final int mUserId;
private final File mStateDirectory;
private final File mDataDirectory;
private final File mBlankStateFile;
private final List<String> mOriginalQueue;
private final List<String> mQueue;
private final List<String> mPendingFullBackups;
private final Object mQueueLock;
@Nullable private final DataChangedJournal mJournal;
@Nullable private PerformFullTransportBackupTask mFullBackupTask;
@Nullable private IBackupAgent mAgent;
@Nullable private PackageInfo mCurrentPackage;
@Nullable private File mSavedStateFile;
@Nullable private File mBackupDataFile;
@Nullable private File mNewStateFile;
@Nullable private ParcelFileDescriptor mSavedState;
@Nullable private ParcelFileDescriptor mBackupData;
@Nullable private ParcelFileDescriptor mNewState;
// Indicates whether there was any data to be backed up, i.e. the queue was not empty
// and at least one of the packages had data. Used to avoid updating current token for
// empty backups.
private boolean mHasDataToBackup;
private boolean mNonIncremental;
/**
* This {@link ConditionVariable} is used to signal that the cancel operation has been
* received by the task and that no more transport calls will be made. Anyone can call {@link
* ConditionVariable#block()} to wait for these conditions to hold true, but there should only
* be one place where {@link ConditionVariable#open()} is called. Also there should be no calls
* to {@link ConditionVariable#close()}, which means there is only one cancel per backup -
* subsequent calls to block will return immediately.
*/
private final ConditionVariable mCancelAcknowledged = new ConditionVariable(false);
/**
* Set it to {@code true} and block on {@code mCancelAcknowledged} to wait for the cancellation.
* DO NOT set it to {@code false}.
*/
private volatile boolean mCancelled = false;
/**
* If non-{@code null} there is a pending agent call being made. This call can be cancelled (and
* control returned to this task) with {@link RemoteCall#cancel()}.
*/
@Nullable private volatile RemoteCall mPendingCall;
@VisibleForTesting
public KeyValueBackupTask(
UserBackupManagerService backupManagerService,
TransportClient transportClient,
String transportDirName,
List<String> queue,
@Nullable DataChangedJournal journal,
KeyValueBackupReporter reporter,
OnTaskFinishedListener taskFinishedListener,
List<String> pendingFullBackups,
boolean userInitiated,
boolean nonIncremental) {
mBackupManagerService = backupManagerService;
mPackageManager = backupManagerService.getPackageManager();
mTransportClient = transportClient;
mOriginalQueue = queue;
// We need to retain the original queue contents in case of transport failure
mQueue = new ArrayList<>(queue);
mJournal = journal;
mReporter = reporter;
mTaskFinishedListener = taskFinishedListener;
mPendingFullBackups = pendingFullBackups;
mUserInitiated = userInitiated;
mNonIncremental = nonIncremental;
mAgentTimeoutParameters =
Objects.requireNonNull(
backupManagerService.getAgentTimeoutParameters(),
"Timeout parameters cannot be null");
mStateDirectory = new File(backupManagerService.getBaseStateDir(), transportDirName);
mDataDirectory = mBackupManagerService.getDataDir();
mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
mQueueLock = mBackupManagerService.getQueueLock();
mBlankStateFile = new File(mStateDirectory, BLANK_STATE_FILE_NAME);
mUserId = backupManagerService.getUserId();
}
private void registerTask() {
mBackupManagerService.putOperation(
mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
}
private void unregisterTask() {
mBackupManagerService.removeOperation(mCurrentOpToken);
}
@Override
public void run() {
Process.setThreadPriority(THREAD_PRIORITY);
mHasDataToBackup = false;
Set<String> backedUpApps = new HashSet<>();
int status = BackupTransport.TRANSPORT_OK;
try {
startTask();
while (!mQueue.isEmpty() && !mCancelled) {
String packageName = mQueue.remove(0);
try {
if (PM_PACKAGE.equals(packageName)) {
backupPm();
} else {
backupPackage(packageName);
}
setSuccessState(packageName, true);
backedUpApps.add(packageName);
} catch (AgentException e) {
setSuccessState(packageName, false);
if (e.isTransitory()) {
// We try again this package in the next backup pass.
mBackupManagerService.dataChangedImpl(packageName);
}
}
}
informTransportOfUnchangedApps(backedUpApps);
} catch (TaskException e) {
if (e.isStateCompromised()) {
mBackupManagerService.resetBackupState(mStateDirectory);
}
revertTask();
status = e.getStatus();
}
finishTask(status);
}
/**
* Tell the transport about all of the packages which have successfully backed up but
* have not informed the framework that they have new data. This allows transports to
* differentiate between packages which are not backing data up due to an error and
* packages which are not backing up data because nothing has changed.
*
* The current implementation involves creating a state file when a backup succeeds,
* on subsequent runs the existence of the file indicates the backup ran successfully
* but there was no data. If a backup fails with an error, or if the package is not
* eligible for backup by the transport any more, the status file is removed and the
* "no data" message will not be sent to the transport until another successful data
* changed backup has succeeded.
*
* @param appsBackedUp The Set of apps backed up during this run so we can exclude them
* from the list of successfully backed up apps that we signal to
* the transport have no data.
*/
private void informTransportOfUnchangedApps(Set<String> appsBackedUp) {
String[] succeedingPackages = getSucceedingPackages();
if (succeedingPackages == null) {
// Nothing is succeeding, so end early.
return;
}
int flags = BackupTransport.FLAG_DATA_NOT_CHANGED;
if (mUserInitiated) {
flags |= BackupTransport.FLAG_USER_INITIATED;
}
boolean noDataPackageEncountered = false;
try {
IBackupTransport transport =
mTransportClient.connectOrThrow("KVBT.informTransportOfEmptyBackups()");
for (String packageName : succeedingPackages) {
if (appsBackedUp.contains(packageName)) {
Log.v(TAG, "Skipping package which was backed up this time :" + packageName);
// Skip packages we backed up in this run.
continue;
}
PackageInfo packageInfo;
try {
packageInfo = mPackageManager.getPackageInfo(packageName, /* flags */ 0);
if (!isEligibleForNoDataCall(packageInfo)) {
// If the package isn't eligible any more we can forget about it and move
// on.
clearStatus(packageName);
continue;
}
} catch (PackageManager.NameNotFoundException e) {
// If the package has been uninstalled we can forget about it and move on.
clearStatus(packageName);
continue;
}
sendNoDataChangedTo(transport, packageInfo, flags);
noDataPackageEncountered = true;
}
if (noDataPackageEncountered) {
// If we've notified the transport of an unchanged package we need to
// tell it that it's seen all of the unchanged packages. We do this by
// reporting the end sentinel package as unchanged.
PackageInfo endSentinal = new PackageInfo();
endSentinal.packageName = NO_DATA_END_SENTINEL;
sendNoDataChangedTo(transport, endSentinal, flags);
}
} catch (TransportNotAvailableException | RemoteException e) {
Log.e(TAG, "Could not inform transport of all unchanged apps", e);
}
}
/** Determine if a package is eligible to be backed up to the transport */
private boolean isEligibleForNoDataCall(PackageInfo packageInfo) {
return AppBackupUtils.appIsKeyValueOnly(packageInfo)
&& AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(mTransportClient,
packageInfo.packageName, mPackageManager, mUserId);
}
/** Send the "no data changed" message to a transport for a specific package */
private void sendNoDataChangedTo(IBackupTransport transport, PackageInfo packageInfo, int flags)
throws RemoteException {
ParcelFileDescriptor pfd;
try {
pfd = ParcelFileDescriptor.open(mBlankStateFile, MODE_READ_ONLY | MODE_CREATE);
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to find blank state file, aborting unchanged apps signal.");
return;
}
try {
int result = transport.performBackup(packageInfo, pfd, flags);
if (result == BackupTransport.TRANSPORT_ERROR
|| result == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
Log.w(
TAG,
"Aborting informing transport of unchanged apps, transport" + " errored");
return;
}
transport.finishBackup();
} finally {
IoUtils.closeQuietly(pfd);
}
}
/** Get the list of package names which are marked as having previously succeeded */
private String[] getSucceedingPackages() {
File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ false);
if (stateDirectory == null) {
// getSuccessStateFileFor logs when we can't use the state area
return null;
}
return stateDirectory.list();
}
/** Sets the indicator that a package backup is succeeding */
private void setSuccessState(String packageName, boolean success) {
File successStateFile = getSuccessStateFileFor(packageName);
if (successStateFile == null) {
// The error will have been logged by getSuccessStateFileFor().
return;
}
if (successStateFile.exists() != success) {
// If there's been a change of state
if (!success) {
// Clear the status if we're now failing
clearStatus(packageName, successStateFile);
return;
}
// For succeeding packages we want the file
try {
if (!successStateFile.createNewFile()) {
Log.w(TAG, "Unable to permanently record success for " + packageName);
}
} catch (IOException e) {
Log.w(TAG, "Unable to permanently record success for " + packageName, e);
}
}
}
/** Clear the status file for a specific package */
private void clearStatus(String packageName) {
File successStateFile = getSuccessStateFileFor(packageName);
if (successStateFile == null) {
// The error will have been logged by getSuccessStateFileFor().
return;
}
clearStatus(packageName, successStateFile);
}
/** Clear the status file for a package once we have the File representation */
private void clearStatus(String packageName, File successStateFile) {
if (successStateFile.exists()) {
if (!successStateFile.delete()) {
Log.w(TAG, "Unable to remove status file for " + packageName);
}
}
}
/** Get the backup state file for a package **/
private File getSuccessStateFileFor(String packageName) {
File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ true);
return stateDirectory == null ? null : new File(stateDirectory, packageName);
}
/** The top level directory for success state files */
private File getTopLevelSuccessStateDirectory(boolean createIfMissing) {
File directory = new File(mStateDirectory, SUCCESS_STATE_SUBDIR);
if (!directory.exists() && createIfMissing && !directory.mkdirs()) {
Log.e(TAG, "Unable to create backing-up state directory");
return null;
}
return directory;
}
/** Returns transport status. */
private int sendDataToTransport(@Nullable PackageInfo packageInfo)
throws AgentException, TaskException {
try {
return sendDataToTransport();
} catch (IOException e) {
mReporter.onAgentDataError(packageInfo.packageName, e);
throw TaskException.causedBy(e);
}
}
@Override
public void execute() {}
@Override
public void operationComplete(long unusedResult) {}
private void startTask() throws TaskException {
if (mBackupManagerService.isBackupOperationInProgress()) {
mReporter.onSkipBackup();
throw TaskException.create();
}
// Unfortunately full backup task constructor registers the task with BMS, so we have to
// create it here instead of in our constructor.
mFullBackupTask = createFullBackupTask(mPendingFullBackups);
registerTask();
if (mQueue.isEmpty() && mPendingFullBackups.isEmpty()) {
mReporter.onEmptyQueueAtStart();
return;
}
// We only backup PM if it was explicitly in the queue or if it's incremental.
boolean backupPm = mQueue.remove(PM_PACKAGE) || !mNonIncremental;
if (backupPm) {
mQueue.add(0, PM_PACKAGE);
} else {
mReporter.onSkipPm();
}
mReporter.onQueueReady(mQueue);
File pmState = new File(mStateDirectory, PM_PACKAGE);
try {
IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.startTask()");
String transportName = transport.name();
if (transportName.contains("EncryptedLocalTransport")) {
// Temporary code for EiTF POC. Only supports non-incremental backups.
mNonIncremental = true;
}
mReporter.onTransportReady(transportName);
// If we haven't stored PM metadata yet, we must initialize the transport.
if (pmState.length() <= 0) {
mReporter.onInitializeTransport(transportName);
mBackupManagerService.resetBackupState(mStateDirectory);
int status = transport.initializeDevice();
mReporter.onTransportInitialized(status);
if (status != BackupTransport.TRANSPORT_OK) {
throw TaskException.stateCompromised();
}
}
} catch (TaskException e) {
throw e;
} catch (Exception e) {
mReporter.onInitializeTransportError(e);
throw TaskException.stateCompromised();
}
}
private PerformFullTransportBackupTask createFullBackupTask(List<String> packages) {
return new PerformFullTransportBackupTask(
mBackupManagerService,
mTransportClient,
/* fullBackupRestoreObserver */ null,
packages.toArray(new String[packages.size()]),
/* updateSchedule */ false,
/* runningJob */ null,
new CountDownLatch(1),
mReporter.getObserver(),
mReporter.getMonitor(),
mTaskFinishedListener,
mUserInitiated);
}
private void backupPm() throws TaskException {
mReporter.onStartPackageBackup(PM_PACKAGE);
mCurrentPackage = new PackageInfo();
mCurrentPackage.packageName = PM_PACKAGE;
try {
extractPmAgentData(mCurrentPackage);
int status = sendDataToTransport(mCurrentPackage);
cleanUpAgentForTransportStatus(status);
} catch (AgentException | TaskException e) {
mReporter.onExtractPmAgentDataError(e);
cleanUpAgentForError(e);
// PM agent failure is task failure.
throw TaskException.stateCompromised(e);
}
}
private void backupPackage(String packageName) throws AgentException, TaskException {
mReporter.onStartPackageBackup(packageName);
mCurrentPackage = getPackageForBackup(packageName);
try {
extractAgentData(mCurrentPackage);
int status = sendDataToTransport(mCurrentPackage);
cleanUpAgentForTransportStatus(status);
} catch (AgentException | TaskException e) {
cleanUpAgentForError(e);
throw e;
}
}
private PackageInfo getPackageForBackup(String packageName) throws AgentException {
final PackageInfo packageInfo;
try {
packageInfo =
mPackageManager.getPackageInfoAsUser(
packageName, PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
} catch (PackageManager.NameNotFoundException e) {
mReporter.onAgentUnknown(packageName);
throw AgentException.permanent(e);
}
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
if (!AppBackupUtils.appIsEligibleForBackup(applicationInfo, mUserId)) {
mReporter.onPackageNotEligibleForBackup(packageName);
throw AgentException.permanent();
}
if (AppBackupUtils.appGetsFullBackup(packageInfo)) {
mReporter.onPackageEligibleForFullBackup(packageName);
throw AgentException.permanent();
}
if (AppBackupUtils.appIsStopped(applicationInfo)) {
mReporter.onPackageStopped(packageName);
throw AgentException.permanent();
}
return packageInfo;
}
private IBackupAgent bindAgent(PackageInfo packageInfo) throws AgentException {
String packageName = packageInfo.packageName;
final IBackupAgent agent;
try {
agent =
mBackupManagerService.bindToAgentSynchronous(
packageInfo.applicationInfo, BACKUP_MODE_INCREMENTAL);
if (agent == null) {
mReporter.onAgentError(packageName);
throw AgentException.transitory();
}
} catch (SecurityException e) {
mReporter.onBindAgentError(packageName, e);
throw AgentException.transitory(e);
}
return agent;
}
private void finishTask(int status) {
// Mark packages that we couldn't backup as pending backup.
for (String packageName : mQueue) {
mBackupManagerService.dataChangedImpl(packageName);
}
// If backup succeeded, we just invalidated this journal. If not, we've already re-enqueued
// the packages and also don't need the journal.
if (mJournal != null && !mJournal.delete()) {
mReporter.onJournalDeleteFailed(mJournal);
}
String callerLogString = "KVBT.finishTask()";
String transportName = null;
// If the backup data was not empty, we succeeded and this is the first time
// we've done a backup, we can record the current backup dataset token.
long currentToken = mBackupManagerService.getCurrentToken();
if (mHasDataToBackup && (status == BackupTransport.TRANSPORT_OK) && (currentToken == 0)) {
try {
IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString);
transportName = transport.name();
mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet());
mBackupManagerService.writeRestoreTokens();
} catch (Exception e) {
// This will be recorded the next time we succeed.
mReporter.onSetCurrentTokenError(e);
}
}
synchronized (mQueueLock) {
mBackupManagerService.setBackupRunning(false);
if (status == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
mReporter.onTransportNotInitialized(transportName);
try {
triggerTransportInitializationLocked();
} catch (Exception e) {
mReporter.onPendingInitializeTransportError(e);
status = BackupTransport.TRANSPORT_ERROR;
}
}
}
unregisterTask();
mReporter.onTaskFinished();
if (mCancelled) {
// We acknowledge the cancel as soon as we unregister the task, allowing other backups
// to be performed.
mCancelAcknowledged.open();
}
if (!mCancelled
&& status == BackupTransport.TRANSPORT_OK
&& mFullBackupTask != null
&& !mPendingFullBackups.isEmpty()) {
mReporter.onStartFullBackup(mPendingFullBackups);
// The key-value backup has finished but not the overall backup. Full-backup task will:
// * Call mObserver.backupFinished() (which is called by mReporter below).
// * Call mTaskFinishedListener.onFinished().
// * Release the wakelock.
(new Thread(mFullBackupTask, "full-transport-requested")).start();
return;
}
if (mFullBackupTask != null) {
mFullBackupTask.unregisterTask();
}
mTaskFinishedListener.onFinished(callerLogString);
mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, status));
mBackupManagerService.getWakelock().release();
}
private int getBackupFinishedStatus(boolean cancelled, int transportStatus) {
if (cancelled) {
return BackupManager.ERROR_BACKUP_CANCELLED;
}
switch (transportStatus) {
case BackupTransport.TRANSPORT_OK:
case BackupTransport.TRANSPORT_QUOTA_EXCEEDED:
case BackupTransport.TRANSPORT_PACKAGE_REJECTED:
return BackupManager.SUCCESS;
case BackupTransport.TRANSPORT_NOT_INITIALIZED:
case BackupTransport.TRANSPORT_ERROR:
default:
return BackupManager.ERROR_TRANSPORT_ABORTED;
}
}
@GuardedBy("mQueueLock")
private void triggerTransportInitializationLocked() throws Exception {
IBackupTransport transport =
mTransportClient.connectOrThrow("KVBT.triggerTransportInitializationLocked");
mBackupManagerService.getPendingInits().add(transport.name());
deletePmStateFile();
mBackupManagerService.backupNow();
}
/** Removes PM state, triggering initialization in the next key-value task. */
private void deletePmStateFile() {
new File(mStateDirectory, PM_PACKAGE).delete();
}
/** Same as {@link #extractAgentData(PackageInfo)}, but only for PM package. */
private void extractPmAgentData(PackageInfo packageInfo) throws AgentException, TaskException {
Preconditions.checkArgument(packageInfo.packageName.equals(PM_PACKAGE));
BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent();
mAgent = IBackupAgent.Stub.asInterface(pmAgent.onBind());
extractAgentData(packageInfo, mAgent);
}
/**
* Binds to the agent and extracts its backup data. If this method returns, the data in {@code
* mBackupData} is ready to be sent to the transport, otherwise it will throw.
*
* <p>This method leaves agent resources (agent binder, files and file-descriptors) opened that
* need to be cleaned up after terminating, either successfully or exceptionally. This clean-up
* can be done with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link
* #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to
* the transport or not. It's the caller responsibility to do the clean-up or delegate it.
*/
private void extractAgentData(PackageInfo packageInfo) throws AgentException, TaskException {
mBackupManagerService.setWorkSource(new WorkSource(packageInfo.applicationInfo.uid));
try {
mAgent = bindAgent(packageInfo);
extractAgentData(packageInfo, mAgent);
} finally {
mBackupManagerService.setWorkSource(null);
}
}
/**
* Calls agent {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor,
* ParcelFileDescriptor, long, IBackupCallback, int)} and waits for the result. If this method
* returns, the data in {@code mBackupData} is ready to be sent to the transport, otherwise it
* will throw.
*
* <p>This method creates files and file-descriptors for the agent that need to be deleted and
* closed after terminating, either successfully or exceptionally. This clean-up can be done
* with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link
* #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to
* the transport or not. It's the caller responsibility to do the clean-up or delegate it.
*/
private void extractAgentData(PackageInfo packageInfo, IBackupAgent agent)
throws AgentException, TaskException {
String packageName = packageInfo.packageName;
mReporter.onExtractAgentData(packageName);
mSavedStateFile = new File(mStateDirectory, packageName);
mBackupDataFile = new File(mDataDirectory, packageName + STAGING_FILE_SUFFIX);
mNewStateFile = new File(mStateDirectory, packageName + NEW_STATE_FILE_SUFFIX);
mReporter.onAgentFilesReady(mBackupDataFile);
boolean callingAgent = false;
final RemoteResult agentResult;
try {
File savedStateFileForAgent = (mNonIncremental) ? mBlankStateFile : mSavedStateFile;
// MODE_CREATE to make an empty file if necessary
mSavedState =
ParcelFileDescriptor.open(savedStateFileForAgent, MODE_READ_ONLY | MODE_CREATE);
mBackupData =
ParcelFileDescriptor.open(
mBackupDataFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
mNewState =
ParcelFileDescriptor.open(
mNewStateFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
// TODO (b/120424138): Remove once the system user is migrated to use the per-user CE
// directory. Per-user CE directories are managed by vold.
if (mUserId == UserHandle.USER_SYSTEM) {
if (!SELinux.restorecon(mBackupDataFile)) {
mReporter.onRestoreconFailed(mBackupDataFile);
}
}
IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.extractAgentData()");
long quota = transport.getBackupQuota(packageName, /* isFullBackup */ false);
int transportFlags = transport.getTransportFlags();
callingAgent = true;
agentResult =
remoteCall(
callback ->
agent.doBackup(
mSavedState,
mBackupData,
mNewState,
quota,
callback,
transportFlags),
mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis(),
"doBackup()");
} catch (Exception e) {
mReporter.onCallAgentDoBackupError(packageName, callingAgent, e);
if (callingAgent) {
throw AgentException.transitory(e);
} else {
throw TaskException.create();
}
}
checkAgentResult(packageInfo, agentResult);
}
private void checkAgentResult(PackageInfo packageInfo, RemoteResult result)
throws AgentException, TaskException {
if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) {
// Not an explicit cancel, we need to flag it.
mCancelled = true;
mReporter.onAgentCancelled(packageInfo);
throw TaskException.create();
}
if (result == RemoteResult.FAILED_CANCELLED) {
mReporter.onAgentCancelled(packageInfo);
throw TaskException.create();
}
if (result == RemoteResult.FAILED_TIMED_OUT) {
mReporter.onAgentTimedOut(packageInfo);
throw AgentException.transitory();
}
Preconditions.checkState(result.isPresent());
long resultCode = result.get();
if (resultCode == BackupAgent.RESULT_ERROR) {
mReporter.onAgentResultError(packageInfo);
throw AgentException.transitory();
}
Preconditions.checkState(resultCode == BackupAgent.RESULT_SUCCESS);
}
private void agentFail(IBackupAgent agent, String message) {
try {
agent.fail(message);
} catch (Exception e) {
mReporter.onFailAgentError(mCurrentPackage.packageName);
}
}
// SHA-1 a byte array and return the result in hex
private String SHA1Checksum(byte[] input) {
final byte[] checksum;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
checksum = md.digest(input);
} catch (NoSuchAlgorithmException e) {
mReporter.onDigestError(e);
return "00";
}
StringBuilder string = new StringBuilder(checksum.length * 2);
for (byte item : checksum) {
string.append(Integer.toHexString(item));
}
return string.toString();
}
private void writeWidgetPayloadIfAppropriate(FileDescriptor fd, String pkgName)
throws IOException {
byte[] widgetState = AppWidgetBackupBridge.getWidgetState(pkgName, mUserId);
File widgetFile = new File(mStateDirectory, pkgName + "_widget");
boolean priorStateExists = widgetFile.exists();
if (!priorStateExists && widgetState == null) {
return;
}
mReporter.onWriteWidgetData(priorStateExists, widgetState);
// if the new state is not null, we might need to compare checksums to
// determine whether to update the widget blob in the archive. If the
// widget state *is* null, we know a priori at this point that we simply
// need to commit a deletion for it.
String newChecksum = null;
if (widgetState != null) {
newChecksum = SHA1Checksum(widgetState);
if (priorStateExists) {
final String priorChecksum;
try (
FileInputStream fin = new FileInputStream(widgetFile);
DataInputStream in = new DataInputStream(fin)
) {
priorChecksum = in.readUTF();
}
if (Objects.equals(newChecksum, priorChecksum)) {
// Same checksum => no state change => don't rewrite the widget data
return;
}
}
} // else widget state *became* empty, so we need to commit a deletion
BackupDataOutput out = new BackupDataOutput(fd);
if (widgetState != null) {
try (
FileOutputStream fout = new FileOutputStream(widgetFile);
DataOutputStream stateOut = new DataOutputStream(fout)
) {
stateOut.writeUTF(newChecksum);
}
out.writeEntityHeader(KEY_WIDGET_STATE, widgetState.length);
out.writeEntityData(widgetState, widgetState.length);
} else {
// Widget state for this app has been removed; commit a deletion
out.writeEntityHeader(KEY_WIDGET_STATE, -1);
widgetFile.delete();
}
}
/** Returns transport status. */
private int sendDataToTransport() throws AgentException, TaskException, IOException {
Preconditions.checkState(mBackupData != null);
checkBackupData(mCurrentPackage.applicationInfo, mBackupDataFile);
String packageName = mCurrentPackage.packageName;
writeWidgetPayloadIfAppropriate(mBackupData.getFileDescriptor(), packageName);
boolean nonIncremental = mSavedStateFile.length() == 0;
int status = transportPerformBackup(mCurrentPackage, mBackupDataFile, nonIncremental);
handleTransportStatus(status, packageName, mBackupDataFile.length());
return status;
}
private int transportPerformBackup(
PackageInfo packageInfo, File backupDataFile, boolean nonIncremental)
throws TaskException {
String packageName = packageInfo.packageName;
long size = backupDataFile.length();
if (size <= 0) {
mReporter.onEmptyData(packageInfo);
return BackupTransport.TRANSPORT_OK;
}
mHasDataToBackup = true;
int status;
try (ParcelFileDescriptor backupData =
ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) {
IBackupTransport transport =
mTransportClient.connectOrThrow("KVBT.transportPerformBackup()");
mReporter.onTransportPerformBackup(packageName);
int flags = getPerformBackupFlags(mUserInitiated, nonIncremental);
status = transport.performBackup(packageInfo, backupData, flags);
if (status == BackupTransport.TRANSPORT_OK) {
status = transport.finishBackup();
} else if (status == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
mReporter.onTransportNotInitialized(transport.name());
}
} catch (Exception e) {
mReporter.onPackageBackupTransportError(packageName, e);
throw TaskException.causedBy(e);
}
if (nonIncremental && status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
mReporter.onPackageBackupNonIncrementalAndNonIncrementalRequired(packageName);
throw TaskException.create();
}
return status;
}
private void handleTransportStatus(int status, String packageName, long size)
throws TaskException, AgentException {
if (status == BackupTransport.TRANSPORT_OK) {
mReporter.onPackageBackupComplete(packageName, size);
return;
}
if (status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
mReporter.onPackageBackupNonIncrementalRequired(mCurrentPackage);
// Immediately retry the current package.
mQueue.add(0, packageName);
return;
}
if (status == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
mReporter.onPackageBackupRejected(packageName);
throw AgentException.permanent();
}
if (status == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
mReporter.onPackageBackupQuotaExceeded(packageName);
agentDoQuotaExceeded(mAgent, packageName, size);
throw AgentException.permanent();
}
// Any other error here indicates a transport-level failure.
mReporter.onPackageBackupTransportFailure(packageName);
throw TaskException.forStatus(status);
}
private void agentDoQuotaExceeded(@Nullable IBackupAgent agent, String packageName, long size) {
if (agent != null) {
try {
IBackupTransport transport =
mTransportClient.connectOrThrow("KVBT.agentDoQuotaExceeded()");
long quota = transport.getBackupQuota(packageName, false);
remoteCall(
callback -> agent.doQuotaExceeded(size, quota, callback),
mAgentTimeoutParameters.getQuotaExceededTimeoutMillis(),
"doQuotaExceeded()");
} catch (Exception e) {
mReporter.onAgentDoQuotaExceededError(e);
}
}
}
/**
* For system apps and pseudo-apps never throws. For regular apps throws {@link AgentException}
* if {@code backupDataFile} has any protected keys, also crashing the app.
*/
private void checkBackupData(@Nullable ApplicationInfo applicationInfo, File backupDataFile)
throws IOException, AgentException {
if (applicationInfo == null || (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
// System apps and pseudo-apps can write what they want.
return;
}
try (ParcelFileDescriptor backupData =
ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) {
BackupDataInput backupDataInput = new BackupDataInput(backupData.getFileDescriptor());
while (backupDataInput.readNextHeader()) {
String key = backupDataInput.getKey();
if (key != null && key.charAt(0) >= 0xff00) {
mReporter.onAgentIllegalKey(mCurrentPackage, key);
// Crash them if they wrote any protected keys.
agentFail(mAgent, "Illegal backup key: " + key);
throw AgentException.permanent();
}
backupDataInput.skipEntityData();
}
}
}
private int getPerformBackupFlags(boolean userInitiated, boolean nonIncremental) {
int userInitiatedFlag = userInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
int incrementalFlag =
nonIncremental
? BackupTransport.FLAG_NON_INCREMENTAL
: BackupTransport.FLAG_INCREMENTAL;
return userInitiatedFlag | incrementalFlag;
}
/**
* Cancels this task.
*
* <p>After this method returns this task won't be registered in {@link BackupManagerService}
* anymore, which means there will be no backups running unless there is a racy request
* coming from another thread in between. As a consequence there will be no more calls to the
* transport originated from this task.
*
* <p>If this method is executed while an agent is performing a backup, we will stop waiting for
* it, disregard its backup data and finalize the task. However, if this method is executed in
* between agent calls, the backup data of the last called agent will be sent to
* the transport and we will not consider the next agent (nor the rest of the queue), proceeding
* to finalize the backup.
*
* <p>Note: This method is inherently racy since there are no guarantees about how much of the
* task will be executed after you made the call.
*
* @param cancelAll MUST be {@code true}. Will be removed.
*/
@Override
public void handleCancel(boolean cancelAll) {
// This is called in a thread different from the one that executes method run().
Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task");
markCancel();
waitCancel();
}
/** Marks this task as cancelled and tries to stop any ongoing agent call. */
@VisibleForTesting
public void markCancel() {
mReporter.onCancel();
mCancelled = true;
RemoteCall pendingCall = mPendingCall;
if (pendingCall != null) {
pendingCall.cancel();
}
}
/** Waits for this task to be cancelled after call to {@link #markCancel()}. */
@VisibleForTesting
public void waitCancel() {
mCancelAcknowledged.block();
}
private void revertTask() {
mReporter.onRevertTask();
long delay;
try {
IBackupTransport transport =
mTransportClient.connectOrThrow("KVBT.revertTask()");
delay = transport.requestBackupTime();
} catch (Exception e) {
mReporter.onTransportRequestBackupTimeError(e);
// Use the scheduler's default.
delay = 0;
}
KeyValueBackupJob.schedule(mBackupManagerService.getUserId(),
mBackupManagerService.getContext(), delay, mBackupManagerService.getConstants());
for (String packageName : mOriginalQueue) {
mBackupManagerService.dataChangedImpl(packageName);
}
}
/**
* Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} for exceptional
* case.
*
* <p>Note: Declaring exception parameter so that the caller only calls this when an exception
* is thrown.
*/
private void cleanUpAgentForError(BackupException exception) {
cleanUpAgent(StateTransaction.DISCARD_NEW);
}
/**
* Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} according to
* transport status returned in {@link #sendDataToTransport(PackageInfo)}.
*/
private void cleanUpAgentForTransportStatus(int status) {
switch (status) {
case BackupTransport.TRANSPORT_OK:
cleanUpAgent(StateTransaction.COMMIT_NEW);
break;
case BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED:
cleanUpAgent(StateTransaction.DISCARD_ALL);
break;
default:
// All other transport statuses are properly converted to agent or task exceptions.
throw new AssertionError();
}
}
private void cleanUpAgent(@StateTransaction int stateTransaction) {
applyStateTransaction(stateTransaction);
if (mBackupDataFile != null) {
mBackupDataFile.delete();
}
mBlankStateFile.delete();
mSavedStateFile = null;
mBackupDataFile = null;
mNewStateFile = null;
tryCloseFileDescriptor(mSavedState, "old state");
tryCloseFileDescriptor(mBackupData, "backup data");
tryCloseFileDescriptor(mNewState, "new state");
mSavedState = null;
mBackupData = null;
mNewState = null;
// For PM metadata (for which applicationInfo is null) there is no agent-bound state.
if (mCurrentPackage.applicationInfo != null) {
mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo);
}
mAgent = null;
}
private void applyStateTransaction(@StateTransaction int stateTransaction) {
switch (stateTransaction) {
case StateTransaction.COMMIT_NEW:
mNewStateFile.renameTo(mSavedStateFile);
break;
case StateTransaction.DISCARD_NEW:
if (mNewStateFile != null) {
mNewStateFile.delete();
}
break;
case StateTransaction.DISCARD_ALL:
mSavedStateFile.delete();
mNewStateFile.delete();
break;
default:
throw new IllegalArgumentException("Unknown state transaction " + stateTransaction);
}
}
private void tryCloseFileDescriptor(@Nullable Closeable closeable, String logName) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
mReporter.onCloseFileDescriptorError(logName);
}
}
}
private RemoteResult remoteCall(
RemoteCallable<IBackupCallback> remoteCallable, long timeoutMs, String logIdentifier)
throws RemoteException {
mPendingCall = new RemoteCall(mCancelled, remoteCallable, timeoutMs);
RemoteResult result = mPendingCall.call();
mReporter.onRemoteCallReturned(result, logIdentifier);
mPendingCall = null;
return result;
}
@IntDef({
StateTransaction.COMMIT_NEW,
StateTransaction.DISCARD_NEW,
StateTransaction.DISCARD_ALL,
})
@Retention(RetentionPolicy.SOURCE)
private @interface StateTransaction {
int COMMIT_NEW = 0;
int DISCARD_NEW = 1;
int DISCARD_ALL = 2;
}
}