| /* |
| * 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.BackupEligibilityRules; |
| |
| 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()}/<transport> |
| * <li>State file: {@link |
| * UserBackupManagerService#getBaseStateDir()}/<transport>/<package><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()}/<package>.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 |
| * <state file>.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, |
| BackupEligibilityRules backupEligibilityRules) { |
| KeyValueBackupReporter reporter = |
| new KeyValueBackupReporter(backupManagerService, observer, monitor); |
| KeyValueBackupTask task = |
| new KeyValueBackupTask( |
| backupManagerService, |
| transportClient, |
| transportDirName, |
| queue, |
| dataChangedJournal, |
| reporter, |
| listener, |
| pendingFullBackups, |
| userInitiated, |
| nonIncremental, |
| backupEligibilityRules); |
| 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; |
| private final BackupEligibilityRules mBackupEligibilityRules; |
| |
| @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, |
| BackupEligibilityRules backupEligibilityRules) { |
| 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(); |
| mBackupEligibilityRules = backupEligibilityRules; |
| } |
| |
| 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 mBackupEligibilityRules.appIsKeyValueOnly(packageInfo) |
| && mBackupEligibilityRules.appIsRunningAndEligibleForBackupWithTransport( |
| mTransportClient, packageInfo.packageName); |
| } |
| |
| /** 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, |
| mBackupEligibilityRules); |
| } |
| |
| private void backupPm() throws TaskException { |
| mReporter.onStartPackageBackup(PM_PACKAGE); |
| mCurrentPackage = new PackageInfo(); |
| mCurrentPackage.packageName = PM_PACKAGE; |
| try { |
| // If we can't even extractPmAgentData(), then we treat the local state as |
| // compromised, just in case. This means that we will clear data and will |
| // start from a clean slate in the next attempt. It's not clear whether that's |
| // the right thing to do, but matches what we have historically done. |
| try { |
| extractPmAgentData(mCurrentPackage); |
| } catch (TaskException e) { |
| throw TaskException.stateCompromised(e); // force stateCompromised |
| } |
| // During sendDataToTransport, we generally trust any thrown TaskException |
| // about whether stateCompromised because those are likely transient; |
| // clearing state for those would have the potential to lead to cascading |
| // failures, as discussed in http://b/144030477. |
| // For specific status codes (e.g. TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED), |
| // cleanUpAgentForTransportStatus() or theoretically handleTransportStatus() |
| // still have the opportunity to perform additional clean-up tasks. |
| int status = sendDataToTransport(mCurrentPackage); |
| cleanUpAgentForTransportStatus(status); |
| } catch (AgentException | TaskException e) { |
| mReporter.onExtractPmAgentDataError(e); |
| cleanUpAgentForError(e); |
| if (e instanceof TaskException) { |
| throw (TaskException) e; |
| } else { |
| throw TaskException.stateCompromised(e); // PM agent failure is task failure. |
| } |
| } |
| } |
| |
| 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 (!mBackupEligibilityRules.appIsEligibleForBackup(applicationInfo)) { |
| mReporter.onPackageNotEligibleForBackup(packageName); |
| throw AgentException.permanent(); |
| } |
| if (mBackupEligibilityRules.appGetsFullBackup(packageInfo)) { |
| mReporter.onPackageEligibleForFullBackup(packageName); |
| throw AgentException.permanent(); |
| } |
| if (mBackupEligibilityRules.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, |
| mBackupEligibilityRules.getOperationType()); |
| 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.makeMetadataAgentWithEligibilityRules(mBackupEligibilityRules); |
| 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; |
| } |
| } |