content: /*
   * 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 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.TransportManager;
  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.utils.AppBackupUtils;
  
  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.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.List;
  import java.util.Objects;
  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 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;
      @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 TransportManager mTransportManager;
      private final TransportClient mTransportClient;
      private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
      private final KeyValueBackupReporter mReporter;
      private final OnTaskFinishedListener mTaskFinishedListener;
      private final boolean mUserInitiated;
      private final boolean mNonIncremental;
      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;
  
      /**
       * 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;
          mTransportManager = backupManagerService.getTransportManager();
          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 =
                  Preconditions.checkNotNull(
                          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;
  
          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);
                      }
                  } catch (AgentException e) {
                      if (e.isTransitory()) {
                          // We try again this package in the next backup pass.
                          mBackupManagerService.dataChangedImpl(packageName);
                      }
                  }
              }
          } catch (TaskException e) {
              if (e.isStateCompromised()) {
                  mBackupManagerService.resetBackupState(mStateDirectory);
              }
              revertTask();
              status = e.getStatus();
          }
          finishTask(status);
      }
  
      /** 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();
              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()";
  
          // 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);
                  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();
                  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();
              }
          } 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;
      }
  }
  