From 75f73f07fe8a172aa0f226484506758c34b55005 Mon Sep 17 00:00:00 2001 From: Bernardo Rufino Date: Thu, 2 Aug 2018 09:39:39 +0100 Subject: [KV] Move KVBT to its own package And related classes. Test: atest RunFrameworksServicesRoboTests Test: adb shell bmgr backupnow com.google.android.apps.backupapp Test: 1. adb shell bmgr backup com.google.android.apps.backupapp 2. adb shell bmgr run Test adb shell cmd jobscheduler run -f android 20537 Change-Id: I1aba694786a4e2a5add5c388cd095700ade1ace9 --- services/art-profile | 28 +- .../server/backup/BackupManagerService.java | 2 +- .../server/backup/internal/BackupHandler.java | 2 + .../server/backup/internal/BackupRequest.java | 51 - .../server/backup/internal/BackupState.java | 12 - .../server/backup/internal/KeyValueBackupTask.java | 1354 ------------- .../server/backup/keyvalue/BackupRequest.java | 51 + .../server/backup/keyvalue/KeyValueBackupTask.java | 1364 +++++++++++++ .../server/backup/BackupManagerServiceTest.java | 2 +- .../server/backup/KeyValueBackupTaskTest.java | 1989 ------------------- .../backup/keyvalue/KeyValueBackupTaskTest.java | 1996 ++++++++++++++++++++ .../testing/shadows/ShadowKeyValueBackupTask.java | 5 +- 12 files changed, 3431 insertions(+), 3425 deletions(-) delete mode 100644 services/backup/java/com/android/server/backup/internal/BackupRequest.java delete mode 100644 services/backup/java/com/android/server/backup/internal/BackupState.java delete mode 100644 services/backup/java/com/android/server/backup/internal/KeyValueBackupTask.java create mode 100644 services/backup/java/com/android/server/backup/keyvalue/BackupRequest.java create mode 100644 services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java delete mode 100644 services/robotests/src/com/android/server/backup/KeyValueBackupTaskTest.java create mode 100644 services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java diff --git a/services/art-profile b/services/art-profile index 24964f32f63e..cbc00ea53427 100644 --- a/services/art-profile +++ b/services/art-profile @@ -9258,24 +9258,24 @@ PLcom/android/server/backup/internal/BackupRequest;->toString()Ljava/lang/String PLcom/android/server/backup/internal/BackupState;->(Ljava/lang/String;I)V PLcom/android/server/backup/internal/BackupState;->values()[Lcom/android/server/backup/internal/BackupState; PLcom/android/server/backup/internal/Operation;->(ILcom/android/server/backup/BackupRestoreTask;I)V -PLcom/android/server/backup/internal/KeyValueBackupTask;->(Lcom/android/server/backup/BackupManagerService;Lcom/android/server/backup/transport/TransportClient;Ljava/lang/String;Ljava/util/List;Lcom/android/server/backup/DataChangedJournal;Landroid/app/backup/IBackupObserver;Landroid/app/backup/IBackupManagerMonitor;Lcom/android/server/backup/internal/OnTaskFinishedListener;Ljava/util/List;ZZ)V -PLcom/android/server/backup/internal/KeyValueBackupTask;->backupPm()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->beginBackup()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->clearAgentState()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->execute()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->executeNextState(Lcom/android/server/backup/internal/BackupState;)V -PLcom/android/server/backup/internal/KeyValueBackupTask;->finalizeBackup()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->invokeAgentForBackup(Ljava/lang/String;Landroid/app/IBackupAgent;)I -PLcom/android/server/backup/internal/KeyValueBackupTask;->invokeNextAgent()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->operationComplete(J)V -PLcom/android/server/backup/internal/KeyValueBackupTask;->registerTask()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->revertAndEndBackup()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->unregisterTask()V -PLcom/android/server/backup/internal/KeyValueBackupTask;->writeWidgetPayloadIfAppropriate(Ljava/io/FileDescriptor;Ljava/lang/String;)V PLcom/android/server/backup/internal/ProvisionedObserver;->(Lcom/android/server/backup/BackupManagerService;Landroid/os/Handler;)V PLcom/android/server/backup/internal/RunBackupReceiver;->(Lcom/android/server/backup/BackupManagerService;)V PLcom/android/server/backup/internal/RunBackupReceiver;->onReceive(Landroid/content/Context;Landroid/content/Intent;)V PLcom/android/server/backup/internal/RunInitializeReceiver;->(Lcom/android/server/backup/BackupManagerService;)V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->(Lcom/android/server/backup/BackupManagerService;Lcom/android/server/backup/transport/TransportClient;Ljava/lang/String;Ljava/util/List;Lcom/android/server/backup/DataChangedJournal;Landroid/app/backup/IBackupObserver;Landroid/app/backup/IBackupManagerMonitor;Lcom/android/server/backup/internal/OnTaskFinishedListener;Ljava/util/List;ZZ)V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->backupPm()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->beginBackup()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->clearAgentState()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->execute()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->executeNextState(Lcom/android/server/backup/internal/BackupState;)V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->finalizeBackup()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->invokeAgentForBackup(Ljava/lang/String;Landroid/app/IBackupAgent;)I +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->invokeNextAgent()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->operationComplete(J)V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->registerTask()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->revertAndEndBackup()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->unregisterTask()V +PLcom/android/server/backup/keyvalue/KeyValueBackupTask;->writeWidgetPayloadIfAppropriate(Ljava/io/FileDescriptor;Ljava/lang/String;)V PLcom/android/server/backup/transport/-$$Lambda$TransportClient$ciIUj0x0CRg93UETUpy2FB5aqCQ;->(Lcom/android/server/backup/transport/TransportClient;Lcom/android/server/backup/transport/TransportConnectionListener;Lcom/android/internal/backup/IBackupTransport;)V PLcom/android/server/backup/transport/-$$Lambda$TransportClient$ciIUj0x0CRg93UETUpy2FB5aqCQ;->run()V PLcom/android/server/backup/transport/-$$Lambda$TransportClient$uc3fygwQjQIS_JT7mlt-yMBfJcE;->(Ljava/util/concurrent/CompletableFuture;)V diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index ec27da90106a..c26ac17c0c0e 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -104,7 +104,7 @@ import com.android.server.SystemService; import com.android.server.backup.fullbackup.FullBackupEntry; import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; import com.android.server.backup.internal.BackupHandler; -import com.android.server.backup.internal.BackupRequest; +import com.android.server.backup.keyvalue.BackupRequest; import com.android.server.backup.internal.ClearDataObserver; import com.android.server.backup.internal.OnTaskFinishedListener; import com.android.server.backup.internal.Operation; diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 6e96fe0ba4ba..2722729b9f4c 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -41,6 +41,8 @@ import com.android.server.backup.DataChangedJournal; import com.android.server.backup.TransportManager; import com.android.server.backup.fullbackup.PerformAdbBackupTask; import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; +import com.android.server.backup.keyvalue.BackupRequest; +import com.android.server.backup.keyvalue.KeyValueBackupTask; import com.android.server.backup.params.AdbBackupParams; import com.android.server.backup.params.AdbParams; import com.android.server.backup.params.AdbRestoreParams; diff --git a/services/backup/java/com/android/server/backup/internal/BackupRequest.java b/services/backup/java/com/android/server/backup/internal/BackupRequest.java deleted file mode 100644 index 01e43851661d..000000000000 --- a/services/backup/java/com/android/server/backup/internal/BackupRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.internal; - -import java.util.Objects; - -/** - * Set of backup services that have pending changes. - */ -public class BackupRequest { - public String packageName; - - public BackupRequest(String pkgName) { - packageName = pkgName; - } - - public String toString() { - return "BackupRequest{pkg=" + packageName + "}"; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BackupRequest)) { - return false; - } - BackupRequest that = (BackupRequest) o; - return Objects.equals(packageName, that.packageName); - } - - @Override - public int hashCode() { - return Objects.hash(packageName); - } -} diff --git a/services/backup/java/com/android/server/backup/internal/BackupState.java b/services/backup/java/com/android/server/backup/internal/BackupState.java deleted file mode 100644 index 320b55525ea4..000000000000 --- a/services/backup/java/com/android/server/backup/internal/BackupState.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.server.backup.internal; - -/** - * Current state of the backup. - */ -enum BackupState { - INITIAL, - BACKUP_PM, - RUNNING_QUEUE, - CANCELLED, - FINAL -} diff --git a/services/backup/java/com/android/server/backup/internal/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/internal/KeyValueBackupTask.java deleted file mode 100644 index 54b8d1fa59af..000000000000 --- a/services/backup/java/com/android/server/backup/internal/KeyValueBackupTask.java +++ /dev/null @@ -1,1354 +0,0 @@ -/* - * 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.internal; - -import static com.android.server.backup.BackupManagerService.DEBUG_BACKUP_TRACE; -import static com.android.server.backup.BackupManagerService.KEY_WIDGET_STATE; -import static com.android.server.backup.BackupManagerService.OP_PENDING; -import static com.android.server.backup.BackupManagerService.OP_TYPE_BACKUP; -import static com.android.server.backup.BackupManagerService.PACKAGE_MANAGER_SENTINEL; - -import android.annotation.Nullable; -import android.app.ApplicationThreadConstants; -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.BackupManagerMonitor; -import android.app.backup.BackupTransport; -import android.app.backup.IBackupCallback; -import android.app.backup.IBackupManager; -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.system.ErrnoException; -import android.system.Os; -import android.util.EventLog; -import android.util.Pair; -import android.util.Slog; - -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.EventLogTags; -import com.android.server.backup.BackupAgentTimeoutParameters; -import com.android.server.backup.BackupManagerService; -import com.android.server.backup.BackupRestoreTask; -import com.android.server.backup.DataChangedJournal; -import com.android.server.backup.KeyValueBackupJob; -import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; -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 com.android.server.backup.utils.BackupManagerMonitorUtils; -import com.android.server.backup.utils.BackupObserverUtils; - -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.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. - * - *

A few definitions: - * - *

    - *
  • State directory: {@link BackupManagerService#getBaseStateDir()}/<transport> - *
  • State file: {@link - * BackupManagerService#getBaseStateDir()}/<transport>/<package>
    - * Represents the state of the backup data for a specific package in the current dataset. - *
  • Stage directory: {@link BackupManagerService#getDataDir()} - *
  • Stage file: {@link BackupManagerService#getDataDir()}/<package>.data
    - * Contains staged data that the agents wrote via {@link BackupDataOutput}, to be transmitted - * to the transport. - *
- * - * If there is no PackageManager (PM) pseudo-package state file in the state directory, the - * specified transport will be initialized with {@link IBackupTransport#initializeDevice()}. - * - *

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. - * - *

Before starting, this task will register itself in {@link BackupManagerService} current - * operations. - * - *

In summary, this task will for each package: - * - *

    - *
  • Bind to its {@link IBackupAgent}. - *
  • Request transport quota and flags. - *
  • Call {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor, - * ParcelFileDescriptor, long, int, IBackupManager, 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: - *
      - *
    • Agent response. - *
    • Agent time-out (specified via {@link - * BackupManagerService#getAgentTimeoutParameters()}. - *
    • External cancellation or thread interrupt. - *
    - *
  • Unbind the agent. - *
  • Assuming agent response, send the staged data that the agent wrote to disk to the transport - * via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}. - *
  • Call {@link IBackupTransport#finishBackup()} if previous call was successful. - *
  • 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. - *
  • Delete the stage file. - *
- * - * In the end, this task will: - * - *
    - *
  • Mark data-changed for the remaining packages in the queue (skipped packages). - *
  • Delete the {@link DataChangedJournal} provided. Note that this should not be the current - * journal. - *
  • Set {@link BackupManagerService} current token as {@link - * IBackupTransport#getCurrentRestoreSet()}, if applicable. - *
  • Add the transport to the list of transports pending initialization ({@link - * BackupManagerService#getPendingInits()}) and kick-off initialization if the transport ever - * returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}. - *
  • Unregister the task in current operations. - *
  • Release the wakelock. - *
  • Kick-off {@link PerformFullTransportBackupTask} if a list of full-backup packages was - * provided. - *
- * - * 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. - * - *

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 = "KeyValueBackupTask"; - private static final boolean DEBUG = BackupManagerService.DEBUG || true; - private static final boolean MORE_DEBUG = BackupManagerService.MORE_DEBUG || false; - 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"; - @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 BackupManagerService} system service. - * @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 packages that will be backed-up, in the form of {@link - * BackupRequest}. - * @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( - BackupManagerService backupManagerService, - TransportClient transportClient, - String transportDirName, - List queue, - @Nullable DataChangedJournal dataChangedJournal, - IBackupObserver observer, - IBackupManagerMonitor monitor, - @Nullable OnTaskFinishedListener listener, - List pendingFullBackups, - boolean userInitiated, - boolean nonIncremental) { - KeyValueBackupTask task = - new KeyValueBackupTask( - backupManagerService, - transportClient, - transportDirName, - queue, - dataChangedJournal, - observer, - monitor, - listener, - pendingFullBackups, - userInitiated, - nonIncremental); - Thread thread = new Thread(task, "key-value-backup-" + THREAD_COUNT.incrementAndGet()); - if (DEBUG) { - Slog.d(TAG, "Spinning thread " + thread.getName()); - } - thread.start(); - return task; - } - - private final BackupManagerService mBackupManagerService; - private final TransportClient mTransportClient; - private final BackupAgentTimeoutParameters mAgentTimeoutParameters; - private final IBackupObserver mObserver; - private final OnTaskFinishedListener mListener; - private final boolean mUserInitiated; - private final boolean mNonIncremental; - private final int mCurrentOpToken; - private final File mStateDir; - private final List mOriginalQueue; - private final List mQueue; - private final List mPendingFullBackups; - @Nullable private final DataChangedJournal mJournal; - private IBackupManagerMonitor mMonitor; - @Nullable private PerformFullTransportBackupTask mFullBackupTask; - - private IBackupAgent mAgentBinder; - private PackageInfo mCurrentPackage; - private File mSavedStateFile; - private File mBackupDataFile; - private File mNewStateFile; - private ParcelFileDescriptor mSavedState; - private ParcelFileDescriptor mBackupData; - private ParcelFileDescriptor mNewState; - private int mStatus; - - /** - * 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( - BackupManagerService backupManagerService, - TransportClient transportClient, - String transportDirName, - List queue, - @Nullable DataChangedJournal journal, - IBackupObserver observer, - IBackupManagerMonitor monitor, - @Nullable OnTaskFinishedListener listener, - List pendingFullBackups, - boolean userInitiated, - boolean nonIncremental) { - mBackupManagerService = backupManagerService; - mTransportClient = transportClient; - mOriginalQueue = queue; - // We need to retain the original queue contents in case of transport failure - mQueue = new ArrayList<>(mOriginalQueue); - mJournal = journal; - mObserver = observer; - mMonitor = monitor; - mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP; - mPendingFullBackups = pendingFullBackups; - mUserInitiated = userInitiated; - mNonIncremental = nonIncremental; - mAgentTimeoutParameters = - Preconditions.checkNotNull( - backupManagerService.getAgentTimeoutParameters(), - "Timeout parameters cannot be null"); - mStateDir = new File(backupManagerService.getBaseStateDir(), transportDirName); - mCurrentOpToken = backupManagerService.generateRandomIntegerToken(); - } - - 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); - - BackupState state = beginBackup(); - while (state == BackupState.RUNNING_QUEUE || state == BackupState.BACKUP_PM) { - if (mCancelled) { - state = BackupState.CANCELLED; - } - switch (state) { - case BACKUP_PM: - state = backupPm(); - break; - case RUNNING_QUEUE: - Pair stateAndResult = invokeNextAgent(); - state = stateAndResult.first; - if (state == null) { - state = processAgentInvocation(stateAndResult.second); - } - break; - } - } - if (state == BackupState.CANCELLED) { - finalizeCancelledBackup(); - } else { - finalizeBackup(); - } - } - - private BackupState processAgentInvocation(RemoteResult result) { - if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) { - // Not an explicit cancel, we need to flag it - mCancelled = true; - handleAgentCancelled(); - return BackupState.CANCELLED; - } - if (result == RemoteResult.FAILED_CANCELLED) { - handleAgentCancelled(); - return BackupState.CANCELLED; - } - if (result == RemoteResult.FAILED_TIMED_OUT) { - handleAgentTimeout(); - return BackupState.RUNNING_QUEUE; - } - Preconditions.checkState(result.succeeded()); - return handleAgentResult(result.get()); - } - - @Override - public void execute() {} - - @Override - public void operationComplete(long unusedResult) {} - - private BackupState beginBackup() { - if (DEBUG_BACKUP_TRACE) { - mBackupManagerService.clearBackupTrace(); - StringBuilder b = new StringBuilder(256); - b.append("beginBackup: ["); - for (BackupRequest req : mOriginalQueue) { - b.append(' '); - b.append(req.packageName); - } - b.append(" ]"); - mBackupManagerService.addBackupTrace(b.toString()); - } - synchronized (mBackupManagerService.getCurrentOpLock()) { - if (mBackupManagerService.isBackupOperationInProgress()) { - if (DEBUG) { - Slog.d(TAG, "Skipping backup since one is already in progress"); - } - mBackupManagerService.addBackupTrace("Skipped. Backup already in progress."); - return BackupState.FINAL; - } - } - - String[] fullBackups = mPendingFullBackups.toArray(new String[mPendingFullBackups.size()]); - mFullBackupTask = - new PerformFullTransportBackupTask( - mBackupManagerService, - mTransportClient, - /* fullBackupRestoreObserver */ null, - fullBackups, - /* updateSchedule */ false, - /* runningJob */ null, - new CountDownLatch(1), - mObserver, - mMonitor, - mListener, - mUserInitiated); - registerTask(); - mBackupManagerService.addBackupTrace("STATE => INITIAL"); - - mAgentBinder = null; - mStatus = BackupTransport.TRANSPORT_OK; - - // Sanity check: if the queue is empty we have no work to do. - if (mOriginalQueue.isEmpty() && mPendingFullBackups.isEmpty()) { - Slog.w(TAG, "Backup begun with an empty queue, nothing to do."); - mBackupManagerService.addBackupTrace("queue empty at begin"); - return BackupState.FINAL; - } - - // When the transport is forcing non-incremental key/value payloads, we send the - // metadata only if it explicitly asks for it. - boolean skipPm = mNonIncremental; - - // The app metadata pseudopackage might also be represented in the - // backup queue if apps have been added/removed since the last time - // we performed a backup. Drop it from the working queue now that - // we're committed to evaluating it for backup regardless. - for (int i = 0; i < mQueue.size(); i++) { - if (PACKAGE_MANAGER_SENTINEL.equals(mQueue.get(i).packageName)) { - if (MORE_DEBUG) { - Slog.i(TAG, "PM metadata in queue, removing"); - } - mQueue.remove(i); - skipPm = false; - break; - } - } - - if (DEBUG) { - Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); - } - File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); - try { - IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.beginBackup()"); - String transportName = transport.name(); - EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); - - // If we haven't stored package manager metadata yet, we must init the transport. - if (pmState.length() <= 0) { - Slog.i(TAG, "Initializing transport and resetting backup state"); - mBackupManagerService.addBackupTrace("initializing transport " + transportName); - mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. - mStatus = transport.initializeDevice(); - - mBackupManagerService.addBackupTrace("transport.initializeDevice() == " + mStatus); - if (mStatus == BackupTransport.TRANSPORT_OK) { - EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); - } else { - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); - Slog.e(TAG, "Transport error in initializeDevice()"); - } - } - } catch (Exception e) { - Slog.e(TAG, "Error during initialization", e); - mBackupManagerService.addBackupTrace("Exception in backup thread during init: " + e); - mStatus = BackupTransport.TRANSPORT_ERROR; - } - mBackupManagerService.addBackupTrace("exiting prelim: " + mStatus); - - if (mStatus != BackupTransport.TRANSPORT_OK) { - // if things went wrong at this point, we need to - // restage everything and try again later. - mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. - return BackupState.FINAL; - } - - if (skipPm) { - Slog.d(TAG, "Skipping backup of PM metadata"); - return BackupState.RUNNING_QUEUE; - } - - return BackupState.BACKUP_PM; - } - - private BackupState backupPm() { - RemoteResult agentResult = null; - BackupState nextState; - try { - // The package manager doesn't have a proper etc, but since it's running - // here in the system process we can just set up its agent directly and use a synthetic - // BackupRequest. - BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent(); - Pair statusAndResult = - invokeAgentForBackup( - PACKAGE_MANAGER_SENTINEL, - IBackupAgent.Stub.asInterface(pmAgent.onBind())); - mStatus = statusAndResult.first; - agentResult = statusAndResult.second; - - mBackupManagerService.addBackupTrace("PM agent invoke: " + mStatus); - } catch (Exception e) { - Slog.e(TAG, "Error during PM metadata backup", e); - mBackupManagerService.addBackupTrace("Exception in backup thread during pm: " + e); - mStatus = BackupTransport.TRANSPORT_ERROR; - } - mBackupManagerService.addBackupTrace("exiting backupPm: " + mStatus); - - if (mStatus == BackupTransport.TRANSPORT_OK) { - Preconditions.checkNotNull(agentResult); - nextState = processAgentInvocation(agentResult); - } else { - // if things went wrong at this point, we need to - // restage everything and try again later. - mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. - nextState = BackupState.FINAL; - } - - return nextState; - } - - /** - * Returns either: - * - *

    - *
  • (next state, {@code null}): In case we failed to call the agent. - *
  • ({@code null}, agent result): In case we successfully called the agent. - *
- */ - private Pair invokeNextAgent() { - mStatus = BackupTransport.TRANSPORT_OK; - mBackupManagerService.addBackupTrace("invoke q=" + mQueue.size()); - - // Sanity check that we have work to do. If not, skip to the end where - // we reestablish the wakelock invariants etc. - if (mQueue.isEmpty()) { - if (MORE_DEBUG) { - Slog.i(TAG, "Queue now empty"); - } - return Pair.create(BackupState.FINAL, null); - } - - // pop the entry we're going to process on this step - BackupRequest request = mQueue.remove(0); - - Slog.d(TAG, "Starting key-value backup of " + request); - mBackupManagerService.addBackupTrace("launch agent for " + request.packageName); - - // Verify that the requested app exists; it might be something that - // requested a backup but was then uninstalled. The request was - // journalled and rather than tamper with the journal it's safer - // to sanity-check here. This also gives us the classname of the - // package's backup agent. - RemoteResult agentResult = null; - try { - PackageManager pm = mBackupManagerService.getPackageManager(); - mCurrentPackage = pm.getPackageInfo(request.packageName, - PackageManager.GET_SIGNING_CERTIFICATES); - if (!AppBackupUtils.appIsEligibleForBackup(mCurrentPackage.applicationInfo, pm)) { - // The manifest has changed but we had a stale backup request pending. This won't - // happen again because the app won't be requesting further backups. - Slog.i(TAG, "Package " + request.packageName - + " no longer supports backup, skipping"); - mBackupManagerService.addBackupTrace("skipping - not eligible, completion is noop"); - // Shouldn't happen in case of requested backup, as pre-check was done in - // #requestBackup(), except to app update done concurrently - BackupObserverUtils.sendBackupOnPackageResult(mObserver, - mCurrentPackage.packageName, - BackupManager.ERROR_BACKUP_NOT_ALLOWED); - return Pair.create(BackupState.RUNNING_QUEUE, null); - } - - if (AppBackupUtils.appGetsFullBackup(mCurrentPackage)) { - // It's possible that this app *formerly* was enqueued for key-value backup, but has - // since been updated and now only supports the full-backup path. Don't proceed with - // a key-value backup for it in this case. - Slog.i(TAG, "Package " + request.packageName - + " performs full-backup rather than key-value, skipping"); - mBackupManagerService.addBackupTrace( - "skipping - fullBackupOnly, completion is noop"); - // Shouldn't happen in case of requested backup, as pre-check was done in - // #requestBackup() - BackupObserverUtils.sendBackupOnPackageResult(mObserver, - mCurrentPackage.packageName, - BackupManager.ERROR_BACKUP_NOT_ALLOWED); - return Pair.create(BackupState.RUNNING_QUEUE, null); - } - - if (AppBackupUtils.appIsStopped(mCurrentPackage.applicationInfo)) { - // The app has been force-stopped or cleared or just installed, - // and not yet launched out of that state, so just as it won't - // receive broadcasts, we won't run it for backup. - mBackupManagerService.addBackupTrace("skipping - stopped"); - BackupObserverUtils.sendBackupOnPackageResult(mObserver, - mCurrentPackage.packageName, - BackupManager.ERROR_BACKUP_NOT_ALLOWED); - return Pair.create(BackupState.RUNNING_QUEUE, null); - } - - try { - mBackupManagerService.setWorkSource( - new WorkSource(mCurrentPackage.applicationInfo.uid)); - IBackupAgent agent = - mBackupManagerService.bindToAgentSynchronous( - mCurrentPackage.applicationInfo, - ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL); - mBackupManagerService.addBackupTrace("agent bound; a? = " + (agent != null)); - if (agent != null) { - mAgentBinder = agent; - Pair statusAndResult = - invokeAgentForBackup(request.packageName, agent); - mStatus = statusAndResult.first; - agentResult = statusAndResult.second; - } else { - // Timeout waiting for the agent - mStatus = BackupTransport.AGENT_ERROR; - } - } catch (SecurityException se) { - // Try for the next one. - Slog.d(TAG, "Error in bind/backup", se); - mStatus = BackupTransport.AGENT_ERROR; - mBackupManagerService.addBackupTrace("agent SE"); - } - } catch (PackageManager.NameNotFoundException e) { - Slog.d(TAG, "Package does not exist, skipping"); - mBackupManagerService.addBackupTrace("no such package"); - mStatus = BackupTransport.AGENT_UNKNOWN; - } finally { - mBackupManagerService.setWorkSource(null); - } - - if (mStatus != BackupTransport.TRANSPORT_OK) { - BackupState nextState = BackupState.RUNNING_QUEUE; - mAgentBinder = null; - - // An agent-level failure means we re-enqueue this one agent for - // a later retry, but otherwise proceed normally. - if (mStatus == BackupTransport.AGENT_ERROR) { - if (MORE_DEBUG) { - Slog.i(TAG, "Agent failure for " + request.packageName + ", re-staging"); - } - mBackupManagerService.dataChangedImpl(request.packageName); - mStatus = BackupTransport.TRANSPORT_OK; - BackupObserverUtils - .sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName, - BackupManager.ERROR_AGENT_FAILURE); - } else if (mStatus == BackupTransport.AGENT_UNKNOWN) { - // Failed lookup of the app, so we couldn't bring up an agent, but - // we're otherwise fine. Just drop it and go on to the next as usual. - mStatus = BackupTransport.TRANSPORT_OK; - BackupObserverUtils - .sendBackupOnPackageResult(mObserver, request.packageName, - BackupManager.ERROR_PACKAGE_NOT_FOUND); - } else { - // Transport-level failure means we re-enqueue everything - revertAndEndBackup(); - nextState = BackupState.FINAL; - } - - return Pair.create(nextState, null); - } - - // Success: caller will figure out the state based on call result - mBackupManagerService.addBackupTrace("call made; result = " + agentResult); - return Pair.create(null, agentResult); - } - - private void finalizeBackup() { - mBackupManagerService.addBackupTrace("finishing"); - - // Mark packages that we didn't backup (because backup was cancelled, etc.) as needing - // backup. - for (BackupRequest req : mQueue) { - mBackupManagerService.dataChangedImpl(req.packageName); - } - - // Either backup was successful, in which case we of course do not need - // this pass's journal any more; or it failed, in which case we just - // re-enqueued all of these packages in the current active journal. - // Either way, we no longer need this pass's journal. - if (mJournal != null && !mJournal.delete()) { - Slog.e(TAG, "Unable to remove backup journal file " + mJournal); - } - - // If everything actually went through and this is the first time we've - // done a backup, we can now record what the current backup dataset token - // is. - String callerLogString = "KVBT.finalizeBackup()"; - if ((mBackupManagerService.getCurrentToken() == 0) && (mStatus - == BackupTransport.TRANSPORT_OK)) { - mBackupManagerService.addBackupTrace("success; recording token"); - try { - IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString); - mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet()); - mBackupManagerService.writeRestoreTokens(); - } catch (Exception e) { - // nothing for it at this point, unfortunately, but this will be - // recorded the next time we fully succeed. - Slog.e(TAG, "Transport threw reporting restore set: " + e); - mBackupManagerService.addBackupTrace("transport threw returning token"); - } - } - - // Set up the next backup pass - at this point we can set mBackupRunning - // to false to allow another pass to fire - synchronized (mBackupManagerService.getQueueLock()) { - mBackupManagerService.setBackupRunning(false); - if (mStatus == BackupTransport.TRANSPORT_NOT_INITIALIZED) { - if (MORE_DEBUG) { - Slog.d(TAG, "Transport requires initialization, rerunning"); - } - mBackupManagerService.addBackupTrace("init required; rerunning"); - try { - String name = mBackupManagerService.getTransportManager() - .getTransportName(mTransportClient.getTransportComponent()); - mBackupManagerService.getPendingInits().add(name); - } catch (Exception e) { - Slog.w(TAG, "Failed to query transport name for init: " + e); - // swallow it and proceed; we don't rely on this - } - clearMetadata(); - mBackupManagerService.backupNow(); - } - } - - mBackupManagerService.clearBackupTrace(); - - unregisterTask(); - - if (!mCancelled && mStatus == BackupTransport.TRANSPORT_OK && - mPendingFullBackups != null && !mPendingFullBackups.isEmpty()) { - Slog.d(TAG, "Starting full backups for: " + mPendingFullBackups); - // Acquiring wakelock for PerformFullTransportBackupTask before its start. - mBackupManagerService.getWakelock().acquire(); - // The full-backup task is now responsible for calling onFinish() on mListener, which - // was the listener we passed it. - (new Thread(mFullBackupTask, "full-transport-requested")).start(); - } else if (mCancelled) { - mListener.onFinished(callerLogString); - if (mFullBackupTask != null) { - mFullBackupTask.unregisterTask(); - } - BackupObserverUtils.sendBackupFinished(mObserver, BackupManager.ERROR_BACKUP_CANCELLED); - } else { - mListener.onFinished(callerLogString); - mFullBackupTask.unregisterTask(); - switch (mStatus) { - case BackupTransport.TRANSPORT_OK: - case BackupTransport.TRANSPORT_QUOTA_EXCEEDED: - case BackupTransport.TRANSPORT_PACKAGE_REJECTED: - BackupObserverUtils.sendBackupFinished(mObserver, BackupManager.SUCCESS); - break; - case BackupTransport.TRANSPORT_NOT_INITIALIZED: - BackupObserverUtils.sendBackupFinished(mObserver, - BackupManager.ERROR_TRANSPORT_ABORTED); - break; - case BackupTransport.TRANSPORT_ERROR: - default: - BackupObserverUtils.sendBackupFinished(mObserver, - BackupManager.ERROR_TRANSPORT_ABORTED); - break; - } - } - Slog.i(TAG, "K/V backup pass finished"); - mBackupManagerService.getWakelock().release(); - } - - // Remove the PM metadata state. This will generate an init on the next pass. - private void clearMetadata() { - final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); - if (pmState.exists()) pmState.delete(); - } - - /** - * Returns a {@link Pair}. The first of the pair contains the status. In case the status is - * {@link BackupTransport#TRANSPORT_OK}, the second of the pair contains the agent result, - * otherwise {@code null}. - */ - private Pair invokeAgentForBackup( - String packageName, IBackupAgent agent) { - if (DEBUG) { - Slog.d(TAG, "Invoking agent on " + packageName); - } - mBackupManagerService.addBackupTrace("invoking " + packageName); - - File blankStateFile = new File(mStateDir, BLANK_STATE_FILE_NAME); - mSavedStateFile = new File(mStateDir, packageName); - mBackupDataFile = - new File(mBackupManagerService.getDataDir(), packageName + STAGING_FILE_SUFFIX); - mNewStateFile = new File(mStateDir, packageName + NEW_STATE_FILE_SUFFIX); - if (MORE_DEBUG) { - Slog.d(TAG, "Data file: " + mBackupDataFile); - } - - - mSavedState = null; - mBackupData = null; - mNewState = null; - - boolean callingAgent = false; - final RemoteResult agentResult; - try { - // Look up the package info & signatures. This is first so that if it - // throws an exception, there's no file setup yet that would need to - // be unraveled. - if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) { - // The metadata 'package' is synthetic; construct one and make - // sure our global state is pointed at it - mCurrentPackage = new PackageInfo(); - mCurrentPackage.packageName = packageName; - } - - mSavedState = ParcelFileDescriptor.open( - (mNonIncremental) ? blankStateFile : mSavedStateFile, - ParcelFileDescriptor.MODE_READ_ONLY | - ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary - - mBackupData = ParcelFileDescriptor.open(mBackupDataFile, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - if (!SELinux.restorecon(mBackupDataFile)) { - Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataFile); - } - - mNewState = ParcelFileDescriptor.open(mNewStateFile, - ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE); - - IBackupTransport transport = - mTransportClient.connectOrThrow("KVBT.invokeAgentForBackup()"); - - final long quota = transport.getBackupQuota(packageName, false /* isFullBackup */); - callingAgent = true; - - // Initiate the target's backup pass - long kvBackupAgentTimeoutMillis = - mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis(); - mBackupManagerService.addBackupTrace("calling agent doBackup()"); - - agentResult = - remoteCall( - callback -> - agent.doBackup( - mSavedState, - mBackupData, - mNewState, - quota, - callback, - transport.getTransportFlags()), - kvBackupAgentTimeoutMillis); - } catch (Exception e) { - Slog.e(TAG, "Error invoking agent on " + packageName + ": " + e); - mBackupManagerService.addBackupTrace("exception: " + e); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, e.toString()); - errorCleanup(); - int status = - callingAgent ? BackupTransport.AGENT_ERROR : BackupTransport.TRANSPORT_ERROR; - return Pair.create(status, null); - } finally { - if (mNonIncremental) { - blankStateFile.delete(); - } - } - - return Pair.create(BackupTransport.TRANSPORT_OK, agentResult); - } - - private void failAgent(IBackupAgent agent, String message) { - try { - agent.fail(message); - } catch (Exception e) { - Slog.w(TAG, "Error conveying failure to " + 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) { - Slog.e(TAG, "Unable to use SHA-1!"); - return "00"; - } - - StringBuffer sb = new StringBuffer(checksum.length * 2); - for (byte item : checksum) { - sb.append(Integer.toHexString(item)); - } - return sb.toString(); - } - - private void writeWidgetPayloadIfAppropriate(FileDescriptor fd, String pkgName) - throws IOException { - // TODO: http://b/22388012 - byte[] widgetState = AppWidgetBackupBridge.getWidgetState(pkgName, UserHandle.USER_SYSTEM); - // has the widget state changed since last time? - final File widgetFile = new File(mStateDir, pkgName + "_widget"); - final boolean priorStateExists = widgetFile.exists(); - - if (MORE_DEBUG) { - if (priorStateExists || widgetState != null) { - Slog.i(TAG, "Checking widget update: state=" + (widgetState != null) - + " prior=" + priorStateExists); - } - } - - if (!priorStateExists && widgetState == null) { - // no prior state, no new state => nothing to do - return; - } - - // 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(); - } - } - - private BackupState handleAgentResult(long unusedResult) { - Preconditions.checkState(mBackupData != null); - - final String pkgName = mCurrentPackage.packageName; - final long filepos = mBackupDataFile.length(); - FileDescriptor fd = mBackupData.getFileDescriptor(); - try { - // If it's a 3rd party app, see whether they wrote any protected keys - // and complain mightily if they are attempting shenanigans. - if (mCurrentPackage.applicationInfo != null && - (mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) - == 0) { - ParcelFileDescriptor readFd = ParcelFileDescriptor.open(mBackupDataFile, - ParcelFileDescriptor.MODE_READ_ONLY); - BackupDataInput in = new BackupDataInput(readFd.getFileDescriptor()); - try { - while (in.readNextHeader()) { - final String key = in.getKey(); - if (key != null && key.charAt(0) >= 0xff00) { - // Not okay: crash them and bail. - failAgent(mAgentBinder, "Illegal backup key: " + key); - mBackupManagerService - .addBackupTrace("illegal key " + key + " from " + pkgName); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, pkgName, - "bad key"); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY, - mCurrentPackage, - BackupManagerMonitor - .LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, - BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, - key)); - BackupObserverUtils - .sendBackupOnPackageResult(mObserver, pkgName, - BackupManager.ERROR_AGENT_FAILURE); - errorCleanup(); - if (MORE_DEBUG) { - Slog.i(TAG, "Agent failure for " + pkgName - + " with illegal key " + key + ", dropped"); - } - - return BackupState.RUNNING_QUEUE; - } - in.skipEntityData(); - } - } finally { - if (readFd != null) { - readFd.close(); - } - } - } - - // Piggyback the widget state payload, if any - writeWidgetPayloadIfAppropriate(fd, pkgName); - } catch (IOException e) { - // Hard disk error; recovery/failure policy TBD. For now roll back, - // but we may want to consider this a transport-level failure (i.e. - // we're in such a bad state that we can't contemplate doing backup - // operations any more during this pass). - Slog.w(TAG, "Unable read backup data or to save widget state for " + pkgName); - try { - Os.ftruncate(fd, filepos); - } catch (ErrnoException ee) { - Slog.w(TAG, "Unable to roll back"); - } - } - - clearAgentState(); - mBackupManagerService.addBackupTrace("operation complete"); - - ParcelFileDescriptor backupData = null; - mStatus = BackupTransport.TRANSPORT_OK; - long size = 0; - try { - IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.handleAgentResult()"); - size = mBackupDataFile.length(); - if (size > 0) { - if (MORE_DEBUG) { - Slog.v(TAG, "Sending non-empty data to transport for " + pkgName); - } - boolean isNonIncremental = mSavedStateFile.length() == 0; - if (mStatus == BackupTransport.TRANSPORT_OK) { - backupData = ParcelFileDescriptor.open(mBackupDataFile, - ParcelFileDescriptor.MODE_READ_ONLY); - mBackupManagerService.addBackupTrace("sending data to transport"); - - int userInitiatedFlag = - mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0; - int incrementalFlag = - isNonIncremental - ? BackupTransport.FLAG_NON_INCREMENTAL - : BackupTransport.FLAG_INCREMENTAL; - int flags = userInitiatedFlag | incrementalFlag; - - mStatus = transport.performBackup(mCurrentPackage, backupData, flags); - } - - if (isNonIncremental - && mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { - // TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED is only valid if the backup was - // incremental, as if the backup is non-incremental there is no state to - // clear. This avoids us ending up in a retry loop if the transport always - // returns this code. - Slog.e(TAG, "Transport requested non-incremental but already the case"); - mBackupManagerService.addBackupTrace( - "Transport requested non-incremental but already the case, error"); - mStatus = BackupTransport.TRANSPORT_ERROR; - } - - mBackupManagerService.addBackupTrace("data delivered: " + mStatus); - if (mStatus == BackupTransport.TRANSPORT_OK) { - mBackupManagerService.addBackupTrace("finishing op on transport"); - mStatus = transport.finishBackup(); - mBackupManagerService.addBackupTrace("finished: " + mStatus); - } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { - mBackupManagerService.addBackupTrace("transport rejected package"); - } - } else { - if (MORE_DEBUG) { - Slog.i(TAG, "No backup data written, not calling transport"); - } - mBackupManagerService.addBackupTrace("no data to send"); - mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); - } - - if (mStatus == BackupTransport.TRANSPORT_OK) { - // After successful transport, delete the now-stale data - // and juggle the files so that next time we supply the agent - // with the new state file it just created. - mBackupDataFile.delete(); - mNewStateFile.renameTo(mSavedStateFile); - BackupObserverUtils.sendBackupOnPackageResult( - mObserver, pkgName, BackupManager.SUCCESS); - EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, pkgName, size); - mBackupManagerService.logBackupComplete(pkgName); - } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { - // The transport has rejected backup of this specific package. Roll it - // back but proceed with running the rest of the queue. - mBackupDataFile.delete(); - mNewStateFile.delete(); - BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, - BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED); - EventLogTags.writeBackupAgentFailure(pkgName, "Transport rejected"); - } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { - BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, - BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED); - EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName); - - } else if (mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { - Slog.i(TAG, "Transport lost data, retrying package"); - mBackupManagerService.addBackupTrace( - "Transport lost data, retrying package:" + pkgName); - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, - /*extras=*/ null); - - mBackupDataFile.delete(); - mSavedStateFile.delete(); - mNewStateFile.delete(); - - // Immediately retry the package by adding it back to the front of the queue. - // We cannot add @pm@ to the queue because we back it up separately at the start - // of the backup pass in state BACKUP_PM. Instead we retry this state (see - // below). - if (!PACKAGE_MANAGER_SENTINEL.equals(pkgName)) { - mQueue.add(0, new BackupRequest(pkgName)); - } - - } else { - // Actual transport-level failure to communicate the data to the backend - BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, - BackupManager.ERROR_TRANSPORT_ABORTED); - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName); - } - } catch (Exception e) { - BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, - BackupManager.ERROR_TRANSPORT_ABORTED); - Slog.e(TAG, "Transport error backing up " + pkgName, e); - EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName); - mStatus = BackupTransport.TRANSPORT_ERROR; - } finally { - try { - if (backupData != null) { - backupData.close(); - } - } catch (IOException e) { - Slog.w(TAG, "Error closing backup data file-descriptor"); - } - } - - final BackupState nextState; - if (mStatus == BackupTransport.TRANSPORT_OK - || mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { - // Success or single-package rejection. Proceed with the next app if any, - // otherwise we're done. - nextState = BackupState.RUNNING_QUEUE; - - } else if (mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { - // We want to immediately retry the current package. - if (PACKAGE_MANAGER_SENTINEL.equals(pkgName)) { - nextState = BackupState.BACKUP_PM; - } else { - // This is an ordinary package so we will have added it back into the queue - // above. Thus, we proceed processing the queue. - nextState = BackupState.RUNNING_QUEUE; - } - - } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { - if (MORE_DEBUG) { - Slog.d(TAG, "Package " + mCurrentPackage.packageName + - " hit quota limit on key-value backup"); - } - if (mAgentBinder != null) { - try { - IBackupTransport transport = - mTransportClient.connectOrThrow("KVBT.handleAgentResult()"); - long quota = transport.getBackupQuota(mCurrentPackage.packageName, false); - mAgentBinder.doQuotaExceeded(size, quota); - } catch (Exception e) { - Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage()); - } - } - nextState = BackupState.RUNNING_QUEUE; - } else { - // Any other error here indicates a transport-level failure. That means - // we need to halt everything and reschedule everything for next time. - revertAndEndBackup(); - nextState = BackupState.FINAL; - } - - return nextState; - } - - /** - * Cancels this task. After this method returns there will be no more calls to the transport. - * - *

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. - * - * @param cancelAll MUST be {@code true}. Will be removed. - */ - @Override - public void handleCancel(boolean cancelAll) { - Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task"); - if (MORE_DEBUG) { - Slog.v(TAG, "Cancel received"); - } - mCancelled = true; - RemoteCall pendingCall = mPendingCall; - if (pendingCall != null) { - pendingCall.cancel(); - } - mCancelAcknowledged.block(); - } - - private void handleAgentTimeout() { - String packageName = getPackageNameForLog(); - Slog.i(TAG, "Agent " + packageName + " timed out"); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); - mBackupManagerService.addBackupTrace("timeout of " + packageName); - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, - BackupManagerMonitorUtils.putMonitoringExtra( - null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false)); - errorCleanup(); - } - - private void handleAgentCancelled() { - String packageName = getPackageNameForLog(); - Slog.i(TAG, "Cancel backing up " + packageName); - EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); - mBackupManagerService.addBackupTrace("cancel of " + packageName); - errorCleanup(); - } - - private void finalizeCancelledBackup() { - mMonitor = - BackupManagerMonitorUtils.monitorEvent( - mMonitor, - BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, - BackupManagerMonitorUtils.putMonitoringExtra( - null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true)); - finalizeBackup(); - // finalizeBackup() may call the transport, so we only acknowledge the cancellation here. - mCancelAcknowledged.open(); - } - - private String getPackageNameForLog() { - return (mCurrentPackage != null) ? mCurrentPackage.packageName : "no_package_yet"; - } - - private void revertAndEndBackup() { - if (MORE_DEBUG) { - Slog.i(TAG, "Reverting backup queue, re-staging everything"); - } - mBackupManagerService.addBackupTrace("transport error; reverting"); - - // We want to reset the backup schedule based on whatever the transport suggests - // by way of retry/backoff time. - long delay; - try { - IBackupTransport transport = - mTransportClient.connectOrThrow("KVBT.revertAndEndBackup()"); - delay = transport.requestBackupTime(); - } catch (Exception e) { - Slog.w(TAG, "Unable to contact transport for recommended backoff: " + e); - delay = 0; // use the scheduler's default - } - KeyValueBackupJob.schedule(mBackupManagerService.getContext(), delay, - mBackupManagerService.getConstants()); - - for (BackupRequest request : mOriginalQueue) { - mBackupManagerService.dataChangedImpl(request.packageName); - } - } - - private void errorCleanup() { - mBackupDataFile.delete(); - mNewStateFile.delete(); - clearAgentState(); - } - - // Cleanup common to both success and failure cases - private void clearAgentState() { - try { - if (mSavedState != null) { - mSavedState.close(); - } - } catch (IOException e) { - Slog.w(TAG, "Error closing old state file-descriptor"); - } - try { - if (mBackupData != null) { - mBackupData.close(); - } - } catch (IOException e) { - Slog.w(TAG, "Error closing backup data file-descriptor"); - } - try { - if (mNewState != null) { - mNewState.close(); - } - } catch (IOException e) { - Slog.w(TAG, "Error closing new state file-descriptor"); - } - synchronized (mBackupManagerService.getCurrentOpLock()) { - // Current-operation callback handling requires the validity of these various - // bits of internal state as an invariant of the operation still being live. - // This means we make sure to clear all of the state in unison inside the lock. - mSavedState = mBackupData = mNewState = null; - } - - // If this was a pseudo-package there's no associated Activity Manager state - if (mCurrentPackage.applicationInfo != null) { - mBackupManagerService.addBackupTrace("unbinding " + mCurrentPackage.packageName); - mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo); - } - } - - private RemoteResult remoteCall(RemoteCallable remoteCallable, long timeoutMs) - throws RemoteException { - mPendingCall = new RemoteCall(mCancelled, remoteCallable, timeoutMs); - RemoteResult result = mPendingCall.call(); - if (MORE_DEBUG) { - Slog.v(TAG, "Agent call returned " + result); - } - mPendingCall = null; - return result; - } -} diff --git a/services/backup/java/com/android/server/backup/keyvalue/BackupRequest.java b/services/backup/java/com/android/server/backup/keyvalue/BackupRequest.java new file mode 100644 index 000000000000..67b2f7282f64 --- /dev/null +++ b/services/backup/java/com/android/server/backup/keyvalue/BackupRequest.java @@ -0,0 +1,51 @@ +/* + * 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 java.util.Objects; + +/** + * Set of backup services that have pending changes. + */ +public class BackupRequest { + public String packageName; + + public BackupRequest(String pkgName) { + packageName = pkgName; + } + + public String toString() { + return "BackupRequest{pkg=" + packageName + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BackupRequest)) { + return false; + } + BackupRequest that = (BackupRequest) o; + return Objects.equals(packageName, that.packageName); + } + + @Override + public int hashCode() { + return Objects.hash(packageName); + } +} diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java new file mode 100644 index 000000000000..113e2b67d04e --- /dev/null +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -0,0 +1,1364 @@ +/* + * 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 com.android.server.backup.BackupManagerService.DEBUG_BACKUP_TRACE; +import static com.android.server.backup.BackupManagerService.KEY_WIDGET_STATE; +import static com.android.server.backup.BackupManagerService.OP_PENDING; +import static com.android.server.backup.BackupManagerService.OP_TYPE_BACKUP; +import static com.android.server.backup.BackupManagerService.PACKAGE_MANAGER_SENTINEL; + +import android.annotation.Nullable; +import android.app.ApplicationThreadConstants; +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.BackupManagerMonitor; +import android.app.backup.BackupTransport; +import android.app.backup.IBackupCallback; +import android.app.backup.IBackupManager; +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.system.ErrnoException; +import android.system.Os; +import android.util.EventLog; +import android.util.Pair; +import android.util.Slog; + +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.EventLogTags; +import com.android.server.backup.BackupAgentTimeoutParameters; +import com.android.server.backup.BackupManagerService; +import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.DataChangedJournal; +import com.android.server.backup.KeyValueBackupJob; +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 com.android.server.backup.utils.BackupManagerMonitorUtils; +import com.android.server.backup.utils.BackupObserverUtils; + +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.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. + * + *

A few definitions: + * + *

    + *
  • State directory: {@link BackupManagerService#getBaseStateDir()}/<transport> + *
  • State file: {@link + * BackupManagerService#getBaseStateDir()}/<transport>/<package>
    + * Represents the state of the backup data for a specific package in the current dataset. + *
  • Stage directory: {@link BackupManagerService#getDataDir()} + *
  • Stage file: {@link BackupManagerService#getDataDir()}/<package>.data
    + * Contains staged data that the agents wrote via {@link BackupDataOutput}, to be transmitted + * to the transport. + *
+ * + * If there is no PackageManager (PM) pseudo-package state file in the state directory, the + * specified transport will be initialized with {@link IBackupTransport#initializeDevice()}. + * + *

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. + * + *

Before starting, this task will register itself in {@link BackupManagerService} current + * operations. + * + *

In summary, this task will for each package: + * + *

    + *
  • Bind to its {@link IBackupAgent}. + *
  • Request transport quota and flags. + *
  • Call {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor, + * ParcelFileDescriptor, long, int, IBackupManager, 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: + *
      + *
    • Agent response. + *
    • Agent time-out (specified via {@link + * BackupManagerService#getAgentTimeoutParameters()}. + *
    • External cancellation or thread interrupt. + *
    + *
  • Unbind the agent. + *
  • Assuming agent response, send the staged data that the agent wrote to disk to the transport + * via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}. + *
  • Call {@link IBackupTransport#finishBackup()} if previous call was successful. + *
  • 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. + *
  • Delete the stage file. + *
+ * + * In the end, this task will: + * + *
    + *
  • Mark data-changed for the remaining packages in the queue (skipped packages). + *
  • Delete the {@link DataChangedJournal} provided. Note that this should not be the current + * journal. + *
  • Set {@link BackupManagerService} current token as {@link + * IBackupTransport#getCurrentRestoreSet()}, if applicable. + *
  • Add the transport to the list of transports pending initialization ({@link + * BackupManagerService#getPendingInits()}) and kick-off initialization if the transport ever + * returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}. + *
  • Unregister the task in current operations. + *
  • Release the wakelock. + *
  • Kick-off {@link PerformFullTransportBackupTask} if a list of full-backup packages was + * provided. + *
+ * + * 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. + * + *

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 = "KeyValueBackupTask"; + private static final boolean DEBUG = BackupManagerService.DEBUG || true; + private static final boolean MORE_DEBUG = BackupManagerService.MORE_DEBUG || false; + 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"; + @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 BackupManagerService} system service. + * @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 packages that will be backed-up, in the form of {@link + * BackupRequest}. + * @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( + BackupManagerService backupManagerService, + TransportClient transportClient, + String transportDirName, + List queue, + @Nullable DataChangedJournal dataChangedJournal, + IBackupObserver observer, + IBackupManagerMonitor monitor, + @Nullable OnTaskFinishedListener listener, + List pendingFullBackups, + boolean userInitiated, + boolean nonIncremental) { + KeyValueBackupTask task = + new KeyValueBackupTask( + backupManagerService, + transportClient, + transportDirName, + queue, + dataChangedJournal, + observer, + monitor, + listener, + pendingFullBackups, + userInitiated, + nonIncremental); + Thread thread = new Thread(task, "key-value-backup-" + THREAD_COUNT.incrementAndGet()); + if (DEBUG) { + Slog.d(TAG, "Spinning thread " + thread.getName()); + } + thread.start(); + return task; + } + + private final BackupManagerService mBackupManagerService; + private final TransportClient mTransportClient; + private final BackupAgentTimeoutParameters mAgentTimeoutParameters; + private final IBackupObserver mObserver; + private final OnTaskFinishedListener mListener; + private final boolean mUserInitiated; + private final boolean mNonIncremental; + private final int mCurrentOpToken; + private final File mStateDir; + private final List mOriginalQueue; + private final List mQueue; + private final List mPendingFullBackups; + @Nullable private final DataChangedJournal mJournal; + private IBackupManagerMonitor mMonitor; + @Nullable private PerformFullTransportBackupTask mFullBackupTask; + + private IBackupAgent mAgentBinder; + private PackageInfo mCurrentPackage; + private File mSavedStateFile; + private File mBackupDataFile; + private File mNewStateFile; + private ParcelFileDescriptor mSavedState; + private ParcelFileDescriptor mBackupData; + private ParcelFileDescriptor mNewState; + private int mStatus; + + /** + * 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( + BackupManagerService backupManagerService, + TransportClient transportClient, + String transportDirName, + List queue, + @Nullable DataChangedJournal journal, + IBackupObserver observer, + IBackupManagerMonitor monitor, + @Nullable OnTaskFinishedListener listener, + List pendingFullBackups, + boolean userInitiated, + boolean nonIncremental) { + mBackupManagerService = backupManagerService; + mTransportClient = transportClient; + mOriginalQueue = queue; + // We need to retain the original queue contents in case of transport failure + mQueue = new ArrayList<>(mOriginalQueue); + mJournal = journal; + mObserver = observer; + mMonitor = monitor; + mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP; + mPendingFullBackups = pendingFullBackups; + mUserInitiated = userInitiated; + mNonIncremental = nonIncremental; + mAgentTimeoutParameters = + Preconditions.checkNotNull( + backupManagerService.getAgentTimeoutParameters(), + "Timeout parameters cannot be null"); + mStateDir = new File(backupManagerService.getBaseStateDir(), transportDirName); + mCurrentOpToken = backupManagerService.generateRandomIntegerToken(); + } + + 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); + + BackupState state = beginBackup(); + while (state == BackupState.RUNNING_QUEUE || state == BackupState.BACKUP_PM) { + if (mCancelled) { + state = BackupState.CANCELLED; + } + switch (state) { + case BACKUP_PM: + state = backupPm(); + break; + case RUNNING_QUEUE: + Pair stateAndResult = invokeNextAgent(); + state = stateAndResult.first; + if (state == null) { + state = processAgentInvocation(stateAndResult.second); + } + break; + } + } + if (state == BackupState.CANCELLED) { + finalizeCancelledBackup(); + } else { + finalizeBackup(); + } + } + + private BackupState processAgentInvocation(RemoteResult result) { + if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) { + // Not an explicit cancel, we need to flag it + mCancelled = true; + handleAgentCancelled(); + return BackupState.CANCELLED; + } + if (result == RemoteResult.FAILED_CANCELLED) { + handleAgentCancelled(); + return BackupState.CANCELLED; + } + if (result == RemoteResult.FAILED_TIMED_OUT) { + handleAgentTimeout(); + return BackupState.RUNNING_QUEUE; + } + Preconditions.checkState(result.succeeded()); + return handleAgentResult(result.get()); + } + + @Override + public void execute() {} + + @Override + public void operationComplete(long unusedResult) {} + + private BackupState beginBackup() { + if (DEBUG_BACKUP_TRACE) { + mBackupManagerService.clearBackupTrace(); + StringBuilder b = new StringBuilder(256); + b.append("beginBackup: ["); + for (BackupRequest req : mOriginalQueue) { + b.append(' '); + b.append(req.packageName); + } + b.append(" ]"); + mBackupManagerService.addBackupTrace(b.toString()); + } + synchronized (mBackupManagerService.getCurrentOpLock()) { + if (mBackupManagerService.isBackupOperationInProgress()) { + if (DEBUG) { + Slog.d(TAG, "Skipping backup since one is already in progress"); + } + mBackupManagerService.addBackupTrace("Skipped. Backup already in progress."); + return BackupState.FINAL; + } + } + + String[] fullBackups = mPendingFullBackups.toArray(new String[mPendingFullBackups.size()]); + mFullBackupTask = + new PerformFullTransportBackupTask( + mBackupManagerService, + mTransportClient, + /* fullBackupRestoreObserver */ null, + fullBackups, + /* updateSchedule */ false, + /* runningJob */ null, + new CountDownLatch(1), + mObserver, + mMonitor, + mListener, + mUserInitiated); + registerTask(); + mBackupManagerService.addBackupTrace("STATE => INITIAL"); + + mAgentBinder = null; + mStatus = BackupTransport.TRANSPORT_OK; + + // Sanity check: if the queue is empty we have no work to do. + if (mOriginalQueue.isEmpty() && mPendingFullBackups.isEmpty()) { + Slog.w(TAG, "Backup begun with an empty queue, nothing to do."); + mBackupManagerService.addBackupTrace("queue empty at begin"); + return BackupState.FINAL; + } + + // When the transport is forcing non-incremental key/value payloads, we send the + // metadata only if it explicitly asks for it. + boolean skipPm = mNonIncremental; + + // The app metadata pseudopackage might also be represented in the + // backup queue if apps have been added/removed since the last time + // we performed a backup. Drop it from the working queue now that + // we're committed to evaluating it for backup regardless. + for (int i = 0; i < mQueue.size(); i++) { + if (PACKAGE_MANAGER_SENTINEL.equals(mQueue.get(i).packageName)) { + if (MORE_DEBUG) { + Slog.i(TAG, "PM metadata in queue, removing"); + } + mQueue.remove(i); + skipPm = false; + break; + } + } + + if (DEBUG) { + Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); + } + File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + try { + IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.beginBackup()"); + String transportName = transport.name(); + EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); + + // If we haven't stored package manager metadata yet, we must init the transport. + if (pmState.length() <= 0) { + Slog.i(TAG, "Initializing transport and resetting backup state"); + mBackupManagerService.addBackupTrace("initializing transport " + transportName); + mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. + mStatus = transport.initializeDevice(); + + mBackupManagerService.addBackupTrace("transport.initializeDevice() == " + mStatus); + if (mStatus == BackupTransport.TRANSPORT_OK) { + EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); + } else { + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + Slog.e(TAG, "Transport error in initializeDevice()"); + } + } + } catch (Exception e) { + Slog.e(TAG, "Error during initialization", e); + mBackupManagerService.addBackupTrace("Exception in backup thread during init: " + e); + mStatus = BackupTransport.TRANSPORT_ERROR; + } + mBackupManagerService.addBackupTrace("exiting prelim: " + mStatus); + + if (mStatus != BackupTransport.TRANSPORT_OK) { + // if things went wrong at this point, we need to + // restage everything and try again later. + mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. + return BackupState.FINAL; + } + + if (skipPm) { + Slog.d(TAG, "Skipping backup of PM metadata"); + return BackupState.RUNNING_QUEUE; + } + + return BackupState.BACKUP_PM; + } + + private BackupState backupPm() { + RemoteResult agentResult = null; + BackupState nextState; + try { + // The package manager doesn't have a proper etc, but since it's running + // here in the system process we can just set up its agent directly and use a synthetic + // BackupRequest. + BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent(); + Pair statusAndResult = + invokeAgentForBackup( + PACKAGE_MANAGER_SENTINEL, + IBackupAgent.Stub.asInterface(pmAgent.onBind())); + mStatus = statusAndResult.first; + agentResult = statusAndResult.second; + + mBackupManagerService.addBackupTrace("PM agent invoke: " + mStatus); + } catch (Exception e) { + Slog.e(TAG, "Error during PM metadata backup", e); + mBackupManagerService.addBackupTrace("Exception in backup thread during pm: " + e); + mStatus = BackupTransport.TRANSPORT_ERROR; + } + mBackupManagerService.addBackupTrace("exiting backupPm: " + mStatus); + + if (mStatus == BackupTransport.TRANSPORT_OK) { + Preconditions.checkNotNull(agentResult); + nextState = processAgentInvocation(agentResult); + } else { + // if things went wrong at this point, we need to + // restage everything and try again later. + mBackupManagerService.resetBackupState(mStateDir); // Just to make sure. + nextState = BackupState.FINAL; + } + + return nextState; + } + + /** + * Returns either: + * + *

    + *
  • (next state, {@code null}): In case we failed to call the agent. + *
  • ({@code null}, agent result): In case we successfully called the agent. + *
+ */ + private Pair invokeNextAgent() { + mStatus = BackupTransport.TRANSPORT_OK; + mBackupManagerService.addBackupTrace("invoke q=" + mQueue.size()); + + // Sanity check that we have work to do. If not, skip to the end where + // we reestablish the wakelock invariants etc. + if (mQueue.isEmpty()) { + if (MORE_DEBUG) { + Slog.i(TAG, "Queue now empty"); + } + return Pair.create(BackupState.FINAL, null); + } + + // pop the entry we're going to process on this step + BackupRequest request = mQueue.remove(0); + + Slog.d(TAG, "Starting key-value backup of " + request); + mBackupManagerService.addBackupTrace("launch agent for " + request.packageName); + + // Verify that the requested app exists; it might be something that + // requested a backup but was then uninstalled. The request was + // journalled and rather than tamper with the journal it's safer + // to sanity-check here. This also gives us the classname of the + // package's backup agent. + RemoteResult agentResult = null; + try { + PackageManager pm = mBackupManagerService.getPackageManager(); + mCurrentPackage = pm.getPackageInfo(request.packageName, + PackageManager.GET_SIGNING_CERTIFICATES); + if (!AppBackupUtils.appIsEligibleForBackup(mCurrentPackage.applicationInfo, pm)) { + // The manifest has changed but we had a stale backup request pending. This won't + // happen again because the app won't be requesting further backups. + Slog.i(TAG, "Package " + request.packageName + + " no longer supports backup, skipping"); + mBackupManagerService.addBackupTrace("skipping - not eligible, completion is noop"); + // Shouldn't happen in case of requested backup, as pre-check was done in + // #requestBackup(), except to app update done concurrently + BackupObserverUtils.sendBackupOnPackageResult(mObserver, + mCurrentPackage.packageName, + BackupManager.ERROR_BACKUP_NOT_ALLOWED); + return Pair.create(BackupState.RUNNING_QUEUE, null); + } + + if (AppBackupUtils.appGetsFullBackup(mCurrentPackage)) { + // It's possible that this app *formerly* was enqueued for key-value backup, but has + // since been updated and now only supports the full-backup path. Don't proceed with + // a key-value backup for it in this case. + Slog.i(TAG, "Package " + request.packageName + + " performs full-backup rather than key-value, skipping"); + mBackupManagerService.addBackupTrace( + "skipping - fullBackupOnly, completion is noop"); + // Shouldn't happen in case of requested backup, as pre-check was done in + // #requestBackup() + BackupObserverUtils.sendBackupOnPackageResult(mObserver, + mCurrentPackage.packageName, + BackupManager.ERROR_BACKUP_NOT_ALLOWED); + return Pair.create(BackupState.RUNNING_QUEUE, null); + } + + if (AppBackupUtils.appIsStopped(mCurrentPackage.applicationInfo)) { + // The app has been force-stopped or cleared or just installed, + // and not yet launched out of that state, so just as it won't + // receive broadcasts, we won't run it for backup. + mBackupManagerService.addBackupTrace("skipping - stopped"); + BackupObserverUtils.sendBackupOnPackageResult(mObserver, + mCurrentPackage.packageName, + BackupManager.ERROR_BACKUP_NOT_ALLOWED); + return Pair.create(BackupState.RUNNING_QUEUE, null); + } + + try { + mBackupManagerService.setWorkSource( + new WorkSource(mCurrentPackage.applicationInfo.uid)); + IBackupAgent agent = + mBackupManagerService.bindToAgentSynchronous( + mCurrentPackage.applicationInfo, + ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL); + mBackupManagerService.addBackupTrace("agent bound; a? = " + (agent != null)); + if (agent != null) { + mAgentBinder = agent; + Pair statusAndResult = + invokeAgentForBackup(request.packageName, agent); + mStatus = statusAndResult.first; + agentResult = statusAndResult.second; + } else { + // Timeout waiting for the agent + mStatus = BackupTransport.AGENT_ERROR; + } + } catch (SecurityException se) { + // Try for the next one. + Slog.d(TAG, "Error in bind/backup", se); + mStatus = BackupTransport.AGENT_ERROR; + mBackupManagerService.addBackupTrace("agent SE"); + } + } catch (PackageManager.NameNotFoundException e) { + Slog.d(TAG, "Package does not exist, skipping"); + mBackupManagerService.addBackupTrace("no such package"); + mStatus = BackupTransport.AGENT_UNKNOWN; + } finally { + mBackupManagerService.setWorkSource(null); + } + + if (mStatus != BackupTransport.TRANSPORT_OK) { + BackupState nextState = BackupState.RUNNING_QUEUE; + mAgentBinder = null; + + // An agent-level failure means we re-enqueue this one agent for + // a later retry, but otherwise proceed normally. + if (mStatus == BackupTransport.AGENT_ERROR) { + if (MORE_DEBUG) { + Slog.i(TAG, "Agent failure for " + request.packageName + ", re-staging"); + } + mBackupManagerService.dataChangedImpl(request.packageName); + mStatus = BackupTransport.TRANSPORT_OK; + BackupObserverUtils + .sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName, + BackupManager.ERROR_AGENT_FAILURE); + } else if (mStatus == BackupTransport.AGENT_UNKNOWN) { + // Failed lookup of the app, so we couldn't bring up an agent, but + // we're otherwise fine. Just drop it and go on to the next as usual. + mStatus = BackupTransport.TRANSPORT_OK; + BackupObserverUtils + .sendBackupOnPackageResult(mObserver, request.packageName, + BackupManager.ERROR_PACKAGE_NOT_FOUND); + } else { + // Transport-level failure means we re-enqueue everything + revertAndEndBackup(); + nextState = BackupState.FINAL; + } + + return Pair.create(nextState, null); + } + + // Success: caller will figure out the state based on call result + mBackupManagerService.addBackupTrace("call made; result = " + agentResult); + return Pair.create(null, agentResult); + } + + private void finalizeBackup() { + mBackupManagerService.addBackupTrace("finishing"); + + // Mark packages that we didn't backup (because backup was cancelled, etc.) as needing + // backup. + for (BackupRequest req : mQueue) { + mBackupManagerService.dataChangedImpl(req.packageName); + } + + // Either backup was successful, in which case we of course do not need + // this pass's journal any more; or it failed, in which case we just + // re-enqueued all of these packages in the current active journal. + // Either way, we no longer need this pass's journal. + if (mJournal != null && !mJournal.delete()) { + Slog.e(TAG, "Unable to remove backup journal file " + mJournal); + } + + // If everything actually went through and this is the first time we've + // done a backup, we can now record what the current backup dataset token + // is. + String callerLogString = "KVBT.finalizeBackup()"; + if ((mBackupManagerService.getCurrentToken() == 0) && (mStatus + == BackupTransport.TRANSPORT_OK)) { + mBackupManagerService.addBackupTrace("success; recording token"); + try { + IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString); + mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet()); + mBackupManagerService.writeRestoreTokens(); + } catch (Exception e) { + // nothing for it at this point, unfortunately, but this will be + // recorded the next time we fully succeed. + Slog.e(TAG, "Transport threw reporting restore set: " + e); + mBackupManagerService.addBackupTrace("transport threw returning token"); + } + } + + // Set up the next backup pass - at this point we can set mBackupRunning + // to false to allow another pass to fire + synchronized (mBackupManagerService.getQueueLock()) { + mBackupManagerService.setBackupRunning(false); + if (mStatus == BackupTransport.TRANSPORT_NOT_INITIALIZED) { + if (MORE_DEBUG) { + Slog.d(TAG, "Transport requires initialization, rerunning"); + } + mBackupManagerService.addBackupTrace("init required; rerunning"); + try { + String name = mBackupManagerService.getTransportManager() + .getTransportName(mTransportClient.getTransportComponent()); + mBackupManagerService.getPendingInits().add(name); + } catch (Exception e) { + Slog.w(TAG, "Failed to query transport name for init: " + e); + // swallow it and proceed; we don't rely on this + } + clearMetadata(); + mBackupManagerService.backupNow(); + } + } + + mBackupManagerService.clearBackupTrace(); + + unregisterTask(); + + if (!mCancelled && mStatus == BackupTransport.TRANSPORT_OK && + mPendingFullBackups != null && !mPendingFullBackups.isEmpty()) { + Slog.d(TAG, "Starting full backups for: " + mPendingFullBackups); + // Acquiring wakelock for PerformFullTransportBackupTask before its start. + mBackupManagerService.getWakelock().acquire(); + // The full-backup task is now responsible for calling onFinish() on mListener, which + // was the listener we passed it. + (new Thread(mFullBackupTask, "full-transport-requested")).start(); + } else if (mCancelled) { + mListener.onFinished(callerLogString); + if (mFullBackupTask != null) { + mFullBackupTask.unregisterTask(); + } + BackupObserverUtils.sendBackupFinished(mObserver, BackupManager.ERROR_BACKUP_CANCELLED); + } else { + mListener.onFinished(callerLogString); + mFullBackupTask.unregisterTask(); + switch (mStatus) { + case BackupTransport.TRANSPORT_OK: + case BackupTransport.TRANSPORT_QUOTA_EXCEEDED: + case BackupTransport.TRANSPORT_PACKAGE_REJECTED: + BackupObserverUtils.sendBackupFinished(mObserver, BackupManager.SUCCESS); + break; + case BackupTransport.TRANSPORT_NOT_INITIALIZED: + BackupObserverUtils.sendBackupFinished(mObserver, + BackupManager.ERROR_TRANSPORT_ABORTED); + break; + case BackupTransport.TRANSPORT_ERROR: + default: + BackupObserverUtils.sendBackupFinished(mObserver, + BackupManager.ERROR_TRANSPORT_ABORTED); + break; + } + } + Slog.i(TAG, "K/V backup pass finished"); + mBackupManagerService.getWakelock().release(); + } + + // Remove the PM metadata state. This will generate an init on the next pass. + private void clearMetadata() { + final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + if (pmState.exists()) pmState.delete(); + } + + /** + * Returns a {@link Pair}. The first of the pair contains the status. In case the status is + * {@link BackupTransport#TRANSPORT_OK}, the second of the pair contains the agent result, + * otherwise {@code null}. + */ + private Pair invokeAgentForBackup( + String packageName, IBackupAgent agent) { + if (DEBUG) { + Slog.d(TAG, "Invoking agent on " + packageName); + } + mBackupManagerService.addBackupTrace("invoking " + packageName); + + File blankStateFile = new File(mStateDir, BLANK_STATE_FILE_NAME); + mSavedStateFile = new File(mStateDir, packageName); + mBackupDataFile = + new File(mBackupManagerService.getDataDir(), packageName + STAGING_FILE_SUFFIX); + mNewStateFile = new File(mStateDir, packageName + NEW_STATE_FILE_SUFFIX); + if (MORE_DEBUG) { + Slog.d(TAG, "Data file: " + mBackupDataFile); + } + + + mSavedState = null; + mBackupData = null; + mNewState = null; + + boolean callingAgent = false; + final RemoteResult agentResult; + try { + // Look up the package info & signatures. This is first so that if it + // throws an exception, there's no file setup yet that would need to + // be unraveled. + if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) { + // The metadata 'package' is synthetic; construct one and make + // sure our global state is pointed at it + mCurrentPackage = new PackageInfo(); + mCurrentPackage.packageName = packageName; + } + + mSavedState = ParcelFileDescriptor.open( + (mNonIncremental) ? blankStateFile : mSavedStateFile, + ParcelFileDescriptor.MODE_READ_ONLY | + ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary + + mBackupData = ParcelFileDescriptor.open(mBackupDataFile, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + if (!SELinux.restorecon(mBackupDataFile)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataFile); + } + + mNewState = ParcelFileDescriptor.open(mNewStateFile, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + IBackupTransport transport = + mTransportClient.connectOrThrow("KVBT.invokeAgentForBackup()"); + + final long quota = transport.getBackupQuota(packageName, false /* isFullBackup */); + callingAgent = true; + + // Initiate the target's backup pass + long kvBackupAgentTimeoutMillis = + mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis(); + mBackupManagerService.addBackupTrace("calling agent doBackup()"); + + agentResult = + remoteCall( + callback -> + agent.doBackup( + mSavedState, + mBackupData, + mNewState, + quota, + callback, + transport.getTransportFlags()), + kvBackupAgentTimeoutMillis); + } catch (Exception e) { + Slog.e(TAG, "Error invoking agent on " + packageName + ": " + e); + mBackupManagerService.addBackupTrace("exception: " + e); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, e.toString()); + errorCleanup(); + int status = + callingAgent ? BackupTransport.AGENT_ERROR : BackupTransport.TRANSPORT_ERROR; + return Pair.create(status, null); + } finally { + if (mNonIncremental) { + blankStateFile.delete(); + } + } + + return Pair.create(BackupTransport.TRANSPORT_OK, agentResult); + } + + private void failAgent(IBackupAgent agent, String message) { + try { + agent.fail(message); + } catch (Exception e) { + Slog.w(TAG, "Error conveying failure to " + 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) { + Slog.e(TAG, "Unable to use SHA-1!"); + return "00"; + } + + StringBuffer sb = new StringBuffer(checksum.length * 2); + for (byte item : checksum) { + sb.append(Integer.toHexString(item)); + } + return sb.toString(); + } + + private void writeWidgetPayloadIfAppropriate(FileDescriptor fd, String pkgName) + throws IOException { + // TODO: http://b/22388012 + byte[] widgetState = AppWidgetBackupBridge.getWidgetState(pkgName, UserHandle.USER_SYSTEM); + // has the widget state changed since last time? + final File widgetFile = new File(mStateDir, pkgName + "_widget"); + final boolean priorStateExists = widgetFile.exists(); + + if (MORE_DEBUG) { + if (priorStateExists || widgetState != null) { + Slog.i(TAG, "Checking widget update: state=" + (widgetState != null) + + " prior=" + priorStateExists); + } + } + + if (!priorStateExists && widgetState == null) { + // no prior state, no new state => nothing to do + return; + } + + // 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(); + } + } + + private BackupState handleAgentResult(long unusedResult) { + Preconditions.checkState(mBackupData != null); + + final String pkgName = mCurrentPackage.packageName; + final long filepos = mBackupDataFile.length(); + FileDescriptor fd = mBackupData.getFileDescriptor(); + try { + // If it's a 3rd party app, see whether they wrote any protected keys + // and complain mightily if they are attempting shenanigans. + if (mCurrentPackage.applicationInfo != null && + (mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) + == 0) { + ParcelFileDescriptor readFd = ParcelFileDescriptor.open(mBackupDataFile, + ParcelFileDescriptor.MODE_READ_ONLY); + BackupDataInput in = new BackupDataInput(readFd.getFileDescriptor()); + try { + while (in.readNextHeader()) { + final String key = in.getKey(); + if (key != null && key.charAt(0) >= 0xff00) { + // Not okay: crash them and bail. + failAgent(mAgentBinder, "Illegal backup key: " + key); + mBackupManagerService + .addBackupTrace("illegal key " + key + " from " + pkgName); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, pkgName, + "bad key"); + mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY, + mCurrentPackage, + BackupManagerMonitor + .LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + BackupManagerMonitorUtils.putMonitoringExtra(null, + BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, + key)); + BackupObserverUtils + .sendBackupOnPackageResult(mObserver, pkgName, + BackupManager.ERROR_AGENT_FAILURE); + errorCleanup(); + if (MORE_DEBUG) { + Slog.i(TAG, "Agent failure for " + pkgName + + " with illegal key " + key + ", dropped"); + } + + return BackupState.RUNNING_QUEUE; + } + in.skipEntityData(); + } + } finally { + if (readFd != null) { + readFd.close(); + } + } + } + + // Piggyback the widget state payload, if any + writeWidgetPayloadIfAppropriate(fd, pkgName); + } catch (IOException e) { + // Hard disk error; recovery/failure policy TBD. For now roll back, + // but we may want to consider this a transport-level failure (i.e. + // we're in such a bad state that we can't contemplate doing backup + // operations any more during this pass). + Slog.w(TAG, "Unable read backup data or to save widget state for " + pkgName); + try { + Os.ftruncate(fd, filepos); + } catch (ErrnoException ee) { + Slog.w(TAG, "Unable to roll back"); + } + } + + clearAgentState(); + mBackupManagerService.addBackupTrace("operation complete"); + + ParcelFileDescriptor backupData = null; + mStatus = BackupTransport.TRANSPORT_OK; + long size = 0; + try { + IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.handleAgentResult()"); + size = mBackupDataFile.length(); + if (size > 0) { + if (MORE_DEBUG) { + Slog.v(TAG, "Sending non-empty data to transport for " + pkgName); + } + boolean isNonIncremental = mSavedStateFile.length() == 0; + if (mStatus == BackupTransport.TRANSPORT_OK) { + backupData = ParcelFileDescriptor.open(mBackupDataFile, + ParcelFileDescriptor.MODE_READ_ONLY); + mBackupManagerService.addBackupTrace("sending data to transport"); + + int userInitiatedFlag = + mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0; + int incrementalFlag = + isNonIncremental + ? BackupTransport.FLAG_NON_INCREMENTAL + : BackupTransport.FLAG_INCREMENTAL; + int flags = userInitiatedFlag | incrementalFlag; + + mStatus = transport.performBackup(mCurrentPackage, backupData, flags); + } + + if (isNonIncremental + && mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { + // TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED is only valid if the backup was + // incremental, as if the backup is non-incremental there is no state to + // clear. This avoids us ending up in a retry loop if the transport always + // returns this code. + Slog.e(TAG, "Transport requested non-incremental but already the case"); + mBackupManagerService.addBackupTrace( + "Transport requested non-incremental but already the case, error"); + mStatus = BackupTransport.TRANSPORT_ERROR; + } + + mBackupManagerService.addBackupTrace("data delivered: " + mStatus); + if (mStatus == BackupTransport.TRANSPORT_OK) { + mBackupManagerService.addBackupTrace("finishing op on transport"); + mStatus = transport.finishBackup(); + mBackupManagerService.addBackupTrace("finished: " + mStatus); + } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { + mBackupManagerService.addBackupTrace("transport rejected package"); + } + } else { + if (MORE_DEBUG) { + Slog.i(TAG, "No backup data written, not calling transport"); + } + mBackupManagerService.addBackupTrace("no data to send"); + mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor, + BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND, + mCurrentPackage, + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); + } + + if (mStatus == BackupTransport.TRANSPORT_OK) { + // After successful transport, delete the now-stale data + // and juggle the files so that next time we supply the agent + // with the new state file it just created. + mBackupDataFile.delete(); + mNewStateFile.renameTo(mSavedStateFile); + BackupObserverUtils.sendBackupOnPackageResult( + mObserver, pkgName, BackupManager.SUCCESS); + EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, pkgName, size); + mBackupManagerService.logBackupComplete(pkgName); + } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { + // The transport has rejected backup of this specific package. Roll it + // back but proceed with running the rest of the queue. + mBackupDataFile.delete(); + mNewStateFile.delete(); + BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, + BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED); + EventLogTags.writeBackupAgentFailure(pkgName, "Transport rejected"); + } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { + BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, + BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED); + EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName); + + } else if (mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { + Slog.i(TAG, "Transport lost data, retrying package"); + mBackupManagerService.addBackupTrace( + "Transport lost data, retrying package:" + pkgName); + BackupManagerMonitorUtils.monitorEvent( + mMonitor, + BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, + mCurrentPackage, + BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, + /*extras=*/ null); + + mBackupDataFile.delete(); + mSavedStateFile.delete(); + mNewStateFile.delete(); + + // Immediately retry the package by adding it back to the front of the queue. + // We cannot add @pm@ to the queue because we back it up separately at the start + // of the backup pass in state BACKUP_PM. Instead we retry this state (see + // below). + if (!PACKAGE_MANAGER_SENTINEL.equals(pkgName)) { + mQueue.add(0, new BackupRequest(pkgName)); + } + + } else { + // Actual transport-level failure to communicate the data to the backend + BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, + BackupManager.ERROR_TRANSPORT_ABORTED); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName); + } + } catch (Exception e) { + BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName, + BackupManager.ERROR_TRANSPORT_ABORTED); + Slog.e(TAG, "Transport error backing up " + pkgName, e); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName); + mStatus = BackupTransport.TRANSPORT_ERROR; + } finally { + try { + if (backupData != null) { + backupData.close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error closing backup data file-descriptor"); + } + } + + final BackupState nextState; + if (mStatus == BackupTransport.TRANSPORT_OK + || mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { + // Success or single-package rejection. Proceed with the next app if any, + // otherwise we're done. + nextState = BackupState.RUNNING_QUEUE; + + } else if (mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) { + // We want to immediately retry the current package. + if (PACKAGE_MANAGER_SENTINEL.equals(pkgName)) { + nextState = BackupState.BACKUP_PM; + } else { + // This is an ordinary package so we will have added it back into the queue + // above. Thus, we proceed processing the queue. + nextState = BackupState.RUNNING_QUEUE; + } + + } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { + if (MORE_DEBUG) { + Slog.d(TAG, "Package " + mCurrentPackage.packageName + + " hit quota limit on key-value backup"); + } + if (mAgentBinder != null) { + try { + IBackupTransport transport = + mTransportClient.connectOrThrow("KVBT.handleAgentResult()"); + long quota = transport.getBackupQuota(mCurrentPackage.packageName, false); + mAgentBinder.doQuotaExceeded(size, quota); + } catch (Exception e) { + Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage()); + } + } + nextState = BackupState.RUNNING_QUEUE; + } else { + // Any other error here indicates a transport-level failure. That means + // we need to halt everything and reschedule everything for next time. + revertAndEndBackup(); + nextState = BackupState.FINAL; + } + + return nextState; + } + + /** + * Cancels this task. After this method returns there will be no more calls to the transport. + * + *

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. + * + * @param cancelAll MUST be {@code true}. Will be removed. + */ + @Override + public void handleCancel(boolean cancelAll) { + Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task"); + if (MORE_DEBUG) { + Slog.v(TAG, "Cancel received"); + } + mCancelled = true; + RemoteCall pendingCall = mPendingCall; + if (pendingCall != null) { + pendingCall.cancel(); + } + mCancelAcknowledged.block(); + } + + private void handleAgentTimeout() { + String packageName = getPackageNameForLog(); + Slog.i(TAG, "Agent " + packageName + " timed out"); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); + mBackupManagerService.addBackupTrace("timeout of " + packageName); + mMonitor = + BackupManagerMonitorUtils.monitorEvent( + mMonitor, + BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, + mCurrentPackage, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + BackupManagerMonitorUtils.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false)); + errorCleanup(); + } + + private void handleAgentCancelled() { + String packageName = getPackageNameForLog(); + Slog.i(TAG, "Cancel backing up " + packageName); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName); + mBackupManagerService.addBackupTrace("cancel of " + packageName); + errorCleanup(); + } + + private void finalizeCancelledBackup() { + mMonitor = + BackupManagerMonitorUtils.monitorEvent( + mMonitor, + BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL, + mCurrentPackage, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + BackupManagerMonitorUtils.putMonitoringExtra( + null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true)); + finalizeBackup(); + // finalizeBackup() may call the transport, so we only acknowledge the cancellation here. + mCancelAcknowledged.open(); + } + + private String getPackageNameForLog() { + return (mCurrentPackage != null) ? mCurrentPackage.packageName : "no_package_yet"; + } + + private void revertAndEndBackup() { + if (MORE_DEBUG) { + Slog.i(TAG, "Reverting backup queue, re-staging everything"); + } + mBackupManagerService.addBackupTrace("transport error; reverting"); + + // We want to reset the backup schedule based on whatever the transport suggests + // by way of retry/backoff time. + long delay; + try { + IBackupTransport transport = + mTransportClient.connectOrThrow("KVBT.revertAndEndBackup()"); + delay = transport.requestBackupTime(); + } catch (Exception e) { + Slog.w(TAG, "Unable to contact transport for recommended backoff: " + e); + delay = 0; // use the scheduler's default + } + KeyValueBackupJob.schedule(mBackupManagerService.getContext(), delay, + mBackupManagerService.getConstants()); + + for (BackupRequest request : mOriginalQueue) { + mBackupManagerService.dataChangedImpl(request.packageName); + } + } + + private void errorCleanup() { + mBackupDataFile.delete(); + mNewStateFile.delete(); + clearAgentState(); + } + + // Cleanup common to both success and failure cases + private void clearAgentState() { + try { + if (mSavedState != null) { + mSavedState.close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error closing old state file-descriptor"); + } + try { + if (mBackupData != null) { + mBackupData.close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error closing backup data file-descriptor"); + } + try { + if (mNewState != null) { + mNewState.close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error closing new state file-descriptor"); + } + synchronized (mBackupManagerService.getCurrentOpLock()) { + // Current-operation callback handling requires the validity of these various + // bits of internal state as an invariant of the operation still being live. + // This means we make sure to clear all of the state in unison inside the lock. + mSavedState = mBackupData = mNewState = null; + } + + // If this was a pseudo-package there's no associated Activity Manager state + if (mCurrentPackage.applicationInfo != null) { + mBackupManagerService.addBackupTrace("unbinding " + mCurrentPackage.packageName); + mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo); + } + } + + private RemoteResult remoteCall(RemoteCallable remoteCallable, long timeoutMs) + throws RemoteException { + mPendingCall = new RemoteCall(mCancelled, remoteCallable, timeoutMs); + RemoteResult result = mPendingCall.call(); + if (MORE_DEBUG) { + Slog.v(TAG, "Agent call returned " + result); + } + mPendingCall = null; + return result; + } + + private enum BackupState { + INITIAL, + BACKUP_PM, + RUNNING_QUEUE, + CANCELLED, + FINAL + } +} diff --git a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java index ea9967b6ea43..2e0ae02664b9 100644 --- a/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java +++ b/services/robotests/src/com/android/server/backup/BackupManagerServiceTest.java @@ -45,7 +45,7 @@ import android.os.PowerManager; import android.os.PowerSaveState; import android.platform.test.annotations.Presubmit; import android.provider.Settings; -import com.android.server.backup.internal.BackupRequest; +import com.android.server.backup.keyvalue.BackupRequest; import com.android.server.backup.testing.BackupManagerServiceTestUtils; import com.android.server.backup.testing.TransportData; import com.android.server.backup.testing.TransportTestUtils.TransportMock; diff --git a/services/robotests/src/com/android/server/backup/KeyValueBackupTaskTest.java b/services/robotests/src/com/android/server/backup/KeyValueBackupTaskTest.java deleted file mode 100644 index 56f5f15371ca..000000000000 --- a/services/robotests/src/com/android/server/backup/KeyValueBackupTaskTest.java +++ /dev/null @@ -1,1989 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.backup; - -import static android.app.backup.BackupManager.ERROR_AGENT_FAILURE; -import static android.app.backup.BackupManager.ERROR_BACKUP_NOT_ALLOWED; -import static android.app.backup.BackupManager.ERROR_PACKAGE_NOT_FOUND; -import static android.app.backup.BackupManager.ERROR_TRANSPORT_ABORTED; -import static android.app.backup.BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED; -import static android.app.backup.BackupManager.SUCCESS; -import static android.app.backup.ForwardingBackupAgent.forward; - -import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createBackupWakeLock; -import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createInitializedBackupManagerService; -import static com.android.server.backup.testing.BackupManagerServiceTestUtils.setUpBackupManagerServiceBasics; -import static com.android.server.backup.testing.BackupManagerServiceTestUtils.setUpBinderCallerAndApplicationAsSystem; -import static com.android.server.backup.testing.PackageData.PM_PACKAGE; -import static com.android.server.backup.testing.PackageData.fullBackupPackage; -import static com.android.server.backup.testing.PackageData.keyValuePackage; -import static com.android.server.backup.testing.TestUtils.assertEventLogged; -import static com.android.server.backup.testing.TestUtils.uncheck; -import static com.android.server.backup.testing.TransportData.backupTransport; -import static com.android.server.backup.testing.Utils.oneTimeIterable; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.intThat; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.shadow.api.Shadow.extract; - -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toCollection; -import static java.util.stream.Collectors.toList; - -import android.annotation.Nullable; -import android.app.Application; -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.IBackupManager; -import android.app.backup.IBackupManagerMonitor; -import android.app.backup.IBackupObserver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.DeadObjectException; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.os.PowerManager; -import android.os.RemoteException; -import android.platform.test.annotations.Presubmit; -import android.util.Pair; - -import com.android.internal.backup.IBackupTransport; -import com.android.server.EventLogTags; -import com.android.server.backup.internal.BackupHandler; -import com.android.server.backup.internal.BackupRequest; -import com.android.server.backup.internal.OnTaskFinishedListener; -import com.android.server.backup.internal.KeyValueBackupTask; -import com.android.server.backup.testing.PackageData; -import com.android.server.backup.testing.TransportData; -import com.android.server.backup.testing.TransportTestUtils; -import com.android.server.backup.testing.TransportTestUtils.TransportMock; -import com.android.server.backup.testing.Utils; -import com.android.server.backup.transport.TransportClient; -import com.android.server.testing.FrameworkRobolectricTestRunner; -import com.android.server.testing.SystemLoaderClasses; -import com.android.server.testing.SystemLoaderPackages; -import com.android.server.testing.shadows.FrameworkShadowLooper; -import com.android.server.testing.shadows.ShadowBackupDataInput; -import com.android.server.testing.shadows.ShadowBackupDataOutput; -import com.android.server.testing.shadows.ShadowEventLog; - -import com.google.common.truth.IterableSubject; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatcher; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowApplication; -import org.robolectric.shadows.ShadowLooper; -import org.robolectric.shadows.ShadowPackageManager; -import org.robolectric.shadows.ShadowQueuedWork; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -// TODO: When returning to RUNNING_QUEUE vs FINAL, RUNNING_QUEUE sets status = OK. Why? Verify? -// TODO: Check queue in general, behavior w/ multiple packages -// TODO: Test PM invocation -@RunWith(FrameworkRobolectricTestRunner.class) -@Config( - manifest = Config.NONE, - sdk = 26, - shadows = { - FrameworkShadowLooper.class, - ShadowBackupDataInput.class, - ShadowBackupDataOutput.class, - ShadowEventLog.class, - ShadowQueuedWork.class, - }) -@SystemLoaderPackages({"com.android.server.backup", "android.app.backup"}) -@SystemLoaderClasses({IBackupTransport.class, IBackupAgent.class, PackageInfo.class}) -@Presubmit -public class KeyValueBackupTaskTest { - private static final PackageData PACKAGE_1 = keyValuePackage(1); - private static final PackageData PACKAGE_2 = keyValuePackage(2); - - @Mock private TransportManager mTransportManager; - @Mock private DataChangedJournal mOldJournal; - @Mock private IBackupObserver mObserver; - @Mock private IBackupManagerMonitor mMonitor; - @Mock private OnTaskFinishedListener mListener; - private BackupManagerService mBackupManagerService; - private TransportData mTransport; - private ShadowLooper mShadowBackupLooper; - private Handler mBackupHandler; - private PowerManager.WakeLock mWakeLock; - private ShadowPackageManager mShadowPackageManager; - private FakeIBackupManager mBackupManager; - private File mBaseStateDir; - private File mDataDir; - private Application mApplication; - private ShadowApplication mShadowApplication; - private FrameworkShadowLooper mShadowMainLooper; - private Context mContext; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - mTransport = backupTransport(); - - mApplication = RuntimeEnvironment.application; - mShadowApplication = shadowOf(mApplication); - mContext = mApplication; - - mShadowMainLooper = extract(Looper.getMainLooper()); - - File cacheDir = mApplication.getCacheDir(); - // Corresponds to /data/backup - mBaseStateDir = new File(cacheDir, "base_state"); - // Corresponds to /cache/backup_stage - mDataDir = new File(cacheDir, "data"); - // We create here simulating init.rc - mDataDir.mkdirs(); - assertThat(mDataDir.isDirectory()).isTrue(); - - PackageManager packageManager = mApplication.getPackageManager(); - mShadowPackageManager = shadowOf(packageManager); - - mWakeLock = createBackupWakeLock(mApplication); - - mBackupManager = spy(FakeIBackupManager.class); - - // Needed to be able to use a real BMS instead of a mock - setUpBinderCallerAndApplicationAsSystem(mApplication); - mBackupManagerService = - spy( - createInitializedBackupManagerService( - mContext, mBaseStateDir, mDataDir, mTransportManager)); - setUpBackupManagerServiceBasics( - mBackupManagerService, - mApplication, - mTransportManager, - packageManager, - mBackupManagerService.getBackupHandler(), - mWakeLock, - mBackupManagerService.getAgentTimeoutParameters()); - when(mBackupManagerService.getBaseStateDir()).thenReturn(mBaseStateDir); - when(mBackupManagerService.getDataDir()).thenReturn(mDataDir); - when(mBackupManagerService.getBackupManagerBinder()).thenReturn(mBackupManager); - - mBackupHandler = mBackupManagerService.getBackupHandler(); - mShadowBackupLooper = shadowOf(mBackupHandler.getLooper()); - ShadowEventLog.setUp(); - } - - @Test - public void testRunTask_whenQueueEmpty_updatesBookkeeping() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(mBackupManagerService.getCurrentToken()).thenReturn(0L); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - - runTask(task); - - assertThat(mBackupManagerService.getPendingInits()).isEmpty(); - assertThat(mBackupManagerService.isBackupRunning()).isFalse(); - assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0); - verify(mOldJournal).delete(); - } - - @Test - public void testRunTask_whenQueueEmpty_releasesWakeLock() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(mBackupManagerService.getCurrentToken()).thenReturn(0L); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - - runTask(task); - - assertThat(mWakeLock.isHeld()).isFalse(); - } - - @Test - public void testRunTask_whenQueueEmpty_doesNotProduceData() throws Exception { - TransportMock transportMock = setUpTransport(mTransport); - when(mBackupManagerService.getCurrentToken()).thenReturn(0L); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - - runTask(task); - - assertDirectory(getStateDirectory(mTransport)).isEmpty(); - assertDirectory(mDataDir.toPath()).isEmpty(); - } - - @Test - public void testRunTask_whenQueueEmpty_doesNotCallTransport() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(mBackupManagerService.getCurrentToken()).thenReturn(0L); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - - runTask(task); - - verify(transportMock.transport, never()).initializeDevice(); - verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); - verify(transportMock.transport, never()).finishBackup(); - } - - @Test - public void testRunTask_whenQueueEmpty_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(mBackupManagerService.getCurrentToken()).thenReturn(0L); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver, never()).onResult(any(), anyInt()); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenQueueEmpty_doesNotChangeStateFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, true); - Files.write(getStateFile(mTransport, PM_PACKAGE), "pmState".getBytes()); - Files.write(getStateFile(mTransport, PACKAGE_1), "packageState".getBytes()); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PM_PACKAGE))) - .isEqualTo("pmState".getBytes()); - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("packageState".getBytes()); - } - - @Test - public void testRunTask_whenOnePackageAndTransportUnavailable() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport.unavailable()); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); - assertBackupPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenOnePackage_logsBackupStartEvent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertEventLogged(EventLogTags.BACKUP_START, mTransport.transportName); - } - - @Test - public void testRunTask_whenOnePackage_releasesWakeLock() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertThat(mWakeLock.isHeld()).isFalse(); - } - - @Test - public void testRunTask_whenOnePackage_updatesBookkeeping() throws Exception { - // Transport has to be initialized to not reset current token - TransportMock transportMock = setUpInitializedTransport(mTransport); - mBackupManagerService.setCurrentToken(0L); - when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertThat(mBackupManagerService.getPendingInits()).isEmpty(); - assertThat(mBackupManagerService.isBackupRunning()).isFalse(); - assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0); - assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(1234L); - verify(mBackupManagerService).writeRestoreTokens(); - verify(mOldJournal).delete(); - } - - @Test - public void testRunTask_whenPackageWithOldStateAndIncremental_passesOldStateToAgent() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - false, - PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertThat(agentMock.oldState).isEqualTo("oldState".getBytes()); - } - - @Test - public void testRunTask_whenPackageWithOldStateAndNonIncremental_passesEmptyOldStateToAgent() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - true, - PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertThat(agentMock.oldState).isEqualTo(new byte[0]); - } - - @Test - public void testRunTask_whenNonPmPackageAndNonIncremental_doesNotBackUpPm() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); - when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - true, - PACKAGE_1); - - runTask(task); - - verify(pmAgent, never()).onBackup(any(), any(), any()); - } - - @Test - public void testRunTask_whenNonPmPackageAndPmAndNonIncremental_backsUpPm() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); - when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - true, - PACKAGE_1, - PM_PACKAGE); - - runTask(task); - - verify(pmAgent).onBackup(any(), any(), any()); - } - - @Test - public void testRunTask_whenNonPmPackageAndIncremental_backsUpPm() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - PackageManagerBackupAgent pmAgent = spy(createPmAgent()); - when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - false, - PACKAGE_1); - - runTask(task); - - verify(pmAgent).onBackup(any(), any(), any()); - } - - @Test - public void testRunTask_whenOnePackageAndNoPmState_initializesTransportAndResetsState() - throws Exception { - TransportMock transportMock = setUpTransport(mTransport); - // Need 2 packages to be able to verify state of package not involved in the task - setUpAgentsWithData(PACKAGE_1, PACKAGE_2); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - deletePmStateFile(); - Files.write(getStateFile(mTransport, PACKAGE_2), "package2State".getBytes()); - - runTask(task); - - verify(transportMock.transport).initializeDevice(); - verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile()); - // Verifying that it deleted all the states (can't verify package 1 because it generated a - // new state in this task execution) - assertThat(Files.exists(getStateFile(mTransport, PACKAGE_2))).isFalse(); - assertEventLogged(EventLogTags.BACKUP_INITIALIZE); - } - - @Test - public void testRunTask_whenOnePackageAndWithPmState_doesNotInitializeTransportOrResetState() - throws Exception { - TransportMock transportMock = setUpTransport(mTransport); - setUpAgentsWithData(PACKAGE_1, PACKAGE_2); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - createPmStateFile(); - Files.write(getStateFile(mTransport, PACKAGE_2), "package2State".getBytes()); - - runTask(task); - - verify(transportMock.transport, never()).initializeDevice(); - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_2))) - .isEqualTo("package2State".getBytes()); - } - - @Test - public void testRunTask_whenTransportReturnsErrorForInitialization() throws Exception { - TransportMock transportMock = setUpTransport(mTransport); - when(transportMock.transport.initializeDevice()) - .thenReturn(BackupTransport.TRANSPORT_ERROR); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - deletePmStateFile(); - - runTask(task); - - // First for initialization and second because of the transport failure - verify(mBackupManagerService, times(2)) - .resetBackupState(getStateDirectory(mTransport).toFile()); - verify(agentMock.agent, never()).onBackup(any(), any(), any()); - verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); - assertBackupPendingFor(PACKAGE_1); - assertEventLogged(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); - } - - @Test - public void testRunTask_whenTransportThrowsDuringInitialization() throws Exception { - TransportMock transportMock = setUpTransport(mTransport); - when(transportMock.transport.initializeDevice()).thenThrow(RemoteException.class); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - deletePmStateFile(); - - runTask(task); - - // First for initialization and second because of the transport failure - verify(mBackupManagerService, times(2)) - .resetBackupState(getStateDirectory(mTransport).toFile()); - verify(agentMock.agent, never()).onBackup(any(), any(), any()); - verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); - assertBackupPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenPackageNotEligibleForBackup() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1.backupNotAllowed()); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(agentMock.agent, never()).onBackup(any(), any(), any()); - verify(transportMock.transport, never()) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_BACKUP_NOT_ALLOWED); - verify(mObserver).backupFinished(SUCCESS); - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenPackageDoesFullBackup() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - PackageData packageData = fullBackupPackage(1); - AgentMock agentMock = setUpAgentWithData(packageData); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, packageData); - - runTask(task); - - verify(agentMock.agent, never()).onBackup(any(), any(), any()); - verify(agentMock.agent, never()).onFullBackup(any()); - verify(mObserver).onResult(packageData.packageName, ERROR_BACKUP_NOT_ALLOWED); - verify(mObserver).backupFinished(SUCCESS); - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenPackageIsStopped() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1.stopped()); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(agentMock.agent, never()).onBackup(any(), any(), any()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_BACKUP_NOT_ALLOWED); - verify(mObserver).backupFinished(SUCCESS); - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenPackageUnknown() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - // Not calling setUpAgent() - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(transportMock.transport, never()) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_PACKAGE_NOT_FOUND); - verify(mObserver).backupFinished(SUCCESS); - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenCallingAgent_setsWakeLockWorkSource() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // In production (for non-system agents) the call is asynchronous, but here is - // synchronous, so it's fine to verify here. - // Verify has set work source and hasn't unset yet. - verify(mBackupManagerService) - .setWorkSource( - argThat(workSource -> workSource.get(0) == PACKAGE_1.uid)); - verify(mBackupManagerService, never()).setWorkSource(null); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - // More verifications inside agent call above - verify(mBackupManagerService).setWorkSource(null); - } - - /** - * Agent unavailable means {@link BackupManagerService#bindToAgentSynchronous(ApplicationInfo, - * int)} returns {@code null}. - * - * @see #setUpAgent(PackageData) - */ - @Test - public void testRunTask_whenAgentUnavailable() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgent(PACKAGE_1.unavailable()); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mBackupManagerService).setWorkSource(null); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); - verify(mObserver).backupFinished(BackupManager.SUCCESS); - } - - @Test - public void testRunTask_whenBindToAgentThrowsSecurityException() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgent(PACKAGE_1); - doThrow(SecurityException.class) - .when(mBackupManagerService) - .bindToAgentSynchronous(argThat(applicationInfo(PACKAGE_1)), anyInt()); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mBackupManagerService).setWorkSource(null); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); - verify(mObserver).backupFinished(BackupManager.SUCCESS); - } - - @Test - public void testRunTask_whenTransportGetBackupQuotaThrows_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) - .thenThrow(DeadObjectException.class); - setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mObserver, never()).onResult(eq(PACKAGE_1.packageName), anyInt()); - verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); - verify(mListener).onFinished(any()); - } - - @Test - public void testRunTask_whenTransportGetBackupQuotaThrows_cleansUp() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) - .thenThrow(DeadObjectException.class); - setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mBackupManagerService).setWorkSource(null); - verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); - } - - @Test - public void testRunTask_whenTransportGetBackupQuotaThrows_doesNotTouchFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) - .thenThrow(DeadObjectException.class); - setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "packageState".getBytes()); - - runTask(task); - - assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("packageState".getBytes()); - } - - @Test - public void testRunTask_whenTransportGetBackupQuotaThrows_revertsOperation() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) - .thenThrow(DeadObjectException.class); - setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(transportMock.transport).requestBackupTime(); - assertBackupPendingFor(PACKAGE_1); - assertThat(KeyValueBackupJob.isScheduled()).isTrue(); - } - - /** - * For local agents the exception is thrown in our stack, so it hits the catch clause around - * invocation earlier than the {@link KeyValueBackupTask#operationComplete(long)} code-path, - * invalidating the latter. Note that this happens because {@link - * BackupManagerService#opComplete(int, long)} schedules the actual execution to the backup - * handler. - */ - @Test - public void testRunTask_whenLocalAgentOnBackupThrows() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - throw new RuntimeException(); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mBackupManagerService).setWorkSource(null); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); - verify(mObserver).backupFinished(SUCCESS); - assertEventLogged( - EventLogTags.BACKUP_AGENT_FAILURE, - PACKAGE_1.packageName, - new RuntimeException().toString()); - assertBackupPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenTransportProvidesFlags_passesThemToTheAgent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; - when(transportMock.transport.getTransportFlags()).thenReturn(flags); - AgentMock agentMock = setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(agentMock.agent) - .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); - } - - @Test - public void testRunTask_whenTransportDoesNotProvidesFlags() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(agentMock.agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any()); - } - - @Test - public void testRunTask_whenTransportProvidesFlagsAndMultipleAgents_passesToAll() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; - when(transportMock.transport.getTransportFlags()).thenReturn(flags); - List agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2); - BackupAgent agent1 = agentMocks.get(0).agent; - BackupAgent agent2 = agentMocks.get(1).agent; - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - PACKAGE_1, - PACKAGE_2); - - runTask(task); - - verify(agent1).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); - verify(agent2).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); - } - - @Test - public void testRunTask_whenTransportChangeFlagsAfterTaskCreation() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; - when(transportMock.transport.getTransportFlags()).thenReturn(flags); - - runTask(task); - - verify(agentMock.agent) - .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); - } - - @Test - public void testRunTask_whenAgentUsesProhibitedKey_failsAgent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(agentMock.agentBinder).fail(any()); - verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); - } - - @Test - public void testRunTask_whenAgentUsesProhibitedKey_updatesAndCleansUpFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("oldState".getBytes()); - assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - } - - @Test - public void testRunTask_whenAgentUsesProhibitedKey_doesNotCallTransport() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - verify(transportMock.transport, never()) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - } - - @Test - public void testRunTask_whenAgentUsesProhibitedKey_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenAgentUsesProhibitedKey_logsAgentFailureEvent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertEventLogged(EventLogTags.BACKUP_AGENT_FAILURE, PACKAGE_1.packageName, "bad key"); - } - - @Test - public void testRunTask_whenFirstAgentUsesProhibitedKeyButLastAgentUsesPermittedKey() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - List agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2); - AgentMock agentMock1 = agentMocks.get(0); - AgentMock agentMock2 = agentMocks.get(1); - agentOnBackupDo( - agentMock1, - (oldState, dataOutput, newState) -> { - char prohibitedChar = 0xff00; - writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - agentOnBackupDo( - agentMock2, - (oldState, dataOutput, newState) -> { - writeData(dataOutput, "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - PACKAGE_1, - PACKAGE_2); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); - verify(agentMock1.agentBinder).fail(any()); - verify(mObserver).onResult(PACKAGE_2.packageName, SUCCESS); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenAgentDoesNotWriteData_doesNotCallTransport() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // No-op - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(transportMock.transport, never()) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - } - - @Test - public void testRunTask_whenAgentDoesNotWriteData_logsEvents() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // No-op - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertEventLogged(EventLogTags.BACKUP_PACKAGE, PACKAGE_1.packageName, 0L); - verify(mBackupManagerService).logBackupComplete(PACKAGE_1.packageName); - } - - @Test - public void testRunTask_whenAgentDoesNotWriteData_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // No-op - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenAgentDoesNotWriteData_updatesBookkeeping() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // No-op - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenAgentDoesNotWriteData_updatesAndCleansUpFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // No-op - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))).isEqualTo(new byte[0]); - assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - } - - @Test - public void testRunTask_whenAgentWritesData_callsTransportPerformBackupWithAgentData() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - Path backupDataPath = createTemporaryFile(); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .then(copyBackupDataTo(backupDataPath)); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - writeData(dataOutput, "key1", "data1".getBytes()); - writeData(dataOutput, "key2", "data2".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(transportMock.transport) - .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); - // Now verify data sent - try (FileInputStream inputStream = new FileInputStream(backupDataPath.toFile())) { - BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); - assertDataHasKeyValue(backupData, "key1", "data1".getBytes()); - assertDataHasKeyValue(backupData, "key2", "data2".getBytes()); - assertThat(backupData.readNextHeader()).isFalse(); - } - } - - @Test - public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_OK); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - // First for PM, then for the package - verify(transportMock.transport, times(2)).finishBackup(); - } - - @Test - public void testRunTask_whenFinishBackupSucceeds_updatesAndCleansUpFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - writeData(dataOutput, "key", "data".getBytes()); - writeState(newState, "newState".getBytes()); - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("newState".getBytes()); - assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - } - - @Test - public void testRunTask_whenFinishBackupSucceeds_logsBackupPackageEvent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - Path backupData = createTemporaryFile(); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .then(copyBackupDataTo(backupData)); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertEventLogged( - EventLogTags.BACKUP_PACKAGE, PACKAGE_1.packageName, Files.size(backupData)); - } - - @Test - public void testRunTask_whenFinishBackupSucceeds_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mBackupManagerService).logBackupComplete(PACKAGE_1.packageName); - verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); - verify(mObserver).backupFinished(SUCCESS); - verify(mListener).onFinished(any()); - } - - @Test - public void testRunTask_whenFinishBackupSucceeds_updatesBookkeeping() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenTransportRejectsPackage_doesNotCallFinishBackup() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - // Called only for PM - verify(transportMock.transport, times(1)).finishBackup(); - } - - @Test - public void testRunTask_whenTransportRejectsPackage_updatesAndCleansUpFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("oldState".getBytes()); - assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - } - - @Test - public void testRunTask_whenTransportRejectsPackage_logsAgentFailureEvent() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertEventLogged( - EventLogTags.BACKUP_AGENT_FAILURE, PACKAGE_1.packageName, "Transport rejected"); - } - - @Test - public void testRunTask_whenTransportRejectsPackage_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); - verify(mObserver).backupFinished(SUCCESS); - verify(mListener).onFinished(any()); - } - - @Test - public void testRunTask_whenTransportRejectsPackage_updatesBookkeeping() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenTransportRejectsFirstPackageButLastSucceeds() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_2)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_OK); - setUpAgentsWithData(PACKAGE_1, PACKAGE_2); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - PACKAGE_1, - PACKAGE_2); - - runTask(task); - - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); - verify(mObserver).onResult(PACKAGE_2.packageName, SUCCESS); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenTransportRejectsLastPackageButFirstSucceeds() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_OK); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_2)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); - setUpAgentsWithData(PACKAGE_1, PACKAGE_2); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - PACKAGE_1, - PACKAGE_2); - - runTask(task); - - verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); - verify(mObserver).onResult(PACKAGE_2.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenTransportReturnsQuotaExceeded() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) - .thenReturn(1234L); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_QUOTA_EXCEEDED); - AgentMock agentMock = setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mObserver) - .onResult(PACKAGE_1.packageName, BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED); - verify(mObserver).backupFinished(SUCCESS); - verify(agentMock.agent).onQuotaExceeded(anyLong(), eq(1234L)); - assertEventLogged(EventLogTags.BACKUP_QUOTA_EXCEEDED, PACKAGE_1.packageName); - assertBackupNotPendingFor(PACKAGE_1); - } - - @Test - public void testRunTask_whenNonIncrementalAndTransportRequestsNonIncremental() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - true, - PACKAGE_1); - // Delete to be non-incremental - Files.deleteIfExists(getStateFile(mTransport, PACKAGE_1)); - - runTask(task); - - // Error because it was non-incremental already, so transport can't request it - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_ABORTED); - verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); - } - - @Test - public void testRunTask_whenIncrementalAndTransportRequestsNonIncremental() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - Path incrementalData = createTemporaryFile(); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), - any(), - intThat(flags -> (flags & BackupTransport.FLAG_INCREMENTAL) != 0))) - .thenAnswer( - copyBackupDataAndReturn( - incrementalData, - BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)); - Path nonIncrementalData = createTemporaryFile(); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), - any(), - intThat(flags -> (flags & BackupTransport.FLAG_NON_INCREMENTAL) != 0))) - .thenAnswer( - copyBackupDataAndReturn(nonIncrementalData, BackupTransport.TRANSPORT_OK)); - AgentMock agentMock = setUpAgent(PACKAGE_1); - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - // agentMock.oldState has already been updated by now. - if (agentMock.oldState.length > 0) { - writeData(dataOutput, "key", "dataForIncremental".getBytes()); - writeState(newState, "stateForIncremental".getBytes()); - } else { - writeData(dataOutput, "key", "dataForNonIncremental".getBytes()); - writeState(newState, "stateForNonIncremental".getBytes()); - } - }); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, - mTransport.transportDirName, - false, - PACKAGE_1); - // Write state to be incremental - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - verify(agentMock.agent, times(2)).onBackup(any(), any(), any()); - byte[] oldStateDuringIncremental = agentMock.oldStateHistory.get(0); - byte[] oldStateDuringNonIncremental = agentMock.oldStateHistory.get(1); - assertThat(oldStateDuringIncremental).isEqualTo("oldState".getBytes()); - assertThat(oldStateDuringNonIncremental).isEqualTo(new byte[0]); - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("stateForNonIncremental".getBytes()); - try (FileInputStream inputStream = new FileInputStream(incrementalData.toFile())) { - BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); - assertDataHasKeyValue(backupData, "key", "dataForIncremental".getBytes()); - assertThat(backupData.readNextHeader()).isFalse(); - } - try (FileInputStream inputStream = new FileInputStream(nonIncrementalData.toFile())) { - BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); - assertDataHasKeyValue(backupData, "key", "dataForNonIncremental".getBytes()); - assertThat(backupData.readNextHeader()).isFalse(); - } - verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); - verify(mObserver).backupFinished(SUCCESS); - } - - @Test - public void testRunTask_whenTransportReturnsError_notifiesCorrectly() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_ERROR); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_ABORTED); - verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); - } - - @Test - public void testRunTask_whenTransportReturnsError_logsBackupTransportFailureEvent() - throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_ERROR); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - assertEventLogged(EventLogTags.BACKUP_TRANSPORT_FAILURE, PACKAGE_1.packageName); - } - - @Test - public void testRunTask_whenTransportReturnsError_revertsOperation() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_ERROR); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(transportMock.transport).requestBackupTime(); - assertBackupPendingFor(PACKAGE_1); - assertThat(KeyValueBackupJob.isScheduled()).isTrue(); - } - - @Test - public void testRunTask_whenTransportReturnsError_updatesAndCleansUpFiles() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.performBackup( - argThat(packageInfo(PACKAGE_1)), any(), anyInt())) - .thenReturn(BackupTransport.TRANSPORT_ERROR); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); - - runTask(task); - - assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) - .isEqualTo("oldState".getBytes()); - // TODO: These should be true (Bug) - // assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); - // assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); - } - - @Test - public void testRunTask_whenTransportGetBackupQuotaThrowsForPm() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - when(transportMock.transport.getBackupQuota(PM_PACKAGE.packageName, false)) - .thenThrow(DeadObjectException.class); - setUpAgentWithData(PACKAGE_1); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); - assertEventLogged( - EventLogTags.BACKUP_AGENT_FAILURE, - PM_PACKAGE.packageName, - new DeadObjectException().toString()); - } - - @Test - public void testRunTask_whenPmAgentFails() throws Exception { - TransportMock transportMock = setUpInitializedTransport(mTransport); - PackageManagerBackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); - when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); - KeyValueBackupTask task = - createKeyValueBackupTask( - transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); - - runTask(task); - - verify(mListener).onFinished(any()); - verify(mObserver).backupFinished(eq(ERROR_TRANSPORT_ABORTED)); - assertEventLogged( - EventLogTags.BACKUP_AGENT_FAILURE, - PM_PACKAGE.packageName, - new RuntimeException().toString()); - } - - private void runTask(KeyValueBackupTask task) { - // Pretend we are not on the main-thread to prevent RemoteCall from complaining - mShadowMainLooper.setCurrentThread(false); - task.run(); - mShadowMainLooper.reset(); - assertTaskPostConditions(); - } - - private TransportMock setUpTransport(TransportData transport) throws Exception { - TransportMock transportMock = - TransportTestUtils.setUpTransport(mTransportManager, transport); - Files.createDirectories(getStateDirectory(transport)); - return transportMock; - } - - /** Sets up the transport and writes a PM state file in the transport state directory. */ - private TransportMock setUpInitializedTransport(TransportData transport) throws Exception { - TransportMock transportMock = setUpTransport(transport); - createPmStateFile(transport); - return transportMock; - } - - private Path getStateDirectory(TransportData transport) { - return mBaseStateDir.toPath().resolve(transport.transportDirName); - } - - private Path getStateFile(TransportData transport, PackageData packageData) { - return getStateDirectory(transport).resolve(packageData.packageName); - } - - private Path getTemporaryStateFile(TransportData transport, PackageData packageData) { - return getStateDirectory(transport) - .resolve(packageData.packageName + KeyValueBackupTask.NEW_STATE_FILE_SUFFIX); - } - - private Path getStagingDirectory() { - return mDataDir.toPath(); - } - - private Path getStagingFile(PackageData packageData) { - return getStagingDirectory() - .resolve(packageData.packageName + KeyValueBackupTask.STAGING_FILE_SUFFIX); - } - - private List setUpAgents(PackageData... packageNames) { - return Stream.of(packageNames).map(this::setUpAgent).collect(toList()); - } - - private AgentMock setUpAgent(PackageData packageData) { - try { - mShadowPackageManager.setApplicationEnabledSetting( - packageData.packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); - PackageInfo packageInfo = getPackageInfo(packageData); - mShadowPackageManager.addPackage(packageInfo); - mShadowApplication.sendBroadcast(getPackageAddedIntent(packageData)); - // Run the backup looper because on the receiver we post MSG_SCHEDULE_BACKUP_PACKAGE - mShadowBackupLooper.runToEndOfTasks(); - BackupAgent backupAgent = spy(BackupAgent.class); - IBackupAgent backupAgentBinder = - spy(IBackupAgent.Stub.asInterface(backupAgent.onBind())); - // Don't crash our only process (in production code this would crash the app, not us) - doNothing().when(backupAgentBinder).fail(any()); - if (packageData.available) { - doReturn(backupAgentBinder) - .when(mBackupManagerService) - .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt()); - } else { - doReturn(null) - .when(mBackupManagerService) - .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt()); - } - return new AgentMock(backupAgentBinder, backupAgent); - } catch (RemoteException e) { - // Never happens, compiler happy - throw new AssertionError(e); - } - } - - private PackageInfo getPackageInfo(PackageData packageData) { - PackageInfo packageInfo = new PackageInfo(); - packageInfo.packageName = packageData.packageName; - packageInfo.applicationInfo = new ApplicationInfo(); - packageInfo.applicationInfo.uid = packageData.uid; - packageInfo.applicationInfo.flags = packageData.flags(); - packageInfo.applicationInfo.backupAgentName = packageData.agentName; - packageInfo.applicationInfo.packageName = packageData.packageName; - return packageInfo; - } - - private Intent getPackageAddedIntent(PackageData packageData) { - Intent intent = - new Intent( - Intent.ACTION_PACKAGE_ADDED, - Uri.parse("package:" + packageData.packageName)); - intent.putExtra(Intent.EXTRA_UID, packageData.uid); - intent.putExtra(Intent.EXTRA_REPLACING, false); - intent.putExtra(Intent.EXTRA_USER_HANDLE, 0); - return intent; - } - - private List setUpAgentsWithData(PackageData... packages) { - return Stream.of(packages).map(this::setUpAgentWithData).collect(toList()); - } - - private AgentMock setUpAgentWithData(PackageData packageData) { - AgentMock agentMock = setUpAgent(packageData); - String packageName = packageData.packageName; - uncheck( - () -> - agentOnBackupDo( - agentMock, - (oldState, dataOutput, newState) -> { - writeData(dataOutput, "key", ("data" + packageName).getBytes()); - writeState(newState, ("state" + packageName).getBytes()); - })); - return agentMock; - } - - private KeyValueBackupTask createKeyValueBackupTask( - TransportClient transportClient, String transportDirName, PackageData... packages) { - return createKeyValueBackupTask(transportClient, transportDirName, false, packages); - } - - private KeyValueBackupTask createKeyValueBackupTask( - TransportClient transportClient, - String transportDirName, - boolean nonIncremental, - PackageData... packages) { - ArrayList keyValueBackupRequests = - Stream.of(packages) - .map(packageData -> packageData.packageName) - .map(BackupRequest::new) - .collect(toCollection(ArrayList::new)); - mBackupManagerService.getPendingBackups().clear(); - // mOldJournal is a mock, but it would be the value returned by BMS.getJournal() now - mBackupManagerService.setJournal(null); - mWakeLock.acquire(); - KeyValueBackupTask task = - new KeyValueBackupTask( - mBackupManagerService, - transportClient, - transportDirName, - keyValueBackupRequests, - mOldJournal, - mObserver, - mMonitor, - mListener, - emptyList(), - /* userInitiated */ false, - nonIncremental); - mBackupManager.setUp(mBackupHandler, task); - return task; - } - - private PackageManagerBackupAgent createPmAgent() { - PackageManagerBackupAgent pmAgent = - new PackageManagerBackupAgent(mApplication.getPackageManager()); - pmAgent.attach(mApplication); - pmAgent.onCreate(); - return pmAgent; - } - - /** - * Returns an implementation of PackageManagerBackupAgent that throws RuntimeException in {@link - * BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)} - */ - private PackageManagerBackupAgent createThrowingPmAgent(RuntimeException exception) { - PackageManagerBackupAgent pmAgent = - new ThrowingPackageManagerBackupAgent(mApplication.getPackageManager(), exception); - pmAgent.attach(mApplication); - pmAgent.onCreate(); - return pmAgent; - } - - /** Matches {@link PackageInfo} whose package name is {@code packageData.packageName}. */ - private static ArgumentMatcher packageInfo(PackageData packageData) { - // We have to test for packageInfo nulity because of Mockito's own stubbing with argThat(). - // E.g. if you do: - // - // 1. when(object.method(argThat(str -> str.equals("foo")))).thenReturn(0) - // 2. when(object.method(argThat(str -> str.equals("bar")))).thenReturn(2) - // - // The second line will throw NPE because it will call lambda 1 with null, since argThat() - // returns null. So we guard against that by checking for null. - return packageInfo -> - packageInfo != null && packageData.packageName.equals(packageInfo.packageName); - } - - /** Matches {@link ApplicationInfo} whose package name is {@code packageData.packageName}. */ - private static ArgumentMatcher applicationInfo(PackageData packageData) { - return applicationInfo -> - applicationInfo != null - && packageData.packageName.equals(applicationInfo.packageName); - } - - private static ArgumentMatcher dataOutputWithTransportFlags(int flags) { - return dataOutput -> dataOutput.getTransportFlags() == flags; - } - - private static void writeData(BackupDataOutput dataOutput, String key, byte[] data) - throws IOException { - dataOutput.writeEntityHeader(key, data.length); - dataOutput.writeEntityData(data, data.length); - } - - private static void writeState(ParcelFileDescriptor newState, byte[] state) throws IOException { - OutputStream outputStream = new FileOutputStream(newState.getFileDescriptor()); - outputStream.write(state); - outputStream.flush(); - } - - /** - * This is to prevent the following: - * - *

    - *
  • The transport being initialized with {@link IBackupTransport#initializeDevice()} - *
  • {@link BackupManagerService#resetBackupState(File)} being called, which will: - *
      - *
    • Call {@link ProcessedPackagesJournal#reset()} - *
    • Reset current token to 0 - *
    • Delete state files - *
    • Mark data changed for every key-value participant - *
    - *
- */ - private void createPmStateFile() throws IOException { - createPmStateFile(mTransport); - } - - /** @see #createPmStateFile() */ - private void createPmStateFile(TransportData transport) throws IOException { - Files.write(getStateFile(transport, PM_PACKAGE), "pmState".getBytes()); - } - - /** - * Forces transport initialization and call to {@link - * BackupManagerService#resetBackupState(File)} - */ - private void deletePmStateFile() throws IOException { - Files.deleteIfExists(getStateFile(mTransport, PM_PACKAGE)); - } - - /** - * Implements {@code function} for {@link BackupAgent#onBackup(ParcelFileDescriptor, - * BackupDataOutput, ParcelFileDescriptor)} of {@code agentMock} and populates {@link - * AgentMock#oldState}. - */ - private static void agentOnBackupDo(AgentMock agentMock, BackupAgentOnBackup function) - throws Exception { - doAnswer( - (BackupAgentOnBackup) - (oldState, dataOutput, newState) -> { - ByteArrayOutputStream outputStream = - new ByteArrayOutputStream(); - Utils.transferStreamedData( - new FileInputStream(oldState.getFileDescriptor()), - outputStream); - agentMock.oldState = outputStream.toByteArray(); - agentMock.oldStateHistory.add(agentMock.oldState); - function.onBackup(oldState, dataOutput, newState); - }) - .when(agentMock.agent) - .onBackup(any(), any(), any()); - } - - /** - * Returns an {@link Answer} that can be used for mocking {@link - * IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)} that copies the - * backup data received to {@code backupDataPath} and returns {@code result}. - */ - private static Answer copyBackupDataAndReturn(Path backupDataPath, int result) { - return invocation -> { - ParcelFileDescriptor backupDataParcelFd = invocation.getArgument(1); - FileDescriptor backupDataFd = backupDataParcelFd.getFileDescriptor(); - Files.copy(new FileInputStream(backupDataFd), backupDataPath, REPLACE_EXISTING); - backupDataParcelFd.close(); - return result; - }; - } - - /** - * Same as {@link #copyBackupDataAndReturn(Path, int)}} with {@code result = - * BackupTransport.TRANSPORT_OK}. - */ - private static Answer copyBackupDataTo(Path backupDataPath) { - return copyBackupDataAndReturn(backupDataPath, BackupTransport.TRANSPORT_OK); - } - - private Path createTemporaryFile() throws IOException { - return Files.createTempFile(mContext.getCacheDir().toPath(), "backup", ".tmp"); - } - - private static IterableSubject< - ? extends IterableSubject>, Path, Iterable> - assertDirectory(Path directory) throws IOException { - return assertThat(oneTimeIterable(Files.newDirectoryStream(directory).iterator())) - .named("directory " + directory); - } - - private static void assertJournalDoesNotContain( - @Nullable DataChangedJournal journal, String packageName) throws IOException { - List packages = (journal == null) ? emptyList() : journal.getPackages(); - assertThat(packages).doesNotContain(packageName); - } - - private void assertBackupPendingFor(PackageData packageData) throws IOException { - String packageName = packageData.packageName; - // We verify the current journal, NOT the old one passed to KeyValueBackupTask constructor - assertThat(mBackupManagerService.getJournal().getPackages()).contains(packageName); - assertThat(mBackupManagerService.getPendingBackups()).containsKey(packageName); - } - - private void assertBackupNotPendingFor(PackageData packageData) throws IOException { - String packageName = packageData.packageName; - // We verify the current journal, NOT the old one passed to KeyValueBackupTask constructor - assertJournalDoesNotContain(mBackupManagerService.getJournal(), packageName); - assertThat(mBackupManagerService.getPendingBackups()).doesNotContainKey(packageName); - } - - private void assertDataHasKeyValue(BackupDataInput backupData, String key, byte[] value) - throws IOException { - assertThat(backupData.readNextHeader()).isTrue(); - assertThat(backupData.getKey()).isEqualTo(key); - int size = backupData.getDataSize(); - byte[] data1 = new byte[size]; - backupData.readEntityData(data1, 0, size); - assertThat(data1).isEqualTo(value); - } - - /** - * Put conditions that should *always* be true after task execution. - * - *

Note: We should generally NOT do this. For every different set of pre-conditions that - * result in different code-paths being executed there should be one test method verifying these - * post-conditions. Since there were a couple of methods here already and these post-conditions - * are pretty serious to be neglected it was decided to over-verify in this case. - */ - private void assertTaskPostConditions() { - assertThat(mWakeLock.isHeld()).isFalse(); - } - - @FunctionalInterface - private interface BackupAgentOnBackup extends Answer { - void onBackup( - ParcelFileDescriptor oldState, - BackupDataOutput dataOutput, - ParcelFileDescriptor newState) - throws IOException; - - @Override - default Void answer(InvocationOnMock invocation) throws Throwable { - onBackup( - invocation.getArgument(0), - invocation.getArgument(1), - invocation.getArgument(2)); - return null; - } - } - - private static class AgentMock { - private final IBackupAgent agentBinder; - private final BackupAgent agent; - private final List oldStateHistory = new ArrayList<>(); - private byte[] oldState; - - private AgentMock(IBackupAgent agentBinder, BackupAgent agent) { - this.agentBinder = agentBinder; - this.agent = agent; - } - } - - private abstract static class FakeIBackupManager extends IBackupManager.Stub { - private Handler mBackupHandler; - private BackupRestoreTask mTask; - - public FakeIBackupManager() {} - - private void setUp(Handler backupHandler, BackupRestoreTask task) { - mBackupHandler = backupHandler; - mTask = task; - } - - @Override - public void opComplete(int token, long result) throws RemoteException { - assertThat(mTask).isNotNull(); - Message message = - mBackupHandler.obtainMessage( - BackupHandler.MSG_OP_COMPLETE, Pair.create(mTask, result)); - mBackupHandler.sendMessage(message); - } - } - - private static class ThrowingPackageManagerBackupAgent extends PackageManagerBackupAgent { - private final RuntimeException mException; - - ThrowingPackageManagerBackupAgent( - PackageManager packageManager, RuntimeException exception) { - super(packageManager); - mException = exception; - } - - @Override - public void onBackup( - ParcelFileDescriptor oldState, - BackupDataOutput data, - ParcelFileDescriptor newState) { - throw mException; - } - } -} diff --git a/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java new file mode 100644 index 000000000000..fde9bc898979 --- /dev/null +++ b/services/robotests/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -0,0 +1,1996 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.backup.keyvalue; + +import static android.app.backup.BackupManager.ERROR_AGENT_FAILURE; +import static android.app.backup.BackupManager.ERROR_BACKUP_NOT_ALLOWED; +import static android.app.backup.BackupManager.ERROR_PACKAGE_NOT_FOUND; +import static android.app.backup.BackupManager.ERROR_TRANSPORT_ABORTED; +import static android.app.backup.BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED; +import static android.app.backup.BackupManager.SUCCESS; +import static android.app.backup.ForwardingBackupAgent.forward; + +import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createBackupWakeLock; +import static com.android.server.backup.testing.BackupManagerServiceTestUtils + .createInitializedBackupManagerService; +import static com.android.server.backup.testing.BackupManagerServiceTestUtils + .setUpBackupManagerServiceBasics; +import static com.android.server.backup.testing.BackupManagerServiceTestUtils + .setUpBinderCallerAndApplicationAsSystem; +import static com.android.server.backup.testing.PackageData.PM_PACKAGE; +import static com.android.server.backup.testing.PackageData.fullBackupPackage; +import static com.android.server.backup.testing.PackageData.keyValuePackage; +import static com.android.server.backup.testing.TestUtils.assertEventLogged; +import static com.android.server.backup.testing.TestUtils.uncheck; +import static com.android.server.backup.testing.TransportData.backupTransport; +import static com.android.server.backup.testing.Utils.oneTimeIterable; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.shadow.api.Shadow.extract; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toList; + +import android.annotation.Nullable; +import android.app.Application; +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.IBackupManager; +import android.app.backup.IBackupManagerMonitor; +import android.app.backup.IBackupObserver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.DeadObjectException; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.util.Pair; + +import com.android.internal.backup.IBackupTransport; +import com.android.server.EventLogTags; +import com.android.server.backup.BackupManagerService; +import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.DataChangedJournal; +import com.android.server.backup.KeyValueBackupJob; +import com.android.server.backup.PackageManagerBackupAgent; +import com.android.server.backup.TransportManager; +import com.android.server.backup.internal.BackupHandler; +import com.android.server.backup.internal.OnTaskFinishedListener; +import com.android.server.backup.testing.PackageData; +import com.android.server.backup.testing.TransportData; +import com.android.server.backup.testing.TransportTestUtils; +import com.android.server.backup.testing.TransportTestUtils.TransportMock; +import com.android.server.backup.testing.Utils; +import com.android.server.backup.transport.TransportClient; +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderClasses; +import com.android.server.testing.SystemLoaderPackages; +import com.android.server.testing.shadows.FrameworkShadowLooper; +import com.android.server.testing.shadows.ShadowBackupDataInput; +import com.android.server.testing.shadows.ShadowBackupDataOutput; +import com.android.server.testing.shadows.ShadowEventLog; + +import com.google.common.truth.IterableSubject; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowPackageManager; +import org.robolectric.shadows.ShadowQueuedWork; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +// TODO: When returning to RUNNING_QUEUE vs FINAL, RUNNING_QUEUE sets status = OK. Why? Verify? +// TODO: Check queue in general, behavior w/ multiple packages +// TODO: Test PM invocation +@RunWith(FrameworkRobolectricTestRunner.class) +@Config( + manifest = Config.NONE, + sdk = 26, + shadows = { + FrameworkShadowLooper.class, + ShadowBackupDataInput.class, + ShadowBackupDataOutput.class, + ShadowEventLog.class, + ShadowQueuedWork.class, + }) +@SystemLoaderPackages({"com.android.server.backup", "android.app.backup"}) +@SystemLoaderClasses({IBackupTransport.class, IBackupAgent.class, PackageInfo.class}) +@Presubmit +public class KeyValueBackupTaskTest { + private static final PackageData PACKAGE_1 = keyValuePackage(1); + private static final PackageData PACKAGE_2 = keyValuePackage(2); + + @Mock private TransportManager mTransportManager; + @Mock private DataChangedJournal mOldJournal; + @Mock private IBackupObserver mObserver; + @Mock private IBackupManagerMonitor mMonitor; + @Mock private OnTaskFinishedListener mListener; + private BackupManagerService mBackupManagerService; + private TransportData mTransport; + private ShadowLooper mShadowBackupLooper; + private Handler mBackupHandler; + private PowerManager.WakeLock mWakeLock; + private ShadowPackageManager mShadowPackageManager; + private FakeIBackupManager mBackupManager; + private File mBaseStateDir; + private File mDataDir; + private Application mApplication; + private ShadowApplication mShadowApplication; + private FrameworkShadowLooper mShadowMainLooper; + private Context mContext; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mTransport = backupTransport(); + + mApplication = RuntimeEnvironment.application; + mShadowApplication = shadowOf(mApplication); + mContext = mApplication; + + mShadowMainLooper = extract(Looper.getMainLooper()); + + File cacheDir = mApplication.getCacheDir(); + // Corresponds to /data/backup + mBaseStateDir = new File(cacheDir, "base_state"); + // Corresponds to /cache/backup_stage + mDataDir = new File(cacheDir, "data"); + // We create here simulating init.rc + mDataDir.mkdirs(); + assertThat(mDataDir.isDirectory()).isTrue(); + + PackageManager packageManager = mApplication.getPackageManager(); + mShadowPackageManager = shadowOf(packageManager); + + mWakeLock = createBackupWakeLock(mApplication); + + mBackupManager = spy(FakeIBackupManager.class); + + // Needed to be able to use a real BMS instead of a mock + setUpBinderCallerAndApplicationAsSystem(mApplication); + mBackupManagerService = + spy( + createInitializedBackupManagerService( + mContext, mBaseStateDir, mDataDir, mTransportManager)); + setUpBackupManagerServiceBasics( + mBackupManagerService, + mApplication, + mTransportManager, + packageManager, + mBackupManagerService.getBackupHandler(), + mWakeLock, + mBackupManagerService.getAgentTimeoutParameters()); + when(mBackupManagerService.getBaseStateDir()).thenReturn(mBaseStateDir); + when(mBackupManagerService.getDataDir()).thenReturn(mDataDir); + when(mBackupManagerService.getBackupManagerBinder()).thenReturn(mBackupManager); + + mBackupHandler = mBackupManagerService.getBackupHandler(); + mShadowBackupLooper = shadowOf(mBackupHandler.getLooper()); + ShadowEventLog.setUp(); + } + + @Test + public void testRunTask_whenQueueEmpty_updatesBookkeeping() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(mBackupManagerService.getCurrentToken()).thenReturn(0L); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + + runTask(task); + + assertThat(mBackupManagerService.getPendingInits()).isEmpty(); + assertThat(mBackupManagerService.isBackupRunning()).isFalse(); + assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0); + verify(mOldJournal).delete(); + } + + @Test + public void testRunTask_whenQueueEmpty_releasesWakeLock() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(mBackupManagerService.getCurrentToken()).thenReturn(0L); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + + runTask(task); + + assertThat(mWakeLock.isHeld()).isFalse(); + } + + @Test + public void testRunTask_whenQueueEmpty_doesNotProduceData() throws Exception { + TransportMock transportMock = setUpTransport(mTransport); + when(mBackupManagerService.getCurrentToken()).thenReturn(0L); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + + runTask(task); + + assertDirectory(getStateDirectory(mTransport)).isEmpty(); + assertDirectory(mDataDir.toPath()).isEmpty(); + } + + @Test + public void testRunTask_whenQueueEmpty_doesNotCallTransport() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(mBackupManagerService.getCurrentToken()).thenReturn(0L); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + + runTask(task); + + verify(transportMock.transport, never()).initializeDevice(); + verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); + verify(transportMock.transport, never()).finishBackup(); + } + + @Test + public void testRunTask_whenQueueEmpty_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(mBackupManagerService.getCurrentToken()).thenReturn(0L); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver, never()).onResult(any(), anyInt()); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenQueueEmpty_doesNotChangeStateFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, true); + Files.write(getStateFile(mTransport, PM_PACKAGE), "pmState".getBytes()); + Files.write(getStateFile(mTransport, PACKAGE_1), "packageState".getBytes()); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PM_PACKAGE))) + .isEqualTo("pmState".getBytes()); + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("packageState".getBytes()); + } + + @Test + public void testRunTask_whenOnePackageAndTransportUnavailable() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport.unavailable()); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); + assertBackupPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenOnePackage_logsBackupStartEvent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertEventLogged(EventLogTags.BACKUP_START, mTransport.transportName); + } + + @Test + public void testRunTask_whenOnePackage_releasesWakeLock() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertThat(mWakeLock.isHeld()).isFalse(); + } + + @Test + public void testRunTask_whenOnePackage_updatesBookkeeping() throws Exception { + // Transport has to be initialized to not reset current token + TransportMock transportMock = setUpInitializedTransport(mTransport); + mBackupManagerService.setCurrentToken(0L); + when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertThat(mBackupManagerService.getPendingInits()).isEmpty(); + assertThat(mBackupManagerService.isBackupRunning()).isFalse(); + assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0); + assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(1234L); + verify(mBackupManagerService).writeRestoreTokens(); + verify(mOldJournal).delete(); + } + + @Test + public void testRunTask_whenPackageWithOldStateAndIncremental_passesOldStateToAgent() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + false, + PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertThat(agentMock.oldState).isEqualTo("oldState".getBytes()); + } + + @Test + public void testRunTask_whenPackageWithOldStateAndNonIncremental_passesEmptyOldStateToAgent() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + true, + PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertThat(agentMock.oldState).isEqualTo(new byte[0]); + } + + @Test + public void testRunTask_whenNonPmPackageAndNonIncremental_doesNotBackUpPm() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + true, + PACKAGE_1); + + runTask(task); + + verify(pmAgent, never()).onBackup(any(), any(), any()); + } + + @Test + public void testRunTask_whenNonPmPackageAndPmAndNonIncremental_backsUpPm() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + true, + PACKAGE_1, + PM_PACKAGE); + + runTask(task); + + verify(pmAgent).onBackup(any(), any(), any()); + } + + @Test + public void testRunTask_whenNonPmPackageAndIncremental_backsUpPm() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + PackageManagerBackupAgent pmAgent = spy(createPmAgent()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(forward(pmAgent)); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + false, + PACKAGE_1); + + runTask(task); + + verify(pmAgent).onBackup(any(), any(), any()); + } + + @Test + public void testRunTask_whenOnePackageAndNoPmState_initializesTransportAndResetsState() + throws Exception { + TransportMock transportMock = setUpTransport(mTransport); + // Need 2 packages to be able to verify state of package not involved in the task + setUpAgentsWithData(PACKAGE_1, PACKAGE_2); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + deletePmStateFile(); + Files.write(getStateFile(mTransport, PACKAGE_2), "package2State".getBytes()); + + runTask(task); + + verify(transportMock.transport).initializeDevice(); + verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile()); + // Verifying that it deleted all the states (can't verify package 1 because it generated a + // new state in this task execution) + assertThat(Files.exists(getStateFile(mTransport, PACKAGE_2))).isFalse(); + assertEventLogged(EventLogTags.BACKUP_INITIALIZE); + } + + @Test + public void testRunTask_whenOnePackageAndWithPmState_doesNotInitializeTransportOrResetState() + throws Exception { + TransportMock transportMock = setUpTransport(mTransport); + setUpAgentsWithData(PACKAGE_1, PACKAGE_2); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + createPmStateFile(); + Files.write(getStateFile(mTransport, PACKAGE_2), "package2State".getBytes()); + + runTask(task); + + verify(transportMock.transport, never()).initializeDevice(); + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_2))) + .isEqualTo("package2State".getBytes()); + } + + @Test + public void testRunTask_whenTransportReturnsErrorForInitialization() throws Exception { + TransportMock transportMock = setUpTransport(mTransport); + when(transportMock.transport.initializeDevice()) + .thenReturn(BackupTransport.TRANSPORT_ERROR); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + deletePmStateFile(); + + runTask(task); + + // First for initialization and second because of the transport failure + verify(mBackupManagerService, times(2)) + .resetBackupState(getStateDirectory(mTransport).toFile()); + verify(agentMock.agent, never()).onBackup(any(), any(), any()); + verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); + assertBackupPendingFor(PACKAGE_1); + assertEventLogged(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + } + + @Test + public void testRunTask_whenTransportThrowsDuringInitialization() throws Exception { + TransportMock transportMock = setUpTransport(mTransport); + when(transportMock.transport.initializeDevice()).thenThrow(RemoteException.class); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + deletePmStateFile(); + + runTask(task); + + // First for initialization and second because of the transport failure + verify(mBackupManagerService, times(2)) + .resetBackupState(getStateDirectory(mTransport).toFile()); + verify(agentMock.agent, never()).onBackup(any(), any(), any()); + verify(transportMock.transport, never()).performBackup(any(), any(), anyInt()); + assertBackupPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenPackageNotEligibleForBackup() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1.backupNotAllowed()); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(agentMock.agent, never()).onBackup(any(), any(), any()); + verify(transportMock.transport, never()) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_BACKUP_NOT_ALLOWED); + verify(mObserver).backupFinished(SUCCESS); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenPackageDoesFullBackup() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + PackageData packageData = fullBackupPackage(1); + AgentMock agentMock = setUpAgentWithData(packageData); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, packageData); + + runTask(task); + + verify(agentMock.agent, never()).onBackup(any(), any(), any()); + verify(agentMock.agent, never()).onFullBackup(any()); + verify(mObserver).onResult(packageData.packageName, ERROR_BACKUP_NOT_ALLOWED); + verify(mObserver).backupFinished(SUCCESS); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenPackageIsStopped() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1.stopped()); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(agentMock.agent, never()).onBackup(any(), any(), any()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_BACKUP_NOT_ALLOWED); + verify(mObserver).backupFinished(SUCCESS); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenPackageUnknown() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + // Not calling setUpAgent() + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport, never()) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_PACKAGE_NOT_FOUND); + verify(mObserver).backupFinished(SUCCESS); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenCallingAgent_setsWakeLockWorkSource() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // In production (for non-system agents) the call is asynchronous, but here is + // synchronous, so it's fine to verify here. + // Verify has set work source and hasn't unset yet. + verify(mBackupManagerService) + .setWorkSource( + argThat(workSource -> workSource.get(0) == PACKAGE_1.uid)); + verify(mBackupManagerService, never()).setWorkSource(null); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + // More verifications inside agent call above + verify(mBackupManagerService).setWorkSource(null); + } + + /** + * Agent unavailable means {@link BackupManagerService#bindToAgentSynchronous(ApplicationInfo, + * int)} returns {@code null}. + * + * @see #setUpAgent(PackageData) + */ + @Test + public void testRunTask_whenAgentUnavailable() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1.unavailable()); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).setWorkSource(null); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); + verify(mObserver).backupFinished(BackupManager.SUCCESS); + } + + @Test + public void testRunTask_whenBindToAgentThrowsSecurityException() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgent(PACKAGE_1); + doThrow(SecurityException.class) + .when(mBackupManagerService) + .bindToAgentSynchronous(argThat(applicationInfo(PACKAGE_1)), anyInt()); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).setWorkSource(null); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); + verify(mObserver).backupFinished(BackupManager.SUCCESS); + } + + @Test + public void testRunTask_whenTransportGetBackupQuotaThrows_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) + .thenThrow(DeadObjectException.class); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mObserver, never()).onResult(eq(PACKAGE_1.packageName), anyInt()); + verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); + verify(mListener).onFinished(any()); + } + + @Test + public void testRunTask_whenTransportGetBackupQuotaThrows_cleansUp() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) + .thenThrow(DeadObjectException.class); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).setWorkSource(null); + verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); + } + + @Test + public void testRunTask_whenTransportGetBackupQuotaThrows_doesNotTouchFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) + .thenThrow(DeadObjectException.class); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "packageState".getBytes()); + + runTask(task); + + assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("packageState".getBytes()); + } + + @Test + public void testRunTask_whenTransportGetBackupQuotaThrows_revertsOperation() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) + .thenThrow(DeadObjectException.class); + setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport).requestBackupTime(); + assertBackupPendingFor(PACKAGE_1); + assertThat(KeyValueBackupJob.isScheduled()).isTrue(); + } + + /** + * For local agents the exception is thrown in our stack, so it hits the catch clause around + * invocation earlier than the {@link KeyValueBackupTask#operationComplete(long)} code-path, + * invalidating the latter. Note that this happens because {@link + * BackupManagerService#opComplete(int, long)} schedules the actual execution to the backup + * handler. + */ + @Test + public void testRunTask_whenLocalAgentOnBackupThrows() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + throw new RuntimeException(); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).setWorkSource(null); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); + verify(mObserver).backupFinished(SUCCESS); + assertEventLogged( + EventLogTags.BACKUP_AGENT_FAILURE, + PACKAGE_1.packageName, + new RuntimeException().toString()); + assertBackupPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenTransportProvidesFlags_passesThemToTheAgent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; + when(transportMock.transport.getTransportFlags()).thenReturn(flags); + AgentMock agentMock = setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(agentMock.agent) + .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + } + + @Test + public void testRunTask_whenTransportDoesNotProvidesFlags() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(agentMock.agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any()); + } + + @Test + public void testRunTask_whenTransportProvidesFlagsAndMultipleAgents_passesToAll() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; + when(transportMock.transport.getTransportFlags()).thenReturn(flags); + List agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2); + BackupAgent agent1 = agentMocks.get(0).agent; + BackupAgent agent2 = agentMocks.get(1).agent; + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + PACKAGE_1, + PACKAGE_2); + + runTask(task); + + verify(agent1).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + verify(agent2).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + } + + @Test + public void testRunTask_whenTransportChangeFlagsAfterTaskCreation() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; + when(transportMock.transport.getTransportFlags()).thenReturn(flags); + + runTask(task); + + verify(agentMock.agent) + .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + } + + @Test + public void testRunTask_whenAgentUsesProhibitedKey_failsAgent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(agentMock.agentBinder).fail(any()); + verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); + } + + @Test + public void testRunTask_whenAgentUsesProhibitedKey_updatesAndCleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("oldState".getBytes()); + assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + } + + @Test + public void testRunTask_whenAgentUsesProhibitedKey_doesNotCallTransport() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + verify(transportMock.transport, never()) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + } + + @Test + public void testRunTask_whenAgentUsesProhibitedKey_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenAgentUsesProhibitedKey_logsAgentFailureEvent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertEventLogged(EventLogTags.BACKUP_AGENT_FAILURE, PACKAGE_1.packageName, "bad key"); + } + + @Test + public void testRunTask_whenFirstAgentUsesProhibitedKeyButLastAgentUsesPermittedKey() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + List agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2); + AgentMock agentMock1 = agentMocks.get(0); + AgentMock agentMock2 = agentMocks.get(1); + agentOnBackupDo( + agentMock1, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + agentOnBackupDo( + agentMock2, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + PACKAGE_1, + PACKAGE_2); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); + verify(agentMock1.agentBinder).fail(any()); + verify(mObserver).onResult(PACKAGE_2.packageName, SUCCESS); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenAgentDoesNotWriteData_doesNotCallTransport() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // No-op + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport, never()) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + } + + @Test + public void testRunTask_whenAgentDoesNotWriteData_logsEvents() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // No-op + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertEventLogged(EventLogTags.BACKUP_PACKAGE, PACKAGE_1.packageName, 0L); + verify(mBackupManagerService).logBackupComplete(PACKAGE_1.packageName); + } + + @Test + public void testRunTask_whenAgentDoesNotWriteData_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // No-op + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenAgentDoesNotWriteData_updatesBookkeeping() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // No-op + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenAgentDoesNotWriteData_updatesAndCleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // No-op + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))).isEqualTo(new byte[0]); + assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + } + + @Test + public void testRunTask_whenAgentWritesData_callsTransportPerformBackupWithAgentData() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + Path backupDataPath = createTemporaryFile(); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .then(copyBackupDataTo(backupDataPath)); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key1", "data1".getBytes()); + writeData(dataOutput, "key2", "data2".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport) + .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + // Now verify data sent + try (FileInputStream inputStream = new FileInputStream(backupDataPath.toFile())) { + BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); + assertDataHasKeyValue(backupData, "key1", "data1".getBytes()); + assertDataHasKeyValue(backupData, "key2", "data2".getBytes()); + assertThat(backupData.readNextHeader()).isFalse(); + } + } + + @Test + public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_OK); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + // First for PM, then for the package + verify(transportMock.transport, times(2)).finishBackup(); + } + + @Test + public void testRunTask_whenFinishBackupSucceeds_updatesAndCleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", "data".getBytes()); + writeState(newState, "newState".getBytes()); + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("newState".getBytes()); + assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + } + + @Test + public void testRunTask_whenFinishBackupSucceeds_logsBackupPackageEvent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + Path backupData = createTemporaryFile(); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .then(copyBackupDataTo(backupData)); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertEventLogged( + EventLogTags.BACKUP_PACKAGE, PACKAGE_1.packageName, Files.size(backupData)); + } + + @Test + public void testRunTask_whenFinishBackupSucceeds_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mBackupManagerService).logBackupComplete(PACKAGE_1.packageName); + verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); + verify(mObserver).backupFinished(SUCCESS); + verify(mListener).onFinished(any()); + } + + @Test + public void testRunTask_whenFinishBackupSucceeds_updatesBookkeeping() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenTransportRejectsPackage_doesNotCallFinishBackup() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + // Called only for PM + verify(transportMock.transport, times(1)).finishBackup(); + } + + @Test + public void testRunTask_whenTransportRejectsPackage_updatesAndCleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("oldState".getBytes()); + assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + } + + @Test + public void testRunTask_whenTransportRejectsPackage_logsAgentFailureEvent() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertEventLogged( + EventLogTags.BACKUP_AGENT_FAILURE, PACKAGE_1.packageName, "Transport rejected"); + } + + @Test + public void testRunTask_whenTransportRejectsPackage_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); + verify(mObserver).backupFinished(SUCCESS); + verify(mListener).onFinished(any()); + } + + @Test + public void testRunTask_whenTransportRejectsPackage_updatesBookkeeping() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenTransportRejectsFirstPackageButLastSucceeds() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_2)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_OK); + setUpAgentsWithData(PACKAGE_1, PACKAGE_2); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + PACKAGE_1, + PACKAGE_2); + + runTask(task); + + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); + verify(mObserver).onResult(PACKAGE_2.packageName, SUCCESS); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenTransportRejectsLastPackageButFirstSucceeds() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_OK); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_2)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED); + setUpAgentsWithData(PACKAGE_1, PACKAGE_2); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + PACKAGE_1, + PACKAGE_2); + + runTask(task); + + verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); + verify(mObserver).onResult(PACKAGE_2.packageName, ERROR_TRANSPORT_PACKAGE_REJECTED); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenTransportReturnsQuotaExceeded() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false)) + .thenReturn(1234L); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_QUOTA_EXCEEDED); + AgentMock agentMock = setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mObserver) + .onResult(PACKAGE_1.packageName, BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED); + verify(mObserver).backupFinished(SUCCESS); + verify(agentMock.agent).onQuotaExceeded(anyLong(), eq(1234L)); + assertEventLogged(EventLogTags.BACKUP_QUOTA_EXCEEDED, PACKAGE_1.packageName); + assertBackupNotPendingFor(PACKAGE_1); + } + + @Test + public void testRunTask_whenNonIncrementalAndTransportRequestsNonIncremental() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + true, + PACKAGE_1); + // Delete to be non-incremental + Files.deleteIfExists(getStateFile(mTransport, PACKAGE_1)); + + runTask(task); + + // Error because it was non-incremental already, so transport can't request it + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_ABORTED); + verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); + } + + @Test + public void testRunTask_whenIncrementalAndTransportRequestsNonIncremental() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + Path incrementalData = createTemporaryFile(); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), + any(), + intThat(flags -> (flags & BackupTransport.FLAG_INCREMENTAL) != 0))) + .thenAnswer( + copyBackupDataAndReturn( + incrementalData, + BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)); + Path nonIncrementalData = createTemporaryFile(); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), + any(), + intThat(flags -> (flags & BackupTransport.FLAG_NON_INCREMENTAL) != 0))) + .thenAnswer( + copyBackupDataAndReturn(nonIncrementalData, BackupTransport.TRANSPORT_OK)); + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + // agentMock.oldState has already been updated by now. + if (agentMock.oldState.length > 0) { + writeData(dataOutput, "key", "dataForIncremental".getBytes()); + writeState(newState, "stateForIncremental".getBytes()); + } else { + writeData(dataOutput, "key", "dataForNonIncremental".getBytes()); + writeState(newState, "stateForNonIncremental".getBytes()); + } + }); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, + mTransport.transportDirName, + false, + PACKAGE_1); + // Write state to be incremental + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + verify(agentMock.agent, times(2)).onBackup(any(), any(), any()); + byte[] oldStateDuringIncremental = agentMock.oldStateHistory.get(0); + byte[] oldStateDuringNonIncremental = agentMock.oldStateHistory.get(1); + assertThat(oldStateDuringIncremental).isEqualTo("oldState".getBytes()); + assertThat(oldStateDuringNonIncremental).isEqualTo(new byte[0]); + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("stateForNonIncremental".getBytes()); + try (FileInputStream inputStream = new FileInputStream(incrementalData.toFile())) { + BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); + assertDataHasKeyValue(backupData, "key", "dataForIncremental".getBytes()); + assertThat(backupData.readNextHeader()).isFalse(); + } + try (FileInputStream inputStream = new FileInputStream(nonIncrementalData.toFile())) { + BackupDataInput backupData = new BackupDataInput(inputStream.getFD()); + assertDataHasKeyValue(backupData, "key", "dataForNonIncremental".getBytes()); + assertThat(backupData.readNextHeader()).isFalse(); + } + verify(mObserver).onResult(PACKAGE_1.packageName, SUCCESS); + verify(mObserver).backupFinished(SUCCESS); + } + + @Test + public void testRunTask_whenTransportReturnsError_notifiesCorrectly() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_ERROR); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_TRANSPORT_ABORTED); + verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); + } + + @Test + public void testRunTask_whenTransportReturnsError_logsBackupTransportFailureEvent() + throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_ERROR); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + assertEventLogged(EventLogTags.BACKUP_TRANSPORT_FAILURE, PACKAGE_1.packageName); + } + + @Test + public void testRunTask_whenTransportReturnsError_revertsOperation() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_ERROR); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(transportMock.transport).requestBackupTime(); + assertBackupPendingFor(PACKAGE_1); + assertThat(KeyValueBackupJob.isScheduled()).isTrue(); + } + + @Test + public void testRunTask_whenTransportReturnsError_updatesAndCleansUpFiles() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.performBackup( + argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_ERROR); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes()); + + runTask(task); + + assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))) + .isEqualTo("oldState".getBytes()); + // TODO: These should be true (Bug) + // assertThat(Files.exists(getTemporaryStateFile(mTransport, PACKAGE_1))).isFalse(); + // assertThat(Files.exists(getStagingFile(PACKAGE_1))).isFalse(); + } + + @Test + public void testRunTask_whenTransportGetBackupQuotaThrowsForPm() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + when(transportMock.transport.getBackupQuota(PM_PACKAGE.packageName, false)) + .thenThrow(DeadObjectException.class); + setUpAgentWithData(PACKAGE_1); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver).backupFinished(ERROR_TRANSPORT_ABORTED); + assertEventLogged( + EventLogTags.BACKUP_AGENT_FAILURE, + PM_PACKAGE.packageName, + new DeadObjectException().toString()); + } + + @Test + public void testRunTask_whenPmAgentFails() throws Exception { + TransportMock transportMock = setUpInitializedTransport(mTransport); + PackageManagerBackupAgent pmAgent = createThrowingPmAgent(new RuntimeException()); + when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent); + KeyValueBackupTask task = + createKeyValueBackupTask( + transportMock.transportClient, mTransport.transportDirName, PACKAGE_1); + + runTask(task); + + verify(mListener).onFinished(any()); + verify(mObserver).backupFinished(eq(ERROR_TRANSPORT_ABORTED)); + assertEventLogged( + EventLogTags.BACKUP_AGENT_FAILURE, + PM_PACKAGE.packageName, + new RuntimeException().toString()); + } + + private void runTask(KeyValueBackupTask task) { + // Pretend we are not on the main-thread to prevent RemoteCall from complaining + mShadowMainLooper.setCurrentThread(false); + task.run(); + mShadowMainLooper.reset(); + assertTaskPostConditions(); + } + + private TransportMock setUpTransport(TransportData transport) throws Exception { + TransportMock transportMock = + TransportTestUtils.setUpTransport(mTransportManager, transport); + Files.createDirectories(getStateDirectory(transport)); + return transportMock; + } + + /** Sets up the transport and writes a PM state file in the transport state directory. */ + private TransportMock setUpInitializedTransport(TransportData transport) throws Exception { + TransportMock transportMock = setUpTransport(transport); + createPmStateFile(transport); + return transportMock; + } + + private Path getStateDirectory(TransportData transport) { + return mBaseStateDir.toPath().resolve(transport.transportDirName); + } + + private Path getStateFile(TransportData transport, PackageData packageData) { + return getStateDirectory(transport).resolve(packageData.packageName); + } + + private Path getTemporaryStateFile(TransportData transport, PackageData packageData) { + return getStateDirectory(transport) + .resolve(packageData.packageName + KeyValueBackupTask.NEW_STATE_FILE_SUFFIX); + } + + private Path getStagingDirectory() { + return mDataDir.toPath(); + } + + private Path getStagingFile(PackageData packageData) { + return getStagingDirectory() + .resolve(packageData.packageName + KeyValueBackupTask.STAGING_FILE_SUFFIX); + } + + private List setUpAgents(PackageData... packageNames) { + return Stream.of(packageNames).map(this::setUpAgent).collect(toList()); + } + + private AgentMock setUpAgent(PackageData packageData) { + try { + mShadowPackageManager.setApplicationEnabledSetting( + packageData.packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); + PackageInfo packageInfo = getPackageInfo(packageData); + mShadowPackageManager.addPackage(packageInfo); + mShadowApplication.sendBroadcast(getPackageAddedIntent(packageData)); + // Run the backup looper because on the receiver we post MSG_SCHEDULE_BACKUP_PACKAGE + mShadowBackupLooper.runToEndOfTasks(); + BackupAgent backupAgent = spy(BackupAgent.class); + IBackupAgent backupAgentBinder = + spy(IBackupAgent.Stub.asInterface(backupAgent.onBind())); + // Don't crash our only process (in production code this would crash the app, not us) + doNothing().when(backupAgentBinder).fail(any()); + if (packageData.available) { + doReturn(backupAgentBinder) + .when(mBackupManagerService) + .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt()); + } else { + doReturn(null) + .when(mBackupManagerService) + .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt()); + } + return new AgentMock(backupAgentBinder, backupAgent); + } catch (RemoteException e) { + // Never happens, compiler happy + throw new AssertionError(e); + } + } + + private PackageInfo getPackageInfo(PackageData packageData) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = packageData.packageName; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.uid = packageData.uid; + packageInfo.applicationInfo.flags = packageData.flags(); + packageInfo.applicationInfo.backupAgentName = packageData.agentName; + packageInfo.applicationInfo.packageName = packageData.packageName; + return packageInfo; + } + + private Intent getPackageAddedIntent(PackageData packageData) { + Intent intent = + new Intent( + Intent.ACTION_PACKAGE_ADDED, + Uri.parse("package:" + packageData.packageName)); + intent.putExtra(Intent.EXTRA_UID, packageData.uid); + intent.putExtra(Intent.EXTRA_REPLACING, false); + intent.putExtra(Intent.EXTRA_USER_HANDLE, 0); + return intent; + } + + private List setUpAgentsWithData(PackageData... packages) { + return Stream.of(packages).map(this::setUpAgentWithData).collect(toList()); + } + + private AgentMock setUpAgentWithData(PackageData packageData) { + AgentMock agentMock = setUpAgent(packageData); + String packageName = packageData.packageName; + uncheck( + () -> + agentOnBackupDo( + agentMock, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key", ("data" + packageName).getBytes()); + writeState(newState, ("state" + packageName).getBytes()); + })); + return agentMock; + } + + private KeyValueBackupTask createKeyValueBackupTask( + TransportClient transportClient, String transportDirName, PackageData... packages) { + return createKeyValueBackupTask(transportClient, transportDirName, false, packages); + } + + private KeyValueBackupTask createKeyValueBackupTask( + TransportClient transportClient, + String transportDirName, + boolean nonIncremental, + PackageData... packages) { + ArrayList keyValueBackupRequests = + Stream.of(packages) + .map(packageData -> packageData.packageName) + .map(BackupRequest::new) + .collect(toCollection(ArrayList::new)); + mBackupManagerService.getPendingBackups().clear(); + // mOldJournal is a mock, but it would be the value returned by BMS.getJournal() now + mBackupManagerService.setJournal(null); + mWakeLock.acquire(); + KeyValueBackupTask task = + new KeyValueBackupTask( + mBackupManagerService, + transportClient, + transportDirName, + keyValueBackupRequests, + mOldJournal, + mObserver, + mMonitor, + mListener, + emptyList(), + /* userInitiated */ false, + nonIncremental); + mBackupManager.setUp(mBackupHandler, task); + return task; + } + + private PackageManagerBackupAgent createPmAgent() { + PackageManagerBackupAgent pmAgent = + new PackageManagerBackupAgent(mApplication.getPackageManager()); + pmAgent.attach(mApplication); + pmAgent.onCreate(); + return pmAgent; + } + + /** + * Returns an implementation of PackageManagerBackupAgent that throws RuntimeException in {@link + * BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)} + */ + private PackageManagerBackupAgent createThrowingPmAgent(RuntimeException exception) { + PackageManagerBackupAgent pmAgent = + new ThrowingPackageManagerBackupAgent(mApplication.getPackageManager(), exception); + pmAgent.attach(mApplication); + pmAgent.onCreate(); + return pmAgent; + } + + /** Matches {@link PackageInfo} whose package name is {@code packageData.packageName}. */ + private static ArgumentMatcher packageInfo(PackageData packageData) { + // We have to test for packageInfo nulity because of Mockito's own stubbing with argThat(). + // E.g. if you do: + // + // 1. when(object.method(argThat(str -> str.equals("foo")))).thenReturn(0) + // 2. when(object.method(argThat(str -> str.equals("bar")))).thenReturn(2) + // + // The second line will throw NPE because it will call lambda 1 with null, since argThat() + // returns null. So we guard against that by checking for null. + return packageInfo -> + packageInfo != null && packageData.packageName.equals(packageInfo.packageName); + } + + /** Matches {@link ApplicationInfo} whose package name is {@code packageData.packageName}. */ + private static ArgumentMatcher applicationInfo(PackageData packageData) { + return applicationInfo -> + applicationInfo != null + && packageData.packageName.equals(applicationInfo.packageName); + } + + private static ArgumentMatcher dataOutputWithTransportFlags(int flags) { + return dataOutput -> dataOutput.getTransportFlags() == flags; + } + + private static void writeData(BackupDataOutput dataOutput, String key, byte[] data) + throws IOException { + dataOutput.writeEntityHeader(key, data.length); + dataOutput.writeEntityData(data, data.length); + } + + private static void writeState(ParcelFileDescriptor newState, byte[] state) throws IOException { + OutputStream outputStream = new FileOutputStream(newState.getFileDescriptor()); + outputStream.write(state); + outputStream.flush(); + } + + /** + * This is to prevent the following: + * + *

    + *
  • The transport being initialized with {@link IBackupTransport#initializeDevice()} + *
  • {@link BackupManagerService#resetBackupState(File)} being called, which will: + *
      + *
    • Reset processed packages journal. + *
    • Reset current token to 0. + *
    • Delete state files. + *
    • Mark data changed for every key-value participant. + *
    + *
+ */ + private void createPmStateFile() throws IOException { + createPmStateFile(mTransport); + } + + /** @see #createPmStateFile() */ + private void createPmStateFile(TransportData transport) throws IOException { + Files.write(getStateFile(transport, PM_PACKAGE), "pmState".getBytes()); + } + + /** + * Forces transport initialization and call to {@link + * BackupManagerService#resetBackupState(File)} + */ + private void deletePmStateFile() throws IOException { + Files.deleteIfExists(getStateFile(mTransport, PM_PACKAGE)); + } + + /** + * Implements {@code function} for {@link BackupAgent#onBackup(ParcelFileDescriptor, + * BackupDataOutput, ParcelFileDescriptor)} of {@code agentMock} and populates {@link + * AgentMock#oldState}. + */ + private static void agentOnBackupDo(AgentMock agentMock, BackupAgentOnBackup function) + throws Exception { + doAnswer( + (BackupAgentOnBackup) + (oldState, dataOutput, newState) -> { + ByteArrayOutputStream outputStream = + new ByteArrayOutputStream(); + Utils.transferStreamedData( + new FileInputStream(oldState.getFileDescriptor()), + outputStream); + agentMock.oldState = outputStream.toByteArray(); + agentMock.oldStateHistory.add(agentMock.oldState); + function.onBackup(oldState, dataOutput, newState); + }) + .when(agentMock.agent) + .onBackup(any(), any(), any()); + } + + /** + * Returns an {@link Answer} that can be used for mocking {@link + * IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)} that copies the + * backup data received to {@code backupDataPath} and returns {@code result}. + */ + private static Answer copyBackupDataAndReturn(Path backupDataPath, int result) { + return invocation -> { + ParcelFileDescriptor backupDataParcelFd = invocation.getArgument(1); + FileDescriptor backupDataFd = backupDataParcelFd.getFileDescriptor(); + Files.copy(new FileInputStream(backupDataFd), backupDataPath, REPLACE_EXISTING); + backupDataParcelFd.close(); + return result; + }; + } + + /** + * Same as {@link #copyBackupDataAndReturn(Path, int)}} with {@code result = + * BackupTransport.TRANSPORT_OK}. + */ + private static Answer copyBackupDataTo(Path backupDataPath) { + return copyBackupDataAndReturn(backupDataPath, BackupTransport.TRANSPORT_OK); + } + + private Path createTemporaryFile() throws IOException { + return Files.createTempFile(mContext.getCacheDir().toPath(), "backup", ".tmp"); + } + + private static IterableSubject< + ? extends IterableSubject>, Path, Iterable> + assertDirectory(Path directory) throws IOException { + return assertThat(oneTimeIterable(Files.newDirectoryStream(directory).iterator())) + .named("directory " + directory); + } + + private static void assertJournalDoesNotContain( + @Nullable DataChangedJournal journal, String packageName) throws IOException { + List packages = (journal == null) ? emptyList() : journal.getPackages(); + assertThat(packages).doesNotContain(packageName); + } + + private void assertBackupPendingFor(PackageData packageData) throws IOException { + String packageName = packageData.packageName; + // We verify the current journal, NOT the old one passed to KeyValueBackupTask constructor + assertThat(mBackupManagerService.getJournal().getPackages()).contains(packageName); + assertThat(mBackupManagerService.getPendingBackups()).containsKey(packageName); + } + + private void assertBackupNotPendingFor(PackageData packageData) throws IOException { + String packageName = packageData.packageName; + // We verify the current journal, NOT the old one passed to KeyValueBackupTask constructor + assertJournalDoesNotContain(mBackupManagerService.getJournal(), packageName); + assertThat(mBackupManagerService.getPendingBackups()).doesNotContainKey(packageName); + } + + private void assertDataHasKeyValue(BackupDataInput backupData, String key, byte[] value) + throws IOException { + assertThat(backupData.readNextHeader()).isTrue(); + assertThat(backupData.getKey()).isEqualTo(key); + int size = backupData.getDataSize(); + byte[] data1 = new byte[size]; + backupData.readEntityData(data1, 0, size); + assertThat(data1).isEqualTo(value); + } + + /** + * Put conditions that should *always* be true after task execution. + * + *

Note: We should generally NOT do this. For every different set of pre-conditions that + * result in different code-paths being executed there should be one test method verifying these + * post-conditions. Since there were a couple of methods here already and these post-conditions + * are pretty serious to be neglected it was decided to over-verify in this case. + */ + private void assertTaskPostConditions() { + assertThat(mWakeLock.isHeld()).isFalse(); + } + + @FunctionalInterface + private interface BackupAgentOnBackup extends Answer { + void onBackup( + ParcelFileDescriptor oldState, + BackupDataOutput dataOutput, + ParcelFileDescriptor newState) + throws IOException; + + @Override + default Void answer(InvocationOnMock invocation) throws Throwable { + onBackup( + invocation.getArgument(0), + invocation.getArgument(1), + invocation.getArgument(2)); + return null; + } + } + + private static class AgentMock { + private final IBackupAgent agentBinder; + private final BackupAgent agent; + private final List oldStateHistory = new ArrayList<>(); + private byte[] oldState; + + private AgentMock(IBackupAgent agentBinder, BackupAgent agent) { + this.agentBinder = agentBinder; + this.agent = agent; + } + } + + private abstract static class FakeIBackupManager extends IBackupManager.Stub { + private Handler mBackupHandler; + private BackupRestoreTask mTask; + + public FakeIBackupManager() {} + + private void setUp(Handler backupHandler, BackupRestoreTask task) { + mBackupHandler = backupHandler; + mTask = task; + } + + @Override + public void opComplete(int token, long result) throws RemoteException { + assertThat(mTask).isNotNull(); + Message message = + mBackupHandler.obtainMessage( + BackupHandler.MSG_OP_COMPLETE, Pair.create(mTask, result)); + mBackupHandler.sendMessage(message); + } + } + + private static class ThrowingPackageManagerBackupAgent extends PackageManagerBackupAgent { + private final RuntimeException mException; + + ThrowingPackageManagerBackupAgent( + PackageManager packageManager, RuntimeException exception) { + super(packageManager); + mException = exception; + } + + @Override + public void onBackup( + ParcelFileDescriptor oldState, + BackupDataOutput data, + ParcelFileDescriptor newState) { + throw mException; + } + } +} diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java index f22cdb8f5860..838902d43031 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java @@ -22,15 +22,14 @@ import android.app.backup.IBackupObserver; import com.android.server.backup.BackupManagerService; import com.android.server.backup.DataChangedJournal; -import com.android.server.backup.internal.BackupRequest; import com.android.server.backup.internal.OnTaskFinishedListener; -import com.android.server.backup.internal.KeyValueBackupTask; +import com.android.server.backup.keyvalue.BackupRequest; +import com.android.server.backup.keyvalue.KeyValueBackupTask; import com.android.server.backup.transport.TransportClient; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import java.util.ArrayList; import java.util.List; @Implements(KeyValueBackupTask.class) -- cgit v1.2.3-59-g8ed1b