Automatic sources dropoff on 2020-06-10 18:32:38.095721

The change is generated with prebuilt drop tool.

Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/com/android/server/backup/AccountManagerBackupHelper.java b/com/android/server/backup/AccountManagerBackupHelper.java
new file mode 100644
index 0000000..39b18c0
--- /dev/null
+++ b/com/android/server/backup/AccountManagerBackupHelper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 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 android.accounts.AccountManagerInternal;
+import android.app.backup.BlobBackupHelper;
+import android.os.UserHandle;
+import android.util.Slog;
+import com.android.server.LocalServices;
+
+/**
+ * Helper for handling backup of account manager specific state.
+ */
+public class AccountManagerBackupHelper extends BlobBackupHelper {
+    private static final String TAG = "AccountsBackup";
+    private static final boolean DEBUG = false;
+
+    // current schema of the backup state blob
+    private static final int STATE_VERSION = 1;
+
+    // key under which the account access grant state blob is committed to backup
+    private static final String KEY_ACCOUNT_ACCESS_GRANTS = "account_access_grants";
+
+    public AccountManagerBackupHelper() {
+        super(STATE_VERSION, KEY_ACCOUNT_ACCESS_GRANTS);
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        AccountManagerInternal am = LocalServices.getService(AccountManagerInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling backup of " + key);
+        }
+        try {
+            switch (key) {
+                case KEY_ACCOUNT_ACCESS_GRANTS: {
+                    return am.backupAccountAccessPermissions(UserHandle.USER_SYSTEM);
+                }
+
+                default: {
+                    Slog.w(TAG, "Unexpected backup key " + key);
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Unable to store payload " + key);
+        }
+
+        return new byte[0];
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        AccountManagerInternal am = LocalServices.getService(AccountManagerInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling restore of " + key);
+        }
+        try {
+            switch (key) {
+                case KEY_ACCOUNT_ACCESS_GRANTS: {
+                    am.restoreAccountAccessPermissions(payload, UserHandle.USER_SYSTEM);
+                } break;
+
+                default: {
+                    Slog.w(TAG, "Unexpected restore key " + key);
+                }
+            }
+        } catch (Exception e) {
+            Slog.w(TAG, "Unable to restore key " + key);
+        }
+    }
+}
diff --git a/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/com/android/server/backup/AccountSyncSettingsBackupHelper.java
new file mode 100644
index 0000000..b4610bd
--- /dev/null
+++ b/com/android/server/backup/AccountSyncSettingsBackupHelper.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2014 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 android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.backup.BackupDataInputStream;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupHelper;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper for backing up account sync settings (whether or not a service should be synced). The
+ * sync settings are backed up as a JSON object containing all the necessary information for
+ * restoring the sync settings later.
+ */
+public class AccountSyncSettingsBackupHelper implements BackupHelper {
+
+    private static final String TAG = "AccountSyncSettingsBackupHelper";
+    private static final boolean DEBUG = false;
+
+    private static final int STATE_VERSION = 1;
+    private static final int MD5_BYTE_SIZE = 16;
+    private static final int SYNC_REQUEST_LATCH_TIMEOUT_SECONDS = 1;
+
+    private static final String JSON_FORMAT_HEADER_KEY = "account_data";
+    private static final String JSON_FORMAT_ENCODING = "UTF-8";
+    private static final int JSON_FORMAT_VERSION = 1;
+
+    private static final String KEY_VERSION = "version";
+    private static final String KEY_MASTER_SYNC_ENABLED = "masterSyncEnabled";
+    private static final String KEY_ACCOUNTS = "accounts";
+    private static final String KEY_ACCOUNT_NAME = "name";
+    private static final String KEY_ACCOUNT_TYPE = "type";
+    private static final String KEY_ACCOUNT_AUTHORITIES = "authorities";
+    private static final String KEY_AUTHORITY_NAME = "name";
+    private static final String KEY_AUTHORITY_SYNC_STATE = "syncState";
+    private static final String KEY_AUTHORITY_SYNC_ENABLED = "syncEnabled";
+    private static final String STASH_FILE = "/backup/unadded_account_syncsettings.json";
+
+    private Context mContext;
+    private AccountManager mAccountManager;
+    private final int mUserId;
+
+    public AccountSyncSettingsBackupHelper(Context context, int userId) {
+        mContext = context;
+        mAccountManager = AccountManager.get(mContext);
+
+        mUserId = userId;
+    }
+
+    /**
+     * Take a snapshot of the current account sync settings and write them to the given output.
+     */
+    @Override
+    public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput output,
+            ParcelFileDescriptor newState) {
+        try {
+            JSONObject dataJSON = serializeAccountSyncSettingsToJSON(mUserId);
+
+            if (DEBUG) {
+                Log.d(TAG, "Account sync settings JSON: " + dataJSON);
+            }
+
+            // Encode JSON data to bytes.
+            byte[] dataBytes = dataJSON.toString().getBytes(JSON_FORMAT_ENCODING);
+            byte[] oldMd5Checksum = readOldMd5Checksum(oldState);
+            byte[] newMd5Checksum = generateMd5Checksum(dataBytes);
+            if (!Arrays.equals(oldMd5Checksum, newMd5Checksum)) {
+                int dataSize = dataBytes.length;
+                output.writeEntityHeader(JSON_FORMAT_HEADER_KEY, dataSize);
+                output.writeEntityData(dataBytes, dataSize);
+
+                Log.i(TAG, "Backup successful.");
+            } else {
+                Log.i(TAG, "Old and new MD5 checksums match. Skipping backup.");
+            }
+
+            writeNewMd5Checksum(newState, newMd5Checksum);
+        } catch (JSONException | IOException | NoSuchAlgorithmException e) {
+            Log.e(TAG, "Couldn't backup account sync settings\n" + e);
+        }
+    }
+
+    /**
+     * Fetch and serialize Account and authority information as a JSON Array.
+     */
+    private JSONObject serializeAccountSyncSettingsToJSON(int userId) throws JSONException {
+        Account[] accounts = mAccountManager.getAccountsAsUser(userId);
+        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
+
+        // Create a map of Account types to authorities. Later this will make it easier for us to
+        // generate our JSON.
+        HashMap<String, List<String>> accountTypeToAuthorities = new HashMap<String,
+                List<String>>();
+        for (SyncAdapterType syncAdapter : syncAdapters) {
+            // Skip adapters that aren’t visible to the user.
+            if (!syncAdapter.isUserVisible()) {
+                continue;
+            }
+            if (!accountTypeToAuthorities.containsKey(syncAdapter.accountType)) {
+                accountTypeToAuthorities.put(syncAdapter.accountType, new ArrayList<String>());
+            }
+            accountTypeToAuthorities.get(syncAdapter.accountType).add(syncAdapter.authority);
+        }
+
+        // Generate JSON.
+        JSONObject backupJSON = new JSONObject();
+        backupJSON.put(KEY_VERSION, JSON_FORMAT_VERSION);
+        backupJSON.put(KEY_MASTER_SYNC_ENABLED, ContentResolver.getMasterSyncAutomaticallyAsUser(
+                userId));
+
+        JSONArray accountJSONArray = new JSONArray();
+        for (Account account : accounts) {
+            List<String> authorities = accountTypeToAuthorities.get(account.type);
+
+            // We ignore Accounts that don't have any authorities because there would be no sync
+            // settings for us to restore.
+            if (authorities == null || authorities.isEmpty()) {
+                continue;
+            }
+
+            JSONObject accountJSON = new JSONObject();
+            accountJSON.put(KEY_ACCOUNT_NAME, account.name);
+            accountJSON.put(KEY_ACCOUNT_TYPE, account.type);
+
+            // Add authorities for this Account type and check whether or not sync is enabled.
+            JSONArray authoritiesJSONArray = new JSONArray();
+            for (String authority : authorities) {
+                int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId);
+                boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
+                        userId);
+
+                JSONObject authorityJSON = new JSONObject();
+                authorityJSON.put(KEY_AUTHORITY_NAME, authority);
+                authorityJSON.put(KEY_AUTHORITY_SYNC_STATE, syncState);
+                authorityJSON.put(KEY_AUTHORITY_SYNC_ENABLED, syncEnabled);
+                authoritiesJSONArray.put(authorityJSON);
+            }
+            accountJSON.put(KEY_ACCOUNT_AUTHORITIES, authoritiesJSONArray);
+
+            accountJSONArray.put(accountJSON);
+        }
+        backupJSON.put(KEY_ACCOUNTS, accountJSONArray);
+
+        return backupJSON;
+    }
+
+    /**
+     * Read the MD5 checksum from the old state.
+     *
+     * @return the old MD5 checksum
+     */
+    private byte[] readOldMd5Checksum(ParcelFileDescriptor oldState) throws IOException {
+        DataInputStream dataInput = new DataInputStream(
+                new FileInputStream(oldState.getFileDescriptor()));
+
+        byte[] oldMd5Checksum = new byte[MD5_BYTE_SIZE];
+        try {
+            int stateVersion = dataInput.readInt();
+            if (stateVersion <= STATE_VERSION) {
+                // If the state version is a version we can understand then read the MD5 sum,
+                // otherwise we return an empty byte array for the MD5 sum which will force a
+                // backup.
+                for (int i = 0; i < MD5_BYTE_SIZE; i++) {
+                    oldMd5Checksum[i] = dataInput.readByte();
+                }
+            } else {
+                Log.i(TAG, "Backup state version is: " + stateVersion
+                        + " (support only up to version " + STATE_VERSION + ")");
+            }
+        } catch (EOFException eof) {
+            // Initial state may be empty.
+        }
+        // We explicitly don't close 'dataInput' because we must not close the backing fd.
+        return oldMd5Checksum;
+    }
+
+    /**
+     * Write the given checksum to the file descriptor.
+     */
+    private void writeNewMd5Checksum(ParcelFileDescriptor newState, byte[] md5Checksum)
+            throws IOException {
+        DataOutputStream dataOutput = new DataOutputStream(
+                new BufferedOutputStream(new FileOutputStream(newState.getFileDescriptor())));
+
+        dataOutput.writeInt(STATE_VERSION);
+        dataOutput.write(md5Checksum);
+
+        // We explicitly don't close 'dataOutput' because we must not close the backing fd.
+        // The FileOutputStream will not close it implicitly.
+
+    }
+
+    private byte[] generateMd5Checksum(byte[] data) throws NoSuchAlgorithmException {
+        if (data == null) {
+            return null;
+        }
+
+        MessageDigest md5 = MessageDigest.getInstance("MD5");
+        return md5.digest(data);
+    }
+
+    /**
+     * Restore account sync settings from the given data input stream.
+     */
+    @Override
+    public void restoreEntity(BackupDataInputStream data) {
+        byte[] dataBytes = new byte[data.size()];
+        try {
+            // Read the data and convert it to a String.
+            data.read(dataBytes);
+            String dataString = new String(dataBytes, JSON_FORMAT_ENCODING);
+
+            // Convert data to a JSON object.
+            JSONObject dataJSON = new JSONObject(dataString);
+            boolean masterSyncEnabled = dataJSON.getBoolean(KEY_MASTER_SYNC_ENABLED);
+            JSONArray accountJSONArray = dataJSON.getJSONArray(KEY_ACCOUNTS);
+
+            boolean currentMasterSyncEnabled = ContentResolver.getMasterSyncAutomaticallyAsUser(
+                    mUserId);
+            if (currentMasterSyncEnabled) {
+                // Disable master sync to prevent any syncs from running.
+                ContentResolver.setMasterSyncAutomaticallyAsUser(false, mUserId);
+            }
+
+            try {
+                restoreFromJsonArray(accountJSONArray, mUserId);
+            } finally {
+                // Set the master sync preference to the value from the backup set.
+                ContentResolver.setMasterSyncAutomaticallyAsUser(masterSyncEnabled, mUserId);
+            }
+            Log.i(TAG, "Restore successful.");
+        } catch (IOException | JSONException e) {
+            Log.e(TAG, "Couldn't restore account sync settings\n" + e);
+        }
+    }
+
+    private void restoreFromJsonArray(JSONArray accountJSONArray, int userId)
+            throws JSONException {
+        Set<Account> currentAccounts = getAccounts(userId);
+        JSONArray unaddedAccountsJSONArray = new JSONArray();
+        for (int i = 0; i < accountJSONArray.length(); i++) {
+            JSONObject accountJSON = (JSONObject) accountJSONArray.get(i);
+            String accountName = accountJSON.getString(KEY_ACCOUNT_NAME);
+            String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE);
+
+            Account account = null;
+            try {
+                account = new Account(accountName, accountType);
+            } catch (IllegalArgumentException iae) {
+                continue;
+            }
+
+            // Check if the account already exists. Accounts that don't exist on the device
+            // yet won't be restored.
+            if (currentAccounts.contains(account)) {
+                if (DEBUG) Log.i(TAG, "Restoring Sync Settings for" + accountName);
+                restoreExistingAccountSyncSettingsFromJSON(accountJSON, userId);
+            } else {
+                unaddedAccountsJSONArray.put(accountJSON);
+            }
+        }
+
+        if (unaddedAccountsJSONArray.length() > 0) {
+            try (FileOutputStream fOutput = new FileOutputStream(getStashFile(userId))) {
+                String jsonString = unaddedAccountsJSONArray.toString();
+                DataOutputStream out = new DataOutputStream(fOutput);
+                out.writeUTF(jsonString);
+            } catch (IOException ioe) {
+                // Error in writing to stash file
+                Log.e(TAG, "unable to write the sync settings to the stash file", ioe);
+            }
+        } else {
+            File stashFile = getStashFile(userId);
+            if (stashFile.exists()) {
+                stashFile.delete();
+            }
+        }
+    }
+
+    /**
+     * Restore SyncSettings for all existing accounts from a stashed backup-set
+     */
+    private void accountAddedInternal(int userId) {
+        String jsonString;
+
+        try (FileInputStream fIn = new FileInputStream(getStashFile(userId))) {
+            DataInputStream in = new DataInputStream(fIn);
+            jsonString = in.readUTF();
+        } catch (FileNotFoundException fnfe) {
+            // This is expected to happen when there is no accounts info stashed
+            if (DEBUG) Log.d(TAG, "unable to find the stash file", fnfe);
+            return;
+        } catch (IOException ioe) {
+            if (DEBUG) Log.d(TAG, "could not read sync settings from stash file", ioe);
+            return;
+        }
+
+        try {
+            JSONArray unaddedAccountsJSONArray = new JSONArray(jsonString);
+            restoreFromJsonArray(unaddedAccountsJSONArray, userId);
+        } catch (JSONException jse) {
+            // Malformed jsonString
+            Log.e(TAG, "there was an error with the stashed sync settings", jse);
+        }
+    }
+
+    /**
+     * Restore SyncSettings for all existing accounts from a stashed backup-set
+     */
+    public static void accountAdded(Context context, int userId) {
+        AccountSyncSettingsBackupHelper helper = new AccountSyncSettingsBackupHelper(context,
+                userId);
+        helper.accountAddedInternal(userId);
+    }
+
+    /**
+     * Helper method - fetch accounts and return them as a HashSet.
+     *
+     * @return Accounts in a HashSet.
+     */
+    private Set<Account> getAccounts(int userId) {
+        Account[] accounts = mAccountManager.getAccountsAsUser(userId);
+        Set<Account> accountHashSet = new HashSet<Account>();
+        for (Account account : accounts) {
+            accountHashSet.add(account);
+        }
+        return accountHashSet;
+    }
+
+    /**
+     * Restore account sync settings using the given JSON. This function won't work if the account
+     * doesn't exist yet.
+     * This function will only be called during Setup Wizard, where we are guaranteed that there
+     * are no active syncs.
+     * There are 2 pieces of data to restore -
+     *      isSyncable (corresponds to {@link ContentResolver#getIsSyncable(Account, String)}
+     *      syncEnabled (corresponds to {@link ContentResolver#getSyncAutomatically(Account, String)}
+     * <strong>The restore favours adapters that were enabled on the old device, and doesn't care
+     * about adapters that were disabled.</strong>
+     *
+     * syncEnabled=true in restore data.
+     * syncEnabled will be true on this device. isSyncable will be left as the default in order to
+     * give the enabled adapter the chance to run an initialization sync.
+     *
+     * syncEnabled=false in restore data.
+     * syncEnabled will be false on this device. isSyncable will be set to 2, unless it was 0 on the
+     * old device in which case it will be set to 0 on this device. This is because isSyncable=0 is
+     * a rare state and was probably set to 0 for good reason (historically isSyncable is a way by
+     * which adapters control their own sync state independently of sync settings which is
+     * toggleable by the user).
+     * isSyncable=2 is a new isSyncable state we introduced specifically to allow adapters that are
+     * disabled after a restore to run initialization logic when the adapter is later enabled.
+     * See com.android.server.content.SyncStorageEngine#setSyncAutomatically
+     *
+     * The end result is that an adapter that the user had on will be turned on and get an
+     * initialization sync, while an adapter that the user had off will be off until the user
+     * enables it on this device at which point it will get an initialization sync.
+     */
+    private void restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON, int userId)
+            throws JSONException {
+        // Restore authorities.
+        JSONArray authorities = accountJSON.getJSONArray(KEY_ACCOUNT_AUTHORITIES);
+        String accountName = accountJSON.getString(KEY_ACCOUNT_NAME);
+        String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE);
+
+        final Account account = new Account(accountName, accountType);
+        for (int i = 0; i < authorities.length(); i++) {
+            JSONObject authority = (JSONObject) authorities.get(i);
+            final String authorityName = authority.getString(KEY_AUTHORITY_NAME);
+            boolean wasSyncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED);
+            int wasSyncable = authority.getInt(KEY_AUTHORITY_SYNC_STATE);
+
+            ContentResolver.setSyncAutomaticallyAsUser(
+                    account, authorityName, wasSyncEnabled, userId);
+
+            if (!wasSyncEnabled) {
+                ContentResolver.setIsSyncableAsUser(
+                        account,
+                        authorityName,
+                        wasSyncable == 0 ?
+                                0 /* not syncable */ : 2 /* syncable but needs initialization */,
+                        userId);
+            }
+        }
+    }
+
+    @Override
+    public void writeNewStateDescription(ParcelFileDescriptor newState) {
+
+    }
+
+    private static File getStashFile(int userId) {
+        File baseDir = userId == UserHandle.USER_SYSTEM ? Environment.getDataDirectory()
+                : Environment.getDataSystemCeDirectory(userId);
+        return new File(baseDir, STASH_FILE);
+    }
+}
diff --git a/com/android/server/backup/BackupAgentTimeoutParameters.java b/com/android/server/backup/BackupAgentTimeoutParameters.java
new file mode 100644
index 0000000..0e99b34
--- /dev/null
+++ b/com/android/server/backup/BackupAgentTimeoutParameters.java
@@ -0,0 +1,172 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+import android.util.KeyValueSettingObserver;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Configure backup and restore agent timeouts.
+ *
+ * <p>These timeout parameters are stored in Settings.Global to be configurable flags with P/H. They
+ * are represented as a comma-delimited key value list.
+ */
+public class BackupAgentTimeoutParameters extends KeyValueSettingObserver {
+    @VisibleForTesting
+    public static final String SETTING = Settings.Global.BACKUP_AGENT_TIMEOUT_PARAMETERS;
+
+    @VisibleForTesting
+    public static final String SETTING_KV_BACKUP_AGENT_TIMEOUT_MILLIS =
+            "kv_backup_agent_timeout_millis";
+
+    @VisibleForTesting
+    public static final String SETTING_FULL_BACKUP_AGENT_TIMEOUT_MILLIS =
+            "full_backup_agent_timeout_millis";
+
+    @VisibleForTesting
+    public static final String SETTING_SHARED_BACKUP_AGENT_TIMEOUT_MILLIS =
+            "shared_backup_agent_timeout_millis";
+
+    @VisibleForTesting
+    public static final String SETTING_RESTORE_AGENT_TIMEOUT_MILLIS =
+            "restore_agent_timeout_millis";
+
+    @VisibleForTesting
+    public static final String SETTING_RESTORE_AGENT_FINISHED_TIMEOUT_MILLIS =
+            "restore_agent_finished_timeout_millis";
+
+    @VisibleForTesting
+    public static final String SETTING_QUOTA_EXCEEDED_TIMEOUT_MILLIS =
+            "quota_exceeded_timeout_millis";
+
+    // Default values
+    @VisibleForTesting public static final long DEFAULT_KV_BACKUP_AGENT_TIMEOUT_MILLIS = 30 * 1000;
+
+    @VisibleForTesting
+    public static final long DEFAULT_FULL_BACKUP_AGENT_TIMEOUT_MILLIS = 5 * 60 * 1000;
+
+    @VisibleForTesting
+    public static final long DEFAULT_SHARED_BACKUP_AGENT_TIMEOUT_MILLIS = 30 * 60 * 1000;
+
+    @VisibleForTesting public static final long DEFAULT_RESTORE_AGENT_TIMEOUT_MILLIS = 60 * 1000;
+
+    @VisibleForTesting
+    public static final long DEFAULT_RESTORE_AGENT_FINISHED_TIMEOUT_MILLIS = 30 * 1000;
+
+    @VisibleForTesting
+    public static final long DEFAULT_QUOTA_EXCEEDED_TIMEOUT_MILLIS = 3 * 1000;
+
+    @GuardedBy("mLock")
+    private long mKvBackupAgentTimeoutMillis;
+
+    @GuardedBy("mLock")
+    private long mFullBackupAgentTimeoutMillis;
+
+    @GuardedBy("mLock")
+    private long mSharedBackupAgentTimeoutMillis;
+
+    @GuardedBy("mLock")
+    private long mRestoreAgentTimeoutMillis;
+
+    @GuardedBy("mLock")
+    private long mRestoreAgentFinishedTimeoutMillis;
+
+    @GuardedBy("mLock")
+    private long mQuotaExceededTimeoutMillis;
+
+    private final Object mLock = new Object();
+
+    public BackupAgentTimeoutParameters(Handler handler, ContentResolver resolver) {
+        super(handler, resolver, Settings.Global.getUriFor(SETTING));
+    }
+
+    public String getSettingValue(ContentResolver resolver) {
+        return Settings.Global.getString(resolver, SETTING);
+    }
+
+    public void update(KeyValueListParser parser) {
+        synchronized (mLock) {
+            mKvBackupAgentTimeoutMillis =
+                    parser.getLong(
+                            SETTING_KV_BACKUP_AGENT_TIMEOUT_MILLIS,
+                            DEFAULT_KV_BACKUP_AGENT_TIMEOUT_MILLIS);
+            mFullBackupAgentTimeoutMillis =
+                    parser.getLong(
+                            SETTING_FULL_BACKUP_AGENT_TIMEOUT_MILLIS,
+                            DEFAULT_FULL_BACKUP_AGENT_TIMEOUT_MILLIS);
+            mSharedBackupAgentTimeoutMillis =
+                    parser.getLong(
+                            SETTING_SHARED_BACKUP_AGENT_TIMEOUT_MILLIS,
+                            DEFAULT_SHARED_BACKUP_AGENT_TIMEOUT_MILLIS);
+            mRestoreAgentTimeoutMillis =
+                    parser.getLong(
+                            SETTING_RESTORE_AGENT_TIMEOUT_MILLIS,
+                            DEFAULT_RESTORE_AGENT_TIMEOUT_MILLIS);
+            mRestoreAgentFinishedTimeoutMillis =
+                    parser.getLong(
+                            SETTING_RESTORE_AGENT_FINISHED_TIMEOUT_MILLIS,
+                            DEFAULT_RESTORE_AGENT_FINISHED_TIMEOUT_MILLIS);
+            mQuotaExceededTimeoutMillis =
+                    parser.getLong(
+                            SETTING_QUOTA_EXCEEDED_TIMEOUT_MILLIS,
+                            DEFAULT_QUOTA_EXCEEDED_TIMEOUT_MILLIS);
+        }
+    }
+
+    public long getKvBackupAgentTimeoutMillis() {
+        synchronized (mLock) {
+            return mKvBackupAgentTimeoutMillis;
+        }
+    }
+
+    public long getFullBackupAgentTimeoutMillis() {
+        synchronized (mLock) {
+            return mFullBackupAgentTimeoutMillis;
+        }
+    }
+
+    public long getSharedBackupAgentTimeoutMillis() {
+        synchronized (mLock) {
+            return mSharedBackupAgentTimeoutMillis;
+        }
+    }
+
+    public long getRestoreAgentTimeoutMillis() {
+        synchronized (mLock) {
+            return mRestoreAgentTimeoutMillis;
+        }
+    }
+
+    public long getRestoreAgentFinishedTimeoutMillis() {
+        synchronized (mLock) {
+            return mRestoreAgentFinishedTimeoutMillis;
+        }
+    }
+
+    public long getQuotaExceededTimeoutMillis() {
+        synchronized (mLock) {
+            return mQuotaExceededTimeoutMillis;
+        }
+    }
+}
diff --git a/com/android/server/backup/BackupAgentTimeoutParametersTest.java b/com/android/server/backup/BackupAgentTimeoutParametersTest.java
new file mode 100644
index 0000000..5b226f3
--- /dev/null
+++ b/com/android/server/backup/BackupAgentTimeoutParametersTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Tests for {@link BackupAgentTimeoutParameters}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupAgentTimeoutParametersTest {
+    private ContentResolver mContentResolver;
+    private BackupAgentTimeoutParameters mParameters;
+
+    /** Initialize timeout parameters and start observing changes. */
+    @Before
+    public void setUp() {
+        Context context = RuntimeEnvironment.application.getApplicationContext();
+
+        mContentResolver = context.getContentResolver();
+        mParameters = new BackupAgentTimeoutParameters(new Handler(), mContentResolver);
+    }
+
+    /** Stop observing changes to the setting. */
+    @After
+    public void tearDown() {
+        mParameters.stop();
+    }
+
+    /** Tests that timeout parameters are initialized with default values on creation. */
+    // TODO: Break down tests
+    @Test
+    public void testGetParameters_afterConstructorWithStart_returnsDefaultValues() {
+        mParameters.start();
+
+        long kvBackupAgentTimeoutMillis = mParameters.getKvBackupAgentTimeoutMillis();
+        long fullBackupAgentTimeoutMillis = mParameters.getFullBackupAgentTimeoutMillis();
+        long sharedBackupAgentTimeoutMillis = mParameters.getSharedBackupAgentTimeoutMillis();
+        long restoreAgentTimeoutMillis = mParameters.getRestoreAgentTimeoutMillis();
+        long restoreAgentFinishedTimeoutMillis = mParameters.getRestoreAgentFinishedTimeoutMillis();
+
+        assertEquals(
+                BackupAgentTimeoutParameters.DEFAULT_KV_BACKUP_AGENT_TIMEOUT_MILLIS,
+                kvBackupAgentTimeoutMillis);
+        assertEquals(
+                BackupAgentTimeoutParameters.DEFAULT_FULL_BACKUP_AGENT_TIMEOUT_MILLIS,
+                fullBackupAgentTimeoutMillis);
+        assertEquals(
+                BackupAgentTimeoutParameters.DEFAULT_SHARED_BACKUP_AGENT_TIMEOUT_MILLIS,
+                sharedBackupAgentTimeoutMillis);
+        assertEquals(
+                BackupAgentTimeoutParameters.DEFAULT_RESTORE_AGENT_TIMEOUT_MILLIS,
+                restoreAgentTimeoutMillis);
+        assertEquals(
+                BackupAgentTimeoutParameters.DEFAULT_RESTORE_AGENT_FINISHED_TIMEOUT_MILLIS,
+                restoreAgentFinishedTimeoutMillis);
+    }
+
+    @Test
+    public void testGetQuotaExceededTimeoutMillis_returnsDefaultValue() {
+        mParameters.start();
+
+        long timeout = mParameters.getQuotaExceededTimeoutMillis();
+
+        assertThat(timeout)
+                .isEqualTo(BackupAgentTimeoutParameters.DEFAULT_QUOTA_EXCEEDED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void testGetQuotaExceededTimeoutMillis_whenSettingSet_returnsSetValue() {
+        putStringAndNotify(
+                BackupAgentTimeoutParameters.SETTING_QUOTA_EXCEEDED_TIMEOUT_MILLIS + "=" + 1279);
+        mParameters.start();
+
+        long timeout = mParameters.getQuotaExceededTimeoutMillis();
+
+        assertThat(timeout).isEqualTo(1279);
+    }
+
+    /**
+     * Tests that timeout parameters are updated when we call start, even when a setting change
+     * occurs while we are not observing.
+     */
+    @Test
+    public void testGetParameters_withSettingChangeBeforeStart_updatesValues() {
+        long testTimeout = BackupAgentTimeoutParameters.DEFAULT_KV_BACKUP_AGENT_TIMEOUT_MILLIS * 2;
+        final String setting =
+                BackupAgentTimeoutParameters.SETTING_KV_BACKUP_AGENT_TIMEOUT_MILLIS
+                        + "="
+                        + testTimeout;
+        putStringAndNotify(setting);
+        mParameters.start();
+
+        long kvBackupAgentTimeoutMillis = mParameters.getKvBackupAgentTimeoutMillis();
+
+        assertEquals(testTimeout, kvBackupAgentTimeoutMillis);
+    }
+
+    /**
+     * Tests that timeout parameters are updated when a setting change occurs while we are observing
+     * changes.
+     */
+    @Test
+    public void testGetParameters_withSettingChangeAfterStart_updatesValues() {
+        mParameters.start();
+        long testTimeout = BackupAgentTimeoutParameters.DEFAULT_KV_BACKUP_AGENT_TIMEOUT_MILLIS * 2;
+        final String setting =
+                BackupAgentTimeoutParameters.SETTING_KV_BACKUP_AGENT_TIMEOUT_MILLIS
+                        + "="
+                        + testTimeout;
+        putStringAndNotify(setting);
+
+        long kvBackupAgentTimeoutMillis = mParameters.getKvBackupAgentTimeoutMillis();
+
+        assertEquals(testTimeout, kvBackupAgentTimeoutMillis);
+    }
+
+    /**
+     * Robolectric does not notify observers of changes to settings so we have to trigger it here.
+     * Currently, the mock of {@link Settings.Secure#putString(ContentResolver, String, String)}
+     * only stores the value. TODO: Implement properly in ShadowSettings.
+     */
+    private void putStringAndNotify(String value) {
+        Settings.Global.putString(mContentResolver, BackupAgentTimeoutParameters.SETTING, value);
+
+        // We pass null as the observer since notifyChange iterates over all available observers and
+        // we don't have access to the local observer.
+        mContentResolver.notifyChange(
+                Settings.Global.getUriFor(BackupAgentTimeoutParameters.SETTING), /*observer*/ null);
+    }
+}
diff --git a/com/android/server/backup/BackupManagerConstants.java b/com/android/server/backup/BackupManagerConstants.java
new file mode 100644
index 0000000..d8c5f6f
--- /dev/null
+++ b/com/android/server/backup/BackupManagerConstants.java
@@ -0,0 +1,238 @@
+/*
+ * 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;
+
+import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
+
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.KeyValueListParser;
+import android.util.KeyValueSettingObserver;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Class to access backup manager constants.
+ *
+ * <p>The backup manager constants are encoded as a key value list separated by commas and stored as
+ * a Settings.Secure.
+ */
+public class BackupManagerConstants extends KeyValueSettingObserver {
+    private static final String TAG = "BackupManagerConstants";
+    private static final String SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS;
+
+    // Key names stored in the secure settings value.
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
+            "key_value_backup_interval_milliseconds";
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_FUZZ_MILLISECONDS =
+            "key_value_backup_fuzz_milliseconds";
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_REQUIRE_CHARGING =
+            "key_value_backup_require_charging";
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE =
+            "key_value_backup_required_network_type";
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_INTERVAL_MILLISECONDS =
+            "full_backup_interval_milliseconds";
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_REQUIRE_CHARGING = "full_backup_require_charging";
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_REQUIRED_NETWORK_TYPE =
+            "full_backup_required_network_type";
+
+    @VisibleForTesting
+    public static final String BACKUP_FINISHED_NOTIFICATION_RECEIVERS =
+            "backup_finished_notification_receivers";
+
+    // Hard coded default values.
+    @VisibleForTesting
+    public static final long DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
+            4 * AlarmManager.INTERVAL_HOUR;
+
+    @VisibleForTesting
+    public static final long DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS = 10 * 60 * 1000;
+
+    @VisibleForTesting public static final boolean DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING = true;
+    @VisibleForTesting
+    public static final int DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE =
+            JobInfo.NETWORK_TYPE_ANY;
+
+    @VisibleForTesting
+    public static final long DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS =
+            24 * AlarmManager.INTERVAL_HOUR;
+
+    @VisibleForTesting public static final boolean DEFAULT_FULL_BACKUP_REQUIRE_CHARGING = true;
+    @VisibleForTesting
+    public static final int DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE =
+            JobInfo.NETWORK_TYPE_UNMETERED;
+
+    @VisibleForTesting
+    public static final String DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS = "";
+
+    // Backup manager constants.
+    private long mKeyValueBackupIntervalMilliseconds;
+    private long mKeyValueBackupFuzzMilliseconds;
+    private boolean mKeyValueBackupRequireCharging;
+    private int mKeyValueBackupRequiredNetworkType;
+    private long mFullBackupIntervalMilliseconds;
+    private boolean mFullBackupRequireCharging;
+    private int mFullBackupRequiredNetworkType;
+    private String[] mBackupFinishedNotificationReceivers;
+
+    public BackupManagerConstants(Handler handler, ContentResolver resolver) {
+        super(handler, resolver, Settings.Secure.getUriFor(SETTING));
+    }
+
+    public String getSettingValue(ContentResolver resolver) {
+        return Settings.Secure.getString(resolver, SETTING);
+    }
+
+    public synchronized void update(KeyValueListParser parser) {
+        mKeyValueBackupIntervalMilliseconds =
+                parser.getLong(
+                        KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS,
+                        DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        mKeyValueBackupFuzzMilliseconds =
+                parser.getLong(
+                        KEY_VALUE_BACKUP_FUZZ_MILLISECONDS,
+                        DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        mKeyValueBackupRequireCharging =
+                parser.getBoolean(
+                        KEY_VALUE_BACKUP_REQUIRE_CHARGING,
+                        DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        mKeyValueBackupRequiredNetworkType =
+                parser.getInt(
+                        KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE,
+                        DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        mFullBackupIntervalMilliseconds =
+                parser.getLong(
+                        FULL_BACKUP_INTERVAL_MILLISECONDS,
+                        DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        mFullBackupRequireCharging =
+                parser.getBoolean(
+                        FULL_BACKUP_REQUIRE_CHARGING, DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        mFullBackupRequiredNetworkType =
+                parser.getInt(
+                        FULL_BACKUP_REQUIRED_NETWORK_TYPE,
+                        DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        String backupFinishedNotificationReceivers =
+                parser.getString(
+                        BACKUP_FINISHED_NOTIFICATION_RECEIVERS,
+                        DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS);
+        if (backupFinishedNotificationReceivers.isEmpty()) {
+            mBackupFinishedNotificationReceivers = new String[] {};
+        } else {
+            mBackupFinishedNotificationReceivers = backupFinishedNotificationReceivers.split(":");
+        }
+    }
+
+    // The following are access methods for the individual parameters.
+    // To be sure to retrieve values from the same set of settings,
+    // group the calls of these methods in a block syncrhonized on
+    // a reference of this object.
+    public synchronized long getKeyValueBackupIntervalMilliseconds() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupIntervalMilliseconds(...) returns "
+                            + mKeyValueBackupIntervalMilliseconds);
+        }
+        return mKeyValueBackupIntervalMilliseconds;
+    }
+
+    public synchronized long getKeyValueBackupFuzzMilliseconds() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupFuzzMilliseconds(...) returns "
+                            + mKeyValueBackupFuzzMilliseconds);
+        }
+        return mKeyValueBackupFuzzMilliseconds;
+    }
+
+    public synchronized boolean getKeyValueBackupRequireCharging() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupRequireCharging(...) returns "
+                            + mKeyValueBackupRequireCharging);
+        }
+        return mKeyValueBackupRequireCharging;
+    }
+
+    public synchronized int getKeyValueBackupRequiredNetworkType() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupRequiredNetworkType(...) returns "
+                            + mKeyValueBackupRequiredNetworkType);
+        }
+        return mKeyValueBackupRequiredNetworkType;
+    }
+
+    public synchronized long getFullBackupIntervalMilliseconds() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getFullBackupIntervalMilliseconds(...) returns "
+                            + mFullBackupIntervalMilliseconds);
+        }
+        return mFullBackupIntervalMilliseconds;
+    }
+
+    public synchronized boolean getFullBackupRequireCharging() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging);
+        }
+        return mFullBackupRequireCharging;
+    }
+
+    public synchronized int getFullBackupRequiredNetworkType() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getFullBackupRequiredNetworkType(...) returns "
+                            + mFullBackupRequiredNetworkType);
+        }
+        return mFullBackupRequiredNetworkType;
+    }
+
+    /** Returns an array of package names that should be notified whenever a backup finishes. */
+    public synchronized String[] getBackupFinishedNotificationReceivers() {
+        if (DEBUG_SCHEDULING) {
+            Slog.v(
+                    TAG,
+                    "getBackupFinishedNotificationReceivers(...) returns "
+                            + TextUtils.join(", ", mBackupFinishedNotificationReceivers));
+        }
+        return mBackupFinishedNotificationReceivers;
+    }
+}
diff --git a/com/android/server/backup/BackupManagerConstantsTest.java b/com/android/server/backup/BackupManagerConstantsTest.java
new file mode 100644
index 0000000..affa1f3
--- /dev/null
+++ b/com/android/server/backup/BackupManagerConstantsTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupManagerConstantsTest {
+    private static final String PACKAGE_NAME = "some.package.name";
+    private static final String ANOTHER_PACKAGE_NAME = "another.package.name";
+
+    private ContentResolver mContentResolver;
+    private BackupManagerConstants mConstants;
+
+    @Before
+    public void setUp() {
+        final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+        mContentResolver = context.getContentResolver();
+        mConstants = new BackupManagerConstants(new Handler(), mContentResolver);
+        mConstants.start();
+    }
+
+    @After
+    public void tearDown() {
+        mConstants.stop();
+    }
+
+    @Test
+    public void testGetConstants_afterConstructorWithStart_returnsDefaultValues() {
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+        boolean keyValueBackupRequireCharging = mConstants.getKeyValueBackupRequireCharging();
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+        boolean fullBackupRequireCharging = mConstants.getFullBackupRequireCharging();
+        int fullBackupRequiredNetworkType = mConstants.getFullBackupRequiredNetworkType();
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
+
+        assertThat(keyValueBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        assertThat(keyValueBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(fullBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(fullBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        assertThat(fullBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(backupFinishedNotificationReceivers).isEqualTo(new String[0]);
+    }
+
+    /**
+     * Tests that if there is a setting change when we are not currently observing the setting, that
+     * once we start observing again, we receive the most up-to-date value.
+     */
+    @Test
+    public void testGetConstant_withSettingChangeBeforeStart_updatesValues() {
+        mConstants.stop();
+        long testInterval =
+                BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS * 2;
+        final String setting =
+                BackupManagerConstants.KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS + "=" + testInterval;
+        putStringAndNotify(setting);
+
+        mConstants.start();
+
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        assertThat(keyValueBackupIntervalMilliseconds).isEqualTo(testInterval);
+    }
+
+    @Test
+    public void testGetConstants_whenSettingIsNull_returnsDefaultValues() {
+        putStringAndNotify(null);
+
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+        boolean keyValueBackupRequireCharging = mConstants.getKeyValueBackupRequireCharging();
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+        boolean fullBackupRequireCharging = mConstants.getFullBackupRequireCharging();
+        int fullBackupRequiredNetworkType = mConstants.getFullBackupRequiredNetworkType();
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
+
+        assertThat(keyValueBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        assertThat(keyValueBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(fullBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(fullBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        assertThat(fullBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(backupFinishedNotificationReceivers).isEqualTo(new String[0]);
+    }
+
+    /**
+     * Test passing in a malformed setting string. The setting expects
+     * "key1=value,key2=value,key3=value..." but we pass in "key1=,value"
+     */
+    @Test
+    public void testGetConstant_whenSettingIsMalformed_doesNotUpdateParamsOrThrow() {
+        long testFuzz = BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS * 2;
+        final String invalidSettingFormat =
+                BackupManagerConstants.KEY_VALUE_BACKUP_FUZZ_MILLISECONDS + "=," + testFuzz;
+        putStringAndNotify(invalidSettingFormat);
+
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+    }
+
+    /**
+     * Test passing in an invalid value type. {@link
+     * BackupManagerConstants#KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE} expects an integer, but we
+     * pass in a boolean.
+     */
+    @Test
+    public void testGetConstant_whenSettingHasInvalidType_doesNotUpdateParamsOrThrow() {
+        boolean testValue = true;
+        final String invalidSettingType =
+                BackupManagerConstants.KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE + "=" + testValue;
+        putStringAndNotify(invalidSettingType);
+
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+    }
+
+    @Test
+    public void testGetConstants_afterSettingChange_updatesValues() {
+        long testKVInterval =
+                BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS * 2;
+        long testFullInterval =
+                BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS * 2;
+        final String intervalSetting =
+                BackupManagerConstants.KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS
+                        + "="
+                        + testKVInterval
+                        + ","
+                        + BackupManagerConstants.FULL_BACKUP_INTERVAL_MILLISECONDS
+                        + "="
+                        + testFullInterval;
+        putStringAndNotify(intervalSetting);
+
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+
+        assertThat(keyValueBackupIntervalMilliseconds).isEqualTo(testKVInterval);
+        assertThat(fullBackupIntervalMilliseconds).isEqualTo(testFullInterval);
+    }
+
+    @Test
+    public void testBackupNotificationReceivers_afterSetting_updatesAndParsesCorrectly() {
+        final String receiversSetting =
+                BackupManagerConstants.BACKUP_FINISHED_NOTIFICATION_RECEIVERS
+                        + "="
+                        + PACKAGE_NAME
+                        + ':'
+                        + ANOTHER_PACKAGE_NAME;
+        putStringAndNotify(receiversSetting);
+
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
+        assertThat(backupFinishedNotificationReceivers)
+                .isEqualTo(new String[] {PACKAGE_NAME, ANOTHER_PACKAGE_NAME});
+    }
+
+    /**
+     * Robolectric does not notify observers of changes to settings so we have to trigger it here.
+     * Currently, the mock of {@link Settings.Secure#putString(ContentResolver, String, String)}
+     * only stores the value. TODO: Implement properly in ShadowSettings.
+     */
+    private void putStringAndNotify(String value) {
+        Settings.Secure.putString(
+                mContentResolver, Settings.Secure.BACKUP_MANAGER_CONSTANTS, value);
+
+        // We pass null as the observer since notifyChange iterates over all available observers and
+        // we don't have access to the local observer.
+        mContentResolver.notifyChange(
+                Settings.Secure.getUriFor(Settings.Secure.BACKUP_MANAGER_CONSTANTS),
+                /*observer*/ null);
+    }
+}
diff --git a/com/android/server/backup/BackupManagerService.java b/com/android/server/backup/BackupManagerService.java
new file mode 100644
index 0000000..b13bef2
--- /dev/null
+++ b/com/android/server/backup/BackupManagerService.java
@@ -0,0 +1,1618 @@
+/*
+ * Copyright (C) 2014 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 java.util.Collections.emptySet;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.admin.DevicePolicyManager;
+import android.app.backup.BackupManager;
+import android.app.backup.IBackupManager;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.app.backup.IFullBackupRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.server.SystemConfig;
+import com.android.server.SystemService;
+import com.android.server.backup.utils.RandomAccessFileUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Definition of the system service that performs backup/restore operations.
+ *
+ * <p>This class is responsible for handling user-aware operations and acts as a delegator, routing
+ * incoming calls to the appropriate per-user {@link UserBackupManagerService} to handle the
+ * corresponding backup/restore operation.
+ *
+ * <p>It also determines whether the backup service is available. It can be disabled in the
+ * following two ways:
+ *
+ * <ul>
+ *  <li>Temporary - call {@link #setBackupServiceActive(int, boolean)}, or
+ *  <li>Permanent - set the system property {@link #BACKUP_DISABLE_PROPERTY} to true.
+ * </ul>
+ *
+ * Temporary disabling is controlled by {@link #setBackupServiceActive(int, boolean)} through
+ * privileged callers (currently {@link DevicePolicyManager}). If called on {@link
+ * UserHandle#USER_SYSTEM}, backup is disabled for all users.
+ */
+public class BackupManagerService extends IBackupManager.Stub {
+    public static final String TAG = "BackupManagerService";
+    public static final boolean DEBUG = true;
+    public static final boolean MORE_DEBUG = false;
+    public static final boolean DEBUG_SCHEDULING = true;
+
+    @VisibleForTesting
+    static final String DUMP_RUNNING_USERS_MESSAGE = "Backup Manager is running for users:";
+
+    /**
+     * Name of file that disables the backup service. If this file exists, then backup is disabled
+     * for all users.
+     */
+    private static final String BACKUP_SUPPRESS_FILENAME = "backup-suppress";
+
+    /**
+     * Name of file for non-system users that enables the backup service for the user. Backup is
+     * disabled by default in non-system users.
+     */
+    private static final String BACKUP_ACTIVATED_FILENAME = "backup-activated";
+
+    /**
+     * Name of file for non-system users that remembers whether backup was explicitly activated or
+     * deactivated with a call to setBackupServiceActive.
+     */
+    private static final String REMEMBER_ACTIVATED_FILENAME = "backup-remember-activated";
+
+    // Product-level suppression of backup/restore.
+    private static final String BACKUP_DISABLE_PROPERTY = "ro.backup.disable";
+
+    private static final String BACKUP_THREAD = "backup";
+
+    static BackupManagerService sInstance;
+
+    static BackupManagerService getInstance() {
+        return Objects.requireNonNull(sInstance);
+    }
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+
+    private final boolean mGlobalDisable;
+    // Lock to write backup suppress files.
+    // TODD(b/121198006): remove this object and synchronized all methods on "this".
+    private final Object mStateLock = new Object();
+
+    private final Handler mHandler;
+    private final Set<ComponentName> mTransportWhitelist;
+
+    /** Keeps track of all unlocked users registered with this service. Indexed by user id. */
+    private final SparseArray<UserBackupManagerService> mUserServices;
+
+    private final BroadcastReceiver mUserRemovedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
+                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+                if (userId > 0) { // for only non system users
+                    mHandler.post(() -> onRemovedNonSystemUser(userId));
+                }
+            }
+        }
+    };
+
+    public BackupManagerService(Context context) {
+        this(context, new SparseArray<>());
+    }
+
+    @VisibleForTesting
+    BackupManagerService(Context context, SparseArray<UserBackupManagerService> userServices) {
+        mContext = context;
+        mGlobalDisable = isBackupDisabled();
+        HandlerThread handlerThread =
+                new HandlerThread(BACKUP_THREAD, Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+        mUserManager = UserManager.get(context);
+        mUserServices = userServices;
+        Set<ComponentName> transportWhitelist =
+                SystemConfig.getInstance().getBackupTransportWhitelist();
+        mTransportWhitelist = (transportWhitelist == null) ? emptySet() : transportWhitelist;
+        mContext.registerReceiver(
+                mUserRemovedReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED));
+    }
+
+    // TODO: Remove this when we implement DI by injecting in the construtor.
+    @VisibleForTesting
+    Handler getBackupHandler() {
+        return mHandler;
+    }
+
+    protected boolean isBackupDisabled() {
+        return SystemProperties.getBoolean(BACKUP_DISABLE_PROPERTY, false);
+    }
+
+    protected int binderGetCallingUserId() {
+        return Binder.getCallingUserHandle().getIdentifier();
+    }
+
+    protected int binderGetCallingUid() {
+        return Binder.getCallingUid();
+    }
+
+    /** Stored in the system user's directory. */
+    protected File getSuppressFileForSystemUser() {
+        return new File(UserBackupManagerFiles.getBaseStateDir(UserHandle.USER_SYSTEM),
+                BACKUP_SUPPRESS_FILENAME);
+    }
+
+    /** Stored in the system user's directory and the file is indexed by the user it refers to. */
+    protected File getRememberActivatedFileForNonSystemUser(int userId) {
+        return UserBackupManagerFiles.getStateFileInSystemDir(REMEMBER_ACTIVATED_FILENAME, userId);
+    }
+
+    /** Stored in the system user's directory and the file is indexed by the user it refers to. */
+    protected File getActivatedFileForNonSystemUser(int userId) {
+        return UserBackupManagerFiles.getStateFileInSystemDir(BACKUP_ACTIVATED_FILENAME, userId);
+    }
+
+    /**
+     * Remove backup state for non system {@code userId} when the user is removed from the device.
+     * For non system users, backup state is stored in both the user's own dir and the system dir.
+     * When the user is removed, the user's own dir gets removed by the OS. This method ensures that
+     * the part of the user backup state which is in the system dir also gets removed.
+     */
+    private void onRemovedNonSystemUser(int userId) {
+        Slog.i(TAG, "Removing state for non system user " + userId);
+        File dir = UserBackupManagerFiles.getStateDirInSystemDir(userId);
+        if (!FileUtils.deleteContentsAndDir(dir)) {
+            Slog.w(TAG, "Failed to delete state dir for removed user: " + userId);
+        }
+    }
+
+    // TODO (b/124359804) move to util method in FileUtils
+    private void createFile(File file) throws IOException {
+        if (file.exists()) {
+            return;
+        }
+
+        file.getParentFile().mkdirs();
+        if (!file.createNewFile()) {
+            Slog.w(TAG, "Failed to create file " + file.getPath());
+        }
+    }
+
+    // TODO (b/124359804) move to util method in FileUtils
+    private void deleteFile(File file) {
+        if (!file.exists()) {
+            return;
+        }
+
+        if (!file.delete()) {
+            Slog.w(TAG, "Failed to delete file " + file.getPath());
+        }
+    }
+
+    /**
+     * Deactivates the backup service for user {@code userId}. If this is the system user, it
+     * creates a suppress file which disables backup for all users. If this is a non-system user, it
+     * only deactivates backup for that user by deleting its activate file.
+     */
+    @GuardedBy("mStateLock")
+    private void deactivateBackupForUserLocked(int userId) throws IOException {
+        if (userId == UserHandle.USER_SYSTEM) {
+            createFile(getSuppressFileForSystemUser());
+        } else {
+            deleteFile(getActivatedFileForNonSystemUser(userId));
+        }
+    }
+
+    /**
+     * Enables the backup service for user {@code userId}. If this is the system user, it deletes
+     * the suppress file. If this is a non-system user, it creates the user's activate file. Note,
+     * deleting the suppress file does not automatically enable backup for non-system users, they
+     * need their own activate file in order to participate in the service.
+     */
+    @GuardedBy("mStateLock")
+    private void activateBackupForUserLocked(int userId) throws IOException {
+        if (userId == UserHandle.USER_SYSTEM) {
+            deleteFile(getSuppressFileForSystemUser());
+        } else {
+            createFile(getActivatedFileForNonSystemUser(userId));
+        }
+    }
+
+    /**
+     * This method should not perform any I/O (e.g. do not call isBackupActivatedForUser),
+     * it's used in multiple places where I/O waits would cause system lock-ups.
+     * @param userId User id for which this operation should be performed.
+     * @return true if the user is ready for backup and false otherwise.
+     */
+    @Override
+    public boolean isUserReadyForBackup(int userId) {
+        return mUserServices.get(UserHandle.USER_SYSTEM) != null
+                && mUserServices.get(userId) != null;
+    }
+
+    /**
+     * Backup is activated for the system user if the suppress file does not exist. Backup is
+     * activated for non-system users if the suppress file does not exist AND the user's activated
+     * file exists.
+     */
+    private boolean isBackupActivatedForUser(int userId) {
+        if (getSuppressFileForSystemUser().exists()) {
+            return false;
+        }
+
+        return userId == UserHandle.USER_SYSTEM
+                || getActivatedFileForNonSystemUser(userId).exists();
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    protected UserManager getUserManager() {
+        return mUserManager;
+    }
+
+    protected void postToHandler(Runnable runnable) {
+        mHandler.post(runnable);
+    }
+
+    /**
+     * Called from {@link BackupManagerService.Lifecycle} when a user {@code userId} is unlocked.
+     * Starts the backup service for this user if backup is active for this user. Offloads work onto
+     * the handler thread {@link #mHandlerThread} to keep unlock time low since backup is not
+     * essential for device functioning.
+     */
+    void onUnlockUser(int userId) {
+        postToHandler(() -> startServiceForUser(userId));
+    }
+
+    /**
+     * Starts the backup service for user {@code userId} by creating a new instance of {@link
+     * UserBackupManagerService} and registering it with this service.
+     */
+    @VisibleForTesting
+    void startServiceForUser(int userId) {
+        // We know that the user is unlocked here because it is called from setBackupServiceActive
+        // and unlockUser which have these guarantees. So we can check if the file exists.
+        if (mGlobalDisable) {
+            Slog.i(TAG, "Backup service not supported");
+            return;
+        }
+        if (!isBackupActivatedForUser(userId)) {
+            Slog.i(TAG, "Backup not activated for user " + userId);
+            return;
+        }
+        if (mUserServices.get(userId) != null) {
+            Slog.i(TAG, "userId " + userId + " already started, so not starting again");
+            return;
+        }
+        Slog.i(TAG, "Starting service for user: " + userId);
+        UserBackupManagerService userBackupManagerService =
+                UserBackupManagerService.createAndInitializeService(
+                        userId, mContext, this, mTransportWhitelist);
+        startServiceForUser(userId, userBackupManagerService);
+    }
+
+    /**
+     * Starts the backup service for user {@code userId} by registering its instance of {@link
+     * UserBackupManagerService} with this service and setting enabled state.
+     */
+    void startServiceForUser(int userId, UserBackupManagerService userBackupManagerService) {
+        mUserServices.put(userId, userBackupManagerService);
+
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backup enable");
+        userBackupManagerService.initializeBackupEnableState();
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+    }
+
+    /** Stops the backup service for user {@code userId} when the user is stopped. */
+    @VisibleForTesting
+    protected void stopServiceForUser(int userId) {
+        UserBackupManagerService userBackupManagerService = mUserServices.removeReturnOld(userId);
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.tearDownService();
+
+            KeyValueBackupJob.cancel(userId, mContext);
+            FullBackupJob.cancel(userId, mContext);
+        }
+    }
+
+    /**
+     *  Returns a list of users currently unlocked that have a {@link UserBackupManagerService}
+     *  registered.
+     *
+     *  Warning: Do NOT modify returned object as it's used inside.
+     *
+     *  TODO: Return a copy or only expose read-only information through other means.
+     */
+    @VisibleForTesting
+    SparseArray<UserBackupManagerService> getUserServices() {
+        return mUserServices;
+    }
+
+    /**
+     * Called from {@link BackupManagerService.Lifecycle} when a user {@code userId} is stopped.
+     * Offloads work onto the handler thread {@link #mHandlerThread} to keep stopping time low.
+     */
+    void onStopUser(int userId) {
+        postToHandler(
+                () -> {
+                    if (!mGlobalDisable) {
+                        Slog.i(TAG, "Stopping service for user: " + userId);
+                        stopServiceForUser(userId);
+                    }
+                });
+    }
+
+    /** Returns {@link UserBackupManagerService} for user {@code userId}. */
+    @Nullable
+    public UserBackupManagerService getUserService(int userId) {
+        return mUserServices.get(userId);
+    }
+
+    /**
+     * The system user and managed profiles can only be acted on by callers in the system or root
+     * processes. Other users can be acted on by callers who have both android.permission.BACKUP and
+     * android.permission.INTERACT_ACROSS_USERS_FULL permissions.
+     */
+    private void enforcePermissionsOnUser(int userId) throws SecurityException {
+        boolean isRestrictedUser =
+                userId == UserHandle.USER_SYSTEM
+                        || getUserManager().getUserInfo(userId).isManagedProfile();
+
+        if (isRestrictedUser) {
+            int caller = binderGetCallingUid();
+            if (caller != Process.SYSTEM_UID && caller != Process.ROOT_UID) {
+                throw new SecurityException("No permission to configure backup activity");
+            }
+        } else {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.BACKUP, "No permission to configure backup activity");
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                    "No permission to configure backup activity");
+        }
+    }
+
+    /**
+     * Only privileged callers should be changing the backup state. Deactivating backup in the
+     * system user also deactivates backup in all users. We are not guaranteed that {@code userId}
+     * is unlocked at this point yet, so handle both cases.
+     */
+    public void setBackupServiceActive(int userId, boolean makeActive) {
+        enforcePermissionsOnUser(userId);
+
+        // In Q, backup is OFF by default for non-system users. In the future, we will change that
+        // to ON unless backup was explicitly deactivated with a (permissioned) call to
+        // setBackupServiceActive.
+        // Therefore, remember this for use in the future. Basically the default in the future will
+        // be: rememberFile.exists() ? rememberFile.value() : ON
+        // Note that this has to be done right after the permission checks and before any other
+        // action since we need to remember that a permissioned call was made irrespective of
+        // whether the call changes the state or not.
+        if (userId != UserHandle.USER_SYSTEM) {
+            try {
+                File rememberFile = getRememberActivatedFileForNonSystemUser(userId);
+                createFile(rememberFile);
+                RandomAccessFileUtils.writeBoolean(rememberFile, makeActive);
+            } catch (IOException e) {
+                Slog.e(TAG, "Unable to persist backup service activity", e);
+            }
+        }
+
+        if (mGlobalDisable) {
+            Slog.i(TAG, "Backup service not supported");
+            return;
+        }
+
+        synchronized (mStateLock) {
+            Slog.i(TAG, "Making backup " + (makeActive ? "" : "in") + "active");
+            if (makeActive) {
+                try {
+                    activateBackupForUserLocked(userId);
+                } catch (IOException e) {
+                    Slog.e(TAG, "Unable to persist backup service activity");
+                }
+
+                // If the user is unlocked, we can start the backup service for it. Otherwise we
+                // will start the service when the user is unlocked as part of its unlock callback.
+                if (getUserManager().isUserUnlocked(userId)) {
+                    // Clear calling identity as initialization enforces the system identity but we
+                    // can be coming from shell.
+                    long oldId = Binder.clearCallingIdentity();
+                    try {
+                        startServiceForUser(userId);
+                    } finally {
+                        Binder.restoreCallingIdentity(oldId);
+                    }
+                }
+            } else {
+                try {
+                    //TODO(b/121198006): what if this throws an exception?
+                    deactivateBackupForUserLocked(userId);
+                } catch (IOException e) {
+                    Slog.e(TAG, "Unable to persist backup service inactivity");
+                }
+                //TODO(b/121198006): loop through active users that have work profile and
+                // stop them as well.
+                onStopUser(userId);
+            }
+        }
+    }
+
+    // IBackupManager binder API
+
+    /**
+     * Querying activity state of backup service.
+     *
+     * @param userId The user in which the activity state of backup service is queried.
+     * @return true if the service is active.
+     */
+    @Override
+    public boolean isBackupServiceActive(int userId) {
+        synchronized (mStateLock) {
+            return !mGlobalDisable && isBackupActivatedForUser(userId);
+        }
+    }
+
+    @Override
+    public void dataChangedForUser(int userId, String packageName) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            dataChanged(userId, packageName);
+        }
+    }
+
+    @Override
+    public void dataChanged(String packageName) throws RemoteException {
+        dataChangedForUser(binderGetCallingUserId(), packageName);
+    }
+
+    /**
+     * An app's backup agent calls this method to let the service know that there's new data to
+     * backup for their app {@code packageName}. Only used for apps participating in key-value
+     * backup.
+     */
+    public void dataChanged(@UserIdInt int userId, String packageName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "dataChanged()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.dataChanged(packageName);
+        }
+    }
+
+    // ---------------------------------------------
+    // TRANSPORT OPERATIONS
+    // ---------------------------------------------
+
+    @Override
+    public void initializeTransportsForUser(
+            int userId, String[] transportNames, IBackupObserver observer) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            initializeTransports(userId, transportNames, observer);
+        }
+    }
+
+    /** Run an initialize operation for the given transports {@code transportNames}. */
+    public void initializeTransports(
+            @UserIdInt int userId, String[] transportNames, IBackupObserver observer) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "initializeTransports()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.initializeTransports(transportNames, observer);
+        }
+    }
+
+    @Override
+    public void clearBackupDataForUser(int userId, String transportName, String packageName)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            clearBackupData(userId, transportName, packageName);
+        }
+    }
+
+    /**
+     * Clear the given package {@code packageName}'s backup data from the transport {@code
+     * transportName}.
+     */
+    public void clearBackupData(@UserIdInt int userId, String transportName, String packageName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "clearBackupData()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.clearBackupData(transportName, packageName);
+        }
+    }
+
+    @Override
+    public void clearBackupData(String transportName, String packageName)
+            throws RemoteException {
+        clearBackupDataForUser(binderGetCallingUserId(), transportName, packageName);
+    }
+
+    @Override
+    public void agentConnectedForUser(int userId, String packageName, IBinder agent)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            agentConnected(userId, packageName, agent);
+        }
+    }
+
+    @Override
+    public void agentConnected(String packageName, IBinder agent) throws RemoteException {
+        agentConnectedForUser(binderGetCallingUserId(), packageName, agent);
+    }
+
+    /**
+     * Callback: a requested backup agent has been instantiated. This should only be called from the
+     * {@link ActivityManager}.
+     */
+    public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "agentConnected()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.agentConnected(packageName, agentBinder);
+        }
+    }
+
+    @Override
+    public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            agentDisconnected(userId, packageName);
+        }
+    }
+
+    @Override
+    public void agentDisconnected(String packageName) throws RemoteException {
+        agentDisconnectedForUser(binderGetCallingUserId(), packageName);
+    }
+
+    /**
+     * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be
+     * called from the {@link ActivityManager}.
+     */
+    public void agentDisconnected(@UserIdInt int userId, String packageName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.agentDisconnected(packageName);
+        }
+    }
+
+    @Override
+    public void restoreAtInstallForUser(int userId, String packageName, int token)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            restoreAtInstall(userId, packageName, token);
+        }
+    }
+
+    @Override
+    public void restoreAtInstall(String packageName, int token) throws RemoteException {
+        restoreAtInstallForUser(binderGetCallingUserId(), packageName, token);
+    }
+
+    /**
+     * Used to run a restore pass for an application that is being installed. This should only be
+     * called from the {@link PackageManager}.
+     */
+    public void restoreAtInstall(@UserIdInt int userId, String packageName, int token) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "restoreAtInstall()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.restoreAtInstall(packageName, token);
+        }
+    }
+
+    @Override
+    public void setBackupEnabledForUser(@UserIdInt int userId, boolean isEnabled)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            setBackupEnabled(userId, isEnabled);
+        }
+    }
+
+    @Override
+    public void setBackupEnabled(boolean isEnabled) throws RemoteException {
+        setBackupEnabledForUser(binderGetCallingUserId(), isEnabled);
+    }
+
+    /** Enable/disable the backup service. This is user-configurable via backup settings. */
+    public void setBackupEnabled(@UserIdInt int userId, boolean enable) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "setBackupEnabled()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.setBackupEnabled(enable);
+        }
+    }
+
+    @Override
+    public void setAutoRestoreForUser(int userId, boolean doAutoRestore) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            setAutoRestore(userId, doAutoRestore);
+        }
+    }
+
+    @Override
+    public void setAutoRestore(boolean doAutoRestore) throws RemoteException {
+        setAutoRestoreForUser(binderGetCallingUserId(), doAutoRestore);
+    }
+
+    /** Enable/disable automatic restore of app data at install time. */
+    public void setAutoRestore(@UserIdInt int userId, boolean autoRestore) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "setAutoRestore()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.setAutoRestore(autoRestore);
+        }
+    }
+
+    @Override
+    public boolean isBackupEnabledForUser(@UserIdInt int userId) throws RemoteException {
+        return isUserReadyForBackup(userId) && isBackupEnabled(userId);
+    }
+
+    @Override
+    public boolean isBackupEnabled() throws RemoteException {
+        return isBackupEnabledForUser(binderGetCallingUserId());
+    }
+
+    /**
+     * Return {@code true} if the backup mechanism is currently enabled, else returns {@code false}.
+     */
+    public boolean isBackupEnabled(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "isBackupEnabled()");
+
+        return userBackupManagerService != null && userBackupManagerService.isBackupEnabled();
+    }
+
+    /** Sets the backup password used when running adb backup. */
+    @Override
+    public boolean setBackupPassword(String currentPassword, String newPassword) {
+        int userId = binderGetCallingUserId();
+        if (!isUserReadyForBackup(userId)) {
+            return false;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(
+                        UserHandle.USER_SYSTEM, "setBackupPassword()");
+
+        return userBackupManagerService != null
+                && userBackupManagerService.setBackupPassword(currentPassword, newPassword);
+    }
+
+    /** Returns {@code true} if adb backup was run with a password, else returns {@code false}. */
+    @Override
+    public boolean hasBackupPassword() throws RemoteException {
+        int userId = binderGetCallingUserId();
+        if (!isUserReadyForBackup(userId)) {
+            return false;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(
+                        UserHandle.USER_SYSTEM, "hasBackupPassword()");
+
+        return userBackupManagerService != null && userBackupManagerService.hasBackupPassword();
+    }
+
+    @Override
+    public void backupNowForUser(@UserIdInt int userId) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            backupNow(userId);
+        }
+    }
+
+    @Override
+    public void backupNow() throws RemoteException {
+        backupNowForUser(binderGetCallingUserId());
+    }
+
+    /**
+     * Run a backup pass immediately for any key-value backup applications that have declared that
+     * they have pending updates.
+     */
+    public void backupNow(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "backupNow()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.backupNow();
+        }
+    }
+
+    /**
+     * Used by 'adb backup' to run a backup pass for packages {@code packageNames} supplied via the
+     * command line, writing the resulting data stream to the supplied {@code fd}. This method is
+     * synchronous and does not return to the caller until the backup has been completed. It
+     * requires on-screen confirmation by the user.
+     */
+    @Override
+    public void adbBackup(
+            @UserIdInt int userId,
+            ParcelFileDescriptor fd,
+            boolean includeApks,
+            boolean includeObbs,
+            boolean includeShared,
+            boolean doWidgets,
+            boolean doAllApps,
+            boolean includeSystem,
+            boolean doCompress,
+            boolean doKeyValue,
+            String[] packageNames) {
+        if (!isUserReadyForBackup(userId)) {
+            return;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "adbBackup()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.adbBackup(
+                    fd,
+                    includeApks,
+                    includeObbs,
+                    includeShared,
+                    doWidgets,
+                    doAllApps,
+                    includeSystem,
+                    doCompress,
+                    doKeyValue,
+                    packageNames);
+        }
+    }
+
+    @Override
+    public void fullTransportBackupForUser(int userId, String[] packageNames)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            fullTransportBackup(userId, packageNames);
+        }
+    }
+
+    /**
+     * Run a full backup pass for the given packages {@code packageNames}. Used by 'adb shell bmgr'.
+     */
+    public void fullTransportBackup(@UserIdInt int userId, String[] packageNames) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "fullTransportBackup()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.fullTransportBackup(packageNames);
+        }
+    }
+
+    /**
+     * Used by 'adb restore' to run a restore pass reading from the supplied {@code fd}. This method
+     * is synchronous and does not return to the caller until the restore has been completed. It
+     * requires on-screen confirmation by the user.
+     */
+    @Override
+    public void adbRestore(@UserIdInt int userId, ParcelFileDescriptor fd) {
+        if (!isUserReadyForBackup(userId)) {
+            return;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "adbRestore()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.adbRestore(fd);
+        }
+    }
+
+    @Override
+    public void acknowledgeFullBackupOrRestoreForUser(
+            int userId,
+            int token,
+            boolean allow,
+            String curPassword,
+            String encryptionPassword,
+            IFullBackupRestoreObserver observer)
+            throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            acknowledgeAdbBackupOrRestore(userId, token, allow,
+                    curPassword, encryptionPassword, observer);
+        }
+    }
+
+    /**
+     * Confirm that the previously requested adb backup/restore operation can proceed. This is used
+     * to require a user-facing disclosure about the operation.
+     */
+    public void acknowledgeAdbBackupOrRestore(
+            @UserIdInt int userId,
+            int token,
+            boolean allow,
+            String currentPassword,
+            String encryptionPassword,
+            IFullBackupRestoreObserver observer) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "acknowledgeAdbBackupOrRestore()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.acknowledgeAdbBackupOrRestore(
+                    token, allow, currentPassword, encryptionPassword, observer);
+        }
+    }
+
+    @Override
+    public void acknowledgeFullBackupOrRestore(int token, boolean allow, String curPassword,
+            String encryptionPassword, IFullBackupRestoreObserver observer)
+            throws RemoteException {
+        acknowledgeFullBackupOrRestoreForUser(
+                binderGetCallingUserId(), token, allow, curPassword, encryptionPassword, observer);
+    }
+
+
+    @Override
+    public String getCurrentTransportForUser(int userId) throws RemoteException {
+        return (isUserReadyForBackup(userId)) ? getCurrentTransport(userId) : null;
+    }
+
+    @Override
+    public String getCurrentTransport() throws RemoteException {
+        return getCurrentTransportForUser(binderGetCallingUserId());
+    }
+
+    /** Return the name of the currently active transport. */
+    @Nullable
+    public String getCurrentTransport(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getCurrentTransport()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getCurrentTransport();
+    }
+
+    /**
+     * Returns the {@link ComponentName} of the host service of the selected transport or
+     * {@code null} if no transport selected or if the transport selected is not registered.
+     */
+    @Override
+    @Nullable
+    public ComponentName getCurrentTransportComponentForUser(int userId) {
+        return (isUserReadyForBackup(userId)) ? getCurrentTransportComponent(userId) : null;
+    }
+
+    /**
+     * Returns the {@link ComponentName} of the host service of the selected transport or {@code
+     * null} if no transport selected or if the transport selected is not registered.
+     */
+    @Nullable
+    public ComponentName getCurrentTransportComponent(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getCurrentTransportComponent()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getCurrentTransportComponent();
+    }
+
+    @Override
+    public String[] listAllTransportsForUser(int userId) throws RemoteException {
+        return (isUserReadyForBackup(userId)) ? listAllTransports(userId) : null;
+    }
+
+    /** Report all known, available backup transports by name. */
+    @Nullable
+    public String[] listAllTransports(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "listAllTransports()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.listAllTransports();
+    }
+
+    @Override
+    public String[] listAllTransports() throws RemoteException {
+        return listAllTransportsForUser(binderGetCallingUserId());
+    }
+
+    @Override
+    public ComponentName[] listAllTransportComponentsForUser(int userId) throws RemoteException {
+        return (isUserReadyForBackup(userId))
+                ? listAllTransportComponents(userId) : null;
+    }
+
+    /** Report all known, available backup transports by {@link ComponentName}. */
+    @Nullable
+    public ComponentName[] listAllTransportComponents(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "listAllTransportComponents()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.listAllTransportComponents();
+    }
+
+    @Override
+    public String[] getTransportWhitelist() {
+        int userId = binderGetCallingUserId();
+        if (!isUserReadyForBackup(userId)) {
+            return null;
+        }
+        // No permission check, intentionally.
+        String[] whitelistedTransports = new String[mTransportWhitelist.size()];
+        int i = 0;
+        for (ComponentName component : mTransportWhitelist) {
+            whitelistedTransports[i] = component.flattenToShortString();
+            i++;
+        }
+        return whitelistedTransports;
+    }
+
+    @Override
+    public void updateTransportAttributesForUser(
+            int userId,
+            ComponentName transportComponent,
+            String name,
+            @Nullable Intent configurationIntent,
+            String currentDestinationString,
+            @Nullable Intent dataManagementIntent,
+            CharSequence dataManagementLabel) {
+        if (isUserReadyForBackup(userId)) {
+            updateTransportAttributes(
+                    userId,
+                    transportComponent,
+                    name,
+                    configurationIntent,
+                    currentDestinationString,
+                    dataManagementIntent,
+                    dataManagementLabel);
+        }
+    }
+
+    /**
+     * Update the attributes of the transport identified by {@code transportComponent}. If the
+     * specified transport has not been bound at least once (for registration), this call will be
+     * ignored. Only the host process of the transport can change its description, otherwise a
+     * {@link SecurityException} will be thrown.
+     *
+     * @param transportComponent The identity of the transport being described.
+     * @param name A {@link String} with the new name for the transport. This is NOT for
+     *     identification. MUST NOT be {@code null}.
+     * @param configurationIntent An {@link Intent} that can be passed to {@link
+     *     Context#startActivity} in order to launch the transport's configuration UI. It may be
+     *     {@code null} if the transport does not offer any user-facing configuration UI.
+     * @param currentDestinationString A {@link String} describing the destination to which the
+     *     transport is currently sending data. MUST NOT be {@code null}.
+     * @param dataManagementIntent An {@link Intent} that can be passed to {@link
+     *     Context#startActivity} in order to launch the transport's data-management UI. It may be
+     *     {@code null} if the transport does not offer any user-facing data management UI.
+     * @param dataManagementLabel A {@link CharSequence} to be used as the label for the transport's
+     *     data management affordance. This MUST be {@code null} when dataManagementIntent is {@code
+     *     null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}.
+     * @throws SecurityException If the UID of the calling process differs from the package UID of
+     *     {@code transportComponent} or if the caller does NOT have BACKUP permission.
+     */
+    public void updateTransportAttributes(
+            @UserIdInt int userId,
+            ComponentName transportComponent,
+            String name,
+            @Nullable Intent configurationIntent,
+            String currentDestinationString,
+            @Nullable Intent dataManagementIntent,
+            CharSequence dataManagementLabel) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "updateTransportAttributes()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.updateTransportAttributes(
+                    transportComponent,
+                    name,
+                    configurationIntent,
+                    currentDestinationString,
+                    dataManagementIntent,
+                    dataManagementLabel);
+        }
+    }
+
+    @Override
+    public String selectBackupTransportForUser(int userId, String transport)
+            throws RemoteException {
+        return (isUserReadyForBackup(userId))
+                ? selectBackupTransport(userId, transport) : null;
+    }
+
+    @Override
+    public String selectBackupTransport(String transport) throws RemoteException {
+        return selectBackupTransportForUser(binderGetCallingUserId(), transport);
+    }
+
+    /**
+     * Selects transport {@code transportName} and returns the previously selected transport.
+     *
+     * @deprecated Use {@link #selectBackupTransportAsync(ComponentName,
+     *     ISelectBackupTransportCallback)} instead.
+     */
+    @Deprecated
+    @Nullable
+    public String selectBackupTransport(@UserIdInt int userId, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "selectBackupTransport()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.selectBackupTransport(transportName);
+    }
+
+    @Override
+    public void selectBackupTransportAsyncForUser(int userId, ComponentName transport,
+            ISelectBackupTransportCallback listener) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            selectBackupTransportAsync(userId, transport, listener);
+        } else {
+            if (listener != null) {
+                try {
+                    listener.onFailure(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                } catch (RemoteException ex) {
+                    // ignore
+                }
+            }
+        }
+    }
+
+    /**
+     * Selects transport {@code transportComponent} asynchronously and notifies {@code listener}
+     * with the result upon completion.
+     */
+    public void selectBackupTransportAsync(
+            @UserIdInt int userId,
+            ComponentName transportComponent,
+            ISelectBackupTransportCallback listener) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "selectBackupTransportAsync()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.selectBackupTransportAsync(transportComponent, listener);
+        }
+    }
+
+    @Override
+    public Intent getConfigurationIntentForUser(int userId, String transport)
+            throws RemoteException {
+        return isUserReadyForBackup(userId) ? getConfigurationIntent(userId, transport)
+                : null;
+    }
+
+    @Override
+    public Intent getConfigurationIntent(String transport)
+            throws RemoteException {
+        return getConfigurationIntentForUser(binderGetCallingUserId(), transport);
+    }
+
+    /**
+     * Supply the configuration intent for the given transport. If the name is not one of the
+     * available transports, or if the transport does not supply any configuration UI, the method
+     * returns {@code null}.
+     */
+    @Nullable
+    public Intent getConfigurationIntent(@UserIdInt int userId, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getConfigurationIntent()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getConfigurationIntent(transportName);
+    }
+
+    @Override
+    public String getDestinationStringForUser(int userId, String transport) throws RemoteException {
+        return isUserReadyForBackup(userId) ? getDestinationString(userId, transport)
+                : null;
+    }
+
+    @Override
+    public String getDestinationString(String transport) throws RemoteException {
+        return getDestinationStringForUser(binderGetCallingUserId(), transport);
+    }
+
+    /**
+     * Supply the current destination string for the given transport. If the name is not one of the
+     * registered transports the method will return null.
+     *
+     * <p>This string is used VERBATIM as the summary text of the relevant Settings item.
+     *
+     * @param transportName The name of the registered transport.
+     * @return The current destination string or null if the transport is not registered.
+     */
+    @Nullable
+    public String getDestinationString(@UserIdInt int userId, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getDestinationString()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getDestinationString(transportName);
+    }
+
+    @Override
+    public Intent getDataManagementIntentForUser(int userId, String transport)
+            throws RemoteException {
+        return isUserReadyForBackup(userId)
+                ? getDataManagementIntent(userId, transport) : null;
+    }
+
+    @Override
+    public Intent getDataManagementIntent(String transport)
+            throws RemoteException {
+        return getDataManagementIntentForUser(binderGetCallingUserId(), transport);
+    }
+
+    /** Supply the manage-data intent for the given transport. */
+    @Nullable
+    public Intent getDataManagementIntent(@UserIdInt int userId, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getDataManagementIntent()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getDataManagementIntent(transportName);
+    }
+
+    @Override
+    public CharSequence getDataManagementLabelForUser(int userId, String transport)
+            throws RemoteException {
+        return isUserReadyForBackup(userId) ? getDataManagementLabel(userId, transport)
+                : null;
+    }
+
+    /**
+     * Supply the menu label for affordances that fire the manage-data intent for the given
+     * transport.
+     */
+    @Nullable
+    public CharSequence getDataManagementLabel(@UserIdInt int userId, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getDataManagementLabel()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.getDataManagementLabel(transportName);
+    }
+
+    @Override
+    public IRestoreSession beginRestoreSessionForUser(
+            int userId, String packageName, String transportID) throws RemoteException {
+        return isUserReadyForBackup(userId)
+                ? beginRestoreSession(userId, packageName, transportID) : null;
+    }
+
+    /**
+     * Begin a restore for the specified package {@code packageName} using the specified transport
+     * {@code transportName}.
+     */
+    @Nullable
+    public IRestoreSession beginRestoreSession(
+            @UserIdInt int userId, String packageName, String transportName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "beginRestoreSession()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.beginRestoreSession(packageName, transportName);
+    }
+
+    @Override
+    public void opCompleteForUser(int userId, int token, long result) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            opComplete(userId, token, result);
+        }
+    }
+
+    @Override
+    public void opComplete(int token, long result) throws RemoteException {
+        opCompleteForUser(binderGetCallingUserId(), token, result);
+    }
+
+    /**
+     * Used by a currently-active backup agent to notify the service that it has completed its given
+     * outstanding asynchronous backup/restore operation.
+     */
+    public void opComplete(@UserIdInt int userId, int token, long result) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "opComplete()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.opComplete(token, result);
+        }
+    }
+
+    @Override
+    public long getAvailableRestoreTokenForUser(int userId, String packageName) {
+        return isUserReadyForBackup(userId) ? getAvailableRestoreToken(userId, packageName) : 0;
+    }
+
+    /**
+     * Get the restore-set token for the best-available restore set for this {@code packageName}:
+     * the active set if possible, else the ancestral one. Returns zero if none available.
+     */
+    public long getAvailableRestoreToken(@UserIdInt int userId, String packageName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "getAvailableRestoreToken()");
+
+        return userBackupManagerService == null
+                ? 0
+                : userBackupManagerService.getAvailableRestoreToken(packageName);
+    }
+
+    @Override
+    public boolean isAppEligibleForBackupForUser(int userId, String packageName) {
+        return isUserReadyForBackup(userId) && isAppEligibleForBackup(userId,
+                packageName);
+    }
+
+    /** Checks if the given package {@code packageName} is eligible for backup. */
+    public boolean isAppEligibleForBackup(@UserIdInt int userId, String packageName) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "isAppEligibleForBackup()");
+
+        return userBackupManagerService != null
+                && userBackupManagerService.isAppEligibleForBackup(packageName);
+    }
+
+    @Override
+    public String[] filterAppsEligibleForBackupForUser(int userId, String[] packages) {
+        return isUserReadyForBackup(userId) ? filterAppsEligibleForBackup(userId, packages) : null;
+    }
+
+    /**
+     * Returns from the inputted packages {@code packages}, the ones that are eligible for backup.
+     */
+    @Nullable
+    public String[] filterAppsEligibleForBackup(@UserIdInt int userId, String[] packages) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "filterAppsEligibleForBackup()");
+
+        return userBackupManagerService == null
+                ? null
+                : userBackupManagerService.filterAppsEligibleForBackup(packages);
+    }
+
+    @Override
+    public int requestBackupForUser(@UserIdInt int userId, String[] packages, IBackupObserver
+            observer, IBackupManagerMonitor monitor, int flags) throws RemoteException {
+        if (!isUserReadyForBackup(userId)) {
+            return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
+        }
+        return requestBackup(userId, packages, observer, monitor, flags);
+    }
+
+    @Override
+    public int requestBackup(String[] packages, IBackupObserver observer,
+            IBackupManagerMonitor monitor, int flags) throws RemoteException {
+        return requestBackupForUser(binderGetCallingUserId(), packages,
+                observer, monitor, flags);
+    }
+
+    /**
+     * Requests a backup for the inputted {@code packages} with a specified callback {@link
+     * IBackupManagerMonitor} for receiving events during the operation.
+     */
+    public int requestBackup(
+            @UserIdInt int userId,
+            String[] packages,
+            IBackupObserver observer,
+            IBackupManagerMonitor monitor,
+            int flags) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "requestBackup()");
+
+        return userBackupManagerService == null
+                ? BackupManager.ERROR_BACKUP_NOT_ALLOWED
+                : userBackupManagerService.requestBackup(packages, observer, monitor, flags);
+    }
+
+    @Override
+    public void cancelBackupsForUser(@UserIdInt int userId) throws RemoteException {
+        if (isUserReadyForBackup(userId)) {
+            cancelBackups(userId);
+        }
+    }
+
+    @Override
+    public void cancelBackups() throws RemoteException {
+        cancelBackupsForUser(binderGetCallingUserId());
+    }
+
+    /** Cancel all running backup operations. */
+    public void cancelBackups(@UserIdInt int userId) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "cancelBackups()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.cancelBackups();
+        }
+    }
+
+    /**
+     * Returns a {@link UserHandle} for the user that has {@code ancestralSerialNumber} as the
+     * serial number of its ancestral work profile or null if there is no {@link
+     * UserBackupManagerService} associated with that user.
+     *
+     * <p> The ancestral work profile is set by {@link #setAncestralSerialNumber(long)}
+     * and it corresponds to the profile that was used to restore to the callers profile.
+     */
+    @Override
+    @Nullable
+    public UserHandle getUserForAncestralSerialNumber(long ancestralSerialNumber) {
+        if (mGlobalDisable) {
+            return null;
+        }
+        int callingUserId = Binder.getCallingUserHandle().getIdentifier();
+        long oldId = Binder.clearCallingIdentity();
+        final int[] userIds;
+        try {
+            userIds = getUserManager().getProfileIds(callingUserId, false);
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+
+        for (int userId : userIds) {
+            UserBackupManagerService userBackupManagerService = mUserServices.get(userId);
+            if (userBackupManagerService != null) {
+                if (userBackupManagerService.getAncestralSerialNumber() == ancestralSerialNumber) {
+                    return UserHandle.of(userId);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Sets the ancestral work profile for the calling user.
+     *
+     * <p> The ancestral work profile corresponds to the profile that was used to restore to the
+     * callers profile.
+     */
+    @Override
+    public void setAncestralSerialNumber(long ancestralSerialNumber) {
+        if (mGlobalDisable) {
+            return;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(
+                        Binder.getCallingUserHandle().getIdentifier(),
+                        "setAncestralSerialNumber()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.setAncestralSerialNumber(ancestralSerialNumber);
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) {
+            return;
+        }
+        dumpWithoutCheckingPermission(fd, pw, args);
+    }
+
+    @VisibleForTesting
+    void dumpWithoutCheckingPermission(FileDescriptor fd, PrintWriter pw, String[] args) {
+        int userId = binderGetCallingUserId();
+        if (!isUserReadyForBackup(userId)) {
+            pw.println("Inactive");
+            return;
+        }
+
+        if (args != null) {
+            for (String arg : args) {
+                if ("-h".equals(arg)) {
+                    pw.println("'dumpsys backup' optional arguments:");
+                    pw.println("  -h       : this help text");
+                    pw.println("  a[gents] : dump information about defined backup agents");
+                    pw.println("  transportclients : dump information about transport clients");
+                    pw.println("  transportstats : dump transport statts");
+                    pw.println("  users    : dump the list of users for which backup service "
+                            + "is running");
+                    return;
+                } else if ("users".equals(arg.toLowerCase())) {
+                    pw.print(DUMP_RUNNING_USERS_MESSAGE);
+                    for (int i = 0; i < mUserServices.size(); i++) {
+                        pw.print(" " + mUserServices.keyAt(i));
+                    }
+                    pw.println();
+                    return;
+                }
+            }
+        }
+
+        for (int i = 0; i < mUserServices.size(); i++) {
+            UserBackupManagerService userBackupManagerService =
+                    getServiceForUserIfCallerHasPermission(mUserServices.keyAt(i), "dump()");
+            if (userBackupManagerService != null) {
+                userBackupManagerService.dump(fd, pw, args);
+            }
+        }
+    }
+
+    /**
+     * Used by the {@link JobScheduler} to run a full backup when conditions are right. The model we
+     * use is to perform one app backup per scheduled job execution, and to reschedule the job with
+     * zero latency as long as conditions remain right and we still have work to do.
+     *
+     * @return Whether ongoing work will continue. The return value here will be passed along as the
+     *     return value to the callback {@link JobService#onStartJob(JobParameters)}.
+     */
+    public boolean beginFullBackup(@UserIdInt int userId, FullBackupJob scheduledJob) {
+        if (!isUserReadyForBackup(userId)) {
+            return false;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "beginFullBackup()");
+
+        return userBackupManagerService != null
+                && userBackupManagerService.beginFullBackup(scheduledJob);
+    }
+
+    /**
+     * Used by the {@link JobScheduler} to end the current full backup task when conditions are no
+     * longer met for running the full backup job.
+     */
+    public void endFullBackup(@UserIdInt int userId) {
+        if (!isUserReadyForBackup(userId)) {
+            return;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "endFullBackup()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.endFullBackup();
+        }
+    }
+
+    /**
+     * Excludes keys from KV restore for a given package. The corresponding data will be excluded
+     * from the data set available the backup agent during restore. However,  final list  of keys
+     * that have been excluded will be passed to the agent to make it aware of the exclusions.
+     */
+    public void excludeKeysFromRestore(String packageName, List<String> keys) {
+        int userId = Binder.getCallingUserHandle().getIdentifier();
+        if (!isUserReadyForBackup(userId)) {
+            Slog.w(TAG, "Returning from excludeKeysFromRestore as backup for user" + userId +
+                    " is not initialized yet");
+            return;
+        }
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId, "excludeKeysFromRestore()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.excludeKeysFromRestore(packageName, keys);
+        }
+    }
+
+    /**
+     * Returns the {@link UserBackupManagerService} instance for the specified user {@code userId}.
+     * If the user is not registered with the service (either the user is locked or not eligible for
+     * the backup service) then return {@code null}.
+     *
+     * @param userId The id of the user to retrieve its instance of {@link
+     *     UserBackupManagerService}.
+     * @param caller A {@link String} identifying the caller for logging purposes.
+     * @throws SecurityException if {@code userId} is different from the calling user id and the
+     *     caller does NOT have the android.permission.INTERACT_ACROSS_USERS_FULL permission.
+     */
+    @Nullable
+    @VisibleForTesting
+    UserBackupManagerService getServiceForUserIfCallerHasPermission(
+            @UserIdInt int userId, String caller) {
+        enforceCallingPermissionOnUserId(userId, caller);
+        UserBackupManagerService userBackupManagerService = mUserServices.get(userId);
+        if (userBackupManagerService == null) {
+            Slog.w(TAG, "Called " + caller + " for unknown user: " + userId);
+        }
+        return userBackupManagerService;
+    }
+
+    /**
+     * If {@code userId} is different from the calling user id, then the caller must hold the
+     * android.permission.INTERACT_ACROSS_USERS_FULL permission.
+     *
+     * @param userId User id on which the backup operation is being requested.
+     * @param message A message to include in the exception if it is thrown.
+     */
+    void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) {
+        if (Binder.getCallingUserHandle().getIdentifier() != userId) {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
+        }
+    }
+
+    /** Implementation to receive lifecycle event callbacks for system services. */
+    public static class Lifecycle extends SystemService {
+        public Lifecycle(Context context) {
+            this(context, new BackupManagerService(context));
+        }
+
+        @VisibleForTesting
+        Lifecycle(Context context, BackupManagerService backupManagerService) {
+            super(context);
+            sInstance = backupManagerService;
+        }
+
+        @Override
+        public void onStart() {
+            publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance);
+        }
+
+        @Override
+        public void onUnlockUser(int userId) {
+            sInstance.onUnlockUser(userId);
+        }
+
+        @Override
+        public void onStopUser(int userId) {
+            sInstance.onStopUser(userId);
+        }
+
+        @VisibleForTesting
+        void publishService(String name, IBinder service) {
+            publishBinderService(name, service);
+        }
+    }
+}
diff --git a/com/android/server/backup/BackupManagerServiceRoboTest.java b/com/android/server/backup/BackupManagerServiceRoboTest.java
new file mode 100644
index 0000000..a1bfcdf
--- /dev/null
+++ b/com/android/server/backup/BackupManagerServiceRoboTest.java
@@ -0,0 +1,1652 @@
+/*
+ * Copyright (C) 2019 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.Manifest.permission.BACKUP;
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.PACKAGE_USAGE_STATS;
+
+import static com.android.server.backup.testing.TransportData.backupTransport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.UserIdInt;
+import android.app.Application;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.app.backup.IFullBackupRestoreObserver;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import com.android.server.backup.testing.TransportData;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowBinder;
+import com.android.server.testing.shadows.ShadowEnvironment;
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+import com.android.server.testing.shadows.ShadowUserManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowContextWrapper;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/** Tests for {@link BackupManagerService}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+                ShadowApplicationPackageManager.class,
+                ShadowBinder.class,
+                ShadowUserManager.class,
+                ShadowEnvironment.class,
+                ShadowSystemServiceRegistry.class
+        })
+@Presubmit
+public class BackupManagerServiceRoboTest {
+    private static final String TEST_PACKAGE = "package";
+    private static final String TEST_TRANSPORT = "transport";
+    private static final String[] ADB_TEST_PACKAGES = {TEST_PACKAGE};
+
+    private Context mContext;
+    private ShadowContextWrapper mShadowContext;
+    private ShadowUserManager mShadowUserManager;
+    @UserIdInt private int mUserOneId;
+    @UserIdInt private int mUserTwoId;
+    @Mock private UserBackupManagerService mUserSystemService;
+    @Mock private UserBackupManagerService mUserOneService;
+    @Mock private UserBackupManagerService mUserTwoService;
+
+    /** Setup */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        Application application = RuntimeEnvironment.application;
+        mContext = application;
+        mShadowContext = shadowOf(application);
+        mShadowUserManager = Shadow.extract(UserManager.get(application));
+
+        mUserOneId = UserHandle.USER_SYSTEM + 1;
+        mUserTwoId = mUserOneId + 1;
+        mShadowUserManager.addUser(mUserOneId, "mUserOneId", 0);
+        mShadowUserManager.addUser(mUserTwoId, "mUserTwoId", 0);
+
+        mShadowContext.grantPermissions(BACKUP);
+        mShadowContext.grantPermissions(INTERACT_ACROSS_USERS_FULL);
+
+        ShadowBinder.setCallingUid(Process.SYSTEM_UID);
+    }
+
+    /** Test that the service registers users. */
+    @Test
+    public void testStartServiceForUser_registersUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        backupManagerService.setBackupServiceActive(mUserOneId, true);
+
+        backupManagerService.startServiceForUser(mUserOneId);
+
+        SparseArray<UserBackupManagerService> serviceUsers = backupManagerService.getUserServices();
+        assertThat(serviceUsers.size()).isEqualTo(1);
+        assertThat(serviceUsers.get(mUserOneId)).isNotNull();
+    }
+
+    /** Test that the service registers users. */
+    @Test
+    public void testStartServiceForUser_withServiceInstance_registersUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        backupManagerService.setBackupServiceActive(mUserOneId, true);
+
+        backupManagerService.startServiceForUser(mUserOneId, mUserOneService);
+
+        SparseArray<UserBackupManagerService> serviceUsers = backupManagerService.getUserServices();
+        assertThat(serviceUsers.size()).isEqualTo(1);
+        assertThat(serviceUsers.get(mUserOneId)).isEqualTo(mUserOneService);
+    }
+
+    /** Test that the service unregisters users when stopped. */
+    @Test
+    public void testStopServiceForUser_forRegisteredUser_unregistersCorrectUser() throws Exception {
+        BackupManagerService backupManagerService =
+                createServiceAndRegisterUser(mUserOneId, mUserOneService);
+        backupManagerService.startServiceForUser(mUserTwoId, mUserTwoService);
+        ShadowBinder.setCallingUid(Process.SYSTEM_UID);
+
+        backupManagerService.stopServiceForUser(mUserOneId);
+
+        SparseArray<UserBackupManagerService> serviceUsers = backupManagerService.getUserServices();
+        assertThat(serviceUsers.size()).isEqualTo(1);
+        assertThat(serviceUsers.get(mUserOneId)).isNull();
+        assertThat(serviceUsers.get(mUserTwoId)).isEqualTo(mUserTwoService);
+    }
+
+    /** Test that the service unregisters users when stopped. */
+    @Test
+    public void testStopServiceForUser_forRegisteredUser_tearsDownCorrectUser() throws Exception {
+        BackupManagerService backupManagerService =
+                createServiceAndRegisterUser(mUserOneId, mUserOneService);
+        backupManagerService.setBackupServiceActive(mUserTwoId, true);
+        backupManagerService.startServiceForUser(mUserTwoId, mUserTwoService);
+
+        backupManagerService.stopServiceForUser(mUserOneId);
+
+        verify(mUserOneService).tearDownService();
+        verify(mUserTwoService, never()).tearDownService();
+    }
+
+    /** Test that the service unregisters users when stopped. */
+    @Test
+    public void testStopServiceForUser_forUnknownUser_doesNothing() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        backupManagerService.setBackupServiceActive(mUserOneId, true);
+        ShadowBinder.setCallingUid(Process.SYSTEM_UID);
+
+        backupManagerService.stopServiceForUser(mUserOneId);
+
+        SparseArray<UserBackupManagerService> serviceUsers = backupManagerService.getUserServices();
+        assertThat(serviceUsers.size()).isEqualTo(0);
+    }
+
+    // ---------------------------------------------
+    // Backup agent tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testDataChanged_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.dataChanged(mUserOneId, TEST_PACKAGE);
+
+        verify(mUserOneService).dataChanged(TEST_PACKAGE);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testDataChanged_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.dataChanged(mUserTwoId, TEST_PACKAGE);
+
+        verify(mUserOneService, never()).dataChanged(TEST_PACKAGE);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testAgentConnected_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        IBinder agentBinder = mock(IBinder.class);
+
+        backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder);
+
+        verify(mUserOneService).agentConnected(TEST_PACKAGE, agentBinder);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testAgentConnected_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        IBinder agentBinder = mock(IBinder.class);
+
+        backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder);
+
+        verify(mUserOneService, never()).agentConnected(TEST_PACKAGE, agentBinder);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testOpComplete_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.opComplete(mUserOneId, /* token */ 0, /* result */ 0L);
+
+        verify(mUserOneService).opComplete(/* token */ 0, /* result */ 0L);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testOpComplete_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.opComplete(mUserTwoId, /* token */ 0, /* result */ 0L);
+
+        verify(mUserOneService, never()).opComplete(/* token */ 0, /* result */ 0L);
+    }
+
+    // ---------------------------------------------
+    // Transport tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testInitializeTransports_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        String[] transports = {TEST_TRANSPORT};
+
+        backupManagerService.initializeTransports(mUserOneId, transports, /* observer */ null);
+
+        verify(mUserOneService).initializeTransports(transports, /* observer */ null);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testInitializeTransports_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        String[] transports = {TEST_TRANSPORT};
+
+        backupManagerService.initializeTransports(mUserTwoId, transports, /* observer */ null);
+
+        verify(mUserOneService, never()).initializeTransports(transports, /* observer */ null);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testClearBackupData_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.clearBackupData(mUserOneId, TEST_TRANSPORT, TEST_PACKAGE);
+
+        verify(mUserOneService).clearBackupData(TEST_TRANSPORT, TEST_PACKAGE);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testClearBackupData_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.clearBackupData(mUserTwoId, TEST_TRANSPORT, TEST_PACKAGE);
+
+        verify(mUserOneService, never()).clearBackupData(TEST_TRANSPORT, TEST_PACKAGE);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetCurrentTransport_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getCurrentTransport(mUserOneId);
+
+        verify(mUserOneService).getCurrentTransport();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetCurrentTransport_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getCurrentTransport(mUserTwoId);
+
+        verify(mUserOneService, never()).getCurrentTransport();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetCurrentTransportComponent_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getCurrentTransportComponent(mUserOneId);
+
+        verify(mUserOneService).getCurrentTransportComponent();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetCurrentTransportComponent_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getCurrentTransportComponent(mUserTwoId);
+
+        verify(mUserOneService, never()).getCurrentTransportComponent();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testListAllTransports_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.listAllTransports(mUserOneId);
+
+        verify(mUserOneService).listAllTransports();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testListAllTransports_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.listAllTransports(mUserTwoId);
+
+        verify(mUserOneService, never()).listAllTransports();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testListAllTransportComponents_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.listAllTransportComponents(mUserOneId);
+
+        verify(mUserOneService).listAllTransportComponents();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testListAllTransportComponents_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.listAllTransportComponents(mUserTwoId);
+
+        verify(mUserOneService, never()).listAllTransportComponents();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testSelectBackupTransport_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.selectBackupTransport(mUserOneId, TEST_TRANSPORT);
+
+        verify(mUserOneService).selectBackupTransport(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testSelectBackupTransport_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.selectBackupTransport(mUserTwoId, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).selectBackupTransport(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testSelectTransportAsync_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        TransportData transport = backupTransport();
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+        backupManagerService.selectBackupTransportAsync(
+                mUserOneId, transport.getTransportComponent(), callback);
+
+        verify(mUserOneService)
+                .selectBackupTransportAsync(transport.getTransportComponent(), callback);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testSelectBackupTransportAsync_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        TransportData transport = backupTransport();
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+        backupManagerService.selectBackupTransportAsync(
+                mUserTwoId, transport.getTransportComponent(), callback);
+
+        verify(mUserOneService, never())
+                .selectBackupTransportAsync(transport.getTransportComponent(), callback);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetConfigurationIntent_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getConfigurationIntent(mUserOneId, TEST_TRANSPORT);
+
+        verify(mUserOneService).getConfigurationIntent(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetConfigurationIntent_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getConfigurationIntent(mUserTwoId, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).getConfigurationIntent(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetDestinationString_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDestinationString(mUserOneId, TEST_TRANSPORT);
+
+        verify(mUserOneService).getDestinationString(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetDestinationString_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDestinationString(mUserTwoId, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).getDestinationString(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetDataManagementIntent_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDataManagementIntent(mUserOneId, TEST_TRANSPORT);
+
+        verify(mUserOneService).getDataManagementIntent(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetDataManagementIntent_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDataManagementIntent(mUserTwoId, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).getDataManagementIntent(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetDataManagementLabel_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDataManagementLabel(mUserOneId, TEST_TRANSPORT);
+
+        verify(mUserOneService).getDataManagementLabel(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetDataManagementLabel_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getDataManagementLabel(mUserTwoId, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).getDataManagementLabel(TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testUpdateTransportAttributes_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        TransportData transport = backupTransport();
+        Intent configurationIntent = new Intent();
+        Intent dataManagementIntent = new Intent();
+
+        backupManagerService.updateTransportAttributes(
+                mUserOneId,
+                transport.getTransportComponent(),
+                transport.transportName,
+                configurationIntent,
+                "currentDestinationString",
+                dataManagementIntent,
+                "dataManagementLabel");
+
+        verify(mUserOneService)
+                .updateTransportAttributes(
+                        transport.getTransportComponent(),
+                        transport.transportName,
+                        configurationIntent,
+                        "currentDestinationString",
+                        dataManagementIntent,
+                        "dataManagementLabel");
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testUpdateTransportAttributes_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        TransportData transport = backupTransport();
+        Intent configurationIntent = new Intent();
+        Intent dataManagementIntent = new Intent();
+
+        backupManagerService.updateTransportAttributes(
+                mUserTwoId,
+                transport.getTransportComponent(),
+                transport.transportName,
+                configurationIntent,
+                "currentDestinationString",
+                dataManagementIntent,
+                "dataManagementLabel");
+
+        verify(mUserOneService, never())
+                .updateTransportAttributes(
+                        transport.getTransportComponent(),
+                        transport.transportName,
+                        configurationIntent,
+                        "currentDestinationString",
+                        dataManagementIntent,
+                        "dataManagementLabel");
+    }
+
+    // ---------------------------------------------
+    // Settings tests
+    // ---------------------------------------------
+
+    /**
+     * Test that the backup services throws a {@link SecurityException} if the caller does not have
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testSetBackupEnabled_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        expectThrows(
+                SecurityException.class,
+                () -> backupManagerService.setBackupEnabled(mUserTwoId, true));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testSetBackupEnabled_withPermission_propagatesForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.setBackupEnabled(mUserTwoId, true);
+
+        verify(mUserTwoService).setBackupEnabled(true);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testSetBackupEnabled_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.setBackupEnabled(mUserOneId, true);
+
+        verify(mUserOneService).setBackupEnabled(true);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testSetBackupEnabled_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.setBackupEnabled(mUserTwoId, true);
+
+        verify(mUserOneService, never()).setBackupEnabled(true);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testSetAutoRestore_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.setAutoRestore(mUserOneId, true);
+
+        verify(mUserOneService).setAutoRestore(true);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testSetAutoRestore_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.setAutoRestore(mUserTwoId, true);
+
+        verify(mUserOneService, never()).setAutoRestore(true);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testIsBackupEnabled_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.isBackupEnabled(mUserOneId);
+
+        verify(mUserOneService).isBackupEnabled();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testIsBackupEnabled_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.isBackupEnabled(mUserTwoId);
+
+        verify(mUserOneService, never()).isBackupEnabled();
+    }
+
+    // ---------------------------------------------
+    // Backup tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testIsAppEligibleForBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.isAppEligibleForBackup(mUserOneId, TEST_PACKAGE);
+
+        verify(mUserOneService).isAppEligibleForBackup(TEST_PACKAGE);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testIsAppEligibleForBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.isAppEligibleForBackup(mUserTwoId, TEST_PACKAGE);
+
+        verify(mUserOneService, never()).isAppEligibleForBackup(TEST_PACKAGE);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testFilterAppsEligibleForBackup_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        String[] packages = {TEST_PACKAGE};
+
+        backupManagerService.filterAppsEligibleForBackup(mUserOneId, packages);
+
+        verify(mUserOneService).filterAppsEligibleForBackup(packages);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testFilterAppsEligibleForBackup_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        String[] packages = {TEST_PACKAGE};
+
+        backupManagerService.filterAppsEligibleForBackup(mUserTwoId, packages);
+
+        verify(mUserOneService, never()).filterAppsEligibleForBackup(packages);
+    }
+
+    /**
+     * Test verifying that {@link BackupManagerService#backupNow(int)} throws a {@link
+     * SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+     */
+    @Test
+    public void testBackupNow_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        expectThrows(SecurityException.class, () -> backupManagerService.backupNow(mUserTwoId));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testBackupNow_withPermission_propagatesForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.backupNow(mUserTwoId);
+
+        verify(mUserTwoService).backupNow();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testBackupNow_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.backupNow(mUserOneId);
+
+        verify(mUserOneService).backupNow();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testBackupNow_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.backupNow(mUserTwoId);
+
+        verify(mUserOneService, never()).backupNow();
+    }
+
+    /**
+     * Test that the backup services throws a {@link SecurityException} if the caller does not have
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testRequestBackup_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        String[] packages = {TEST_PACKAGE};
+        IBackupObserver observer = mock(IBackupObserver.class);
+        IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.requestBackup(
+                                mUserTwoId, packages, observer, monitor, 0));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testRequestBackup_withPermission_propagatesForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+
+        String[] packages = {TEST_PACKAGE};
+        IBackupObserver observer = mock(IBackupObserver.class);
+        IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.requestBackup(mUserTwoId, packages, observer, monitor, /* flags */ 0);
+
+        verify(mUserTwoService).requestBackup(packages, observer, monitor, /* flags */ 0);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testRequestBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        String[] packages = {TEST_PACKAGE};
+        IBackupObserver observer = mock(IBackupObserver.class);
+        IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.requestBackup(mUserOneId, packages, observer, monitor, /* flags */ 0);
+
+        verify(mUserOneService).requestBackup(packages, observer, monitor, /* flags */ 0);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testRequestBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        String[] packages = {TEST_PACKAGE};
+        IBackupObserver observer = mock(IBackupObserver.class);
+        IBackupManagerMonitor monitor = mock(IBackupManagerMonitor.class);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.requestBackup(mUserTwoId, packages, observer, monitor, /* flags */ 0);
+
+        verify(mUserOneService, never()).requestBackup(packages, observer, monitor, /* flags */ 0);
+    }
+
+    /**
+     * Test verifying that {@link BackupManagerService#cancelBackups(int)} throws a {@link
+     * SecurityException} if the caller does not have INTERACT_ACROSS_USERS_FULL permission.
+     */
+    @Test
+    public void testCancelBackups_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        expectThrows(SecurityException.class, () -> backupManagerService.cancelBackups(mUserTwoId));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testCancelBackups_withPermission_propagatesForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.cancelBackups(mUserTwoId);
+
+        verify(mUserTwoService).cancelBackups();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testCancelBackups_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.cancelBackups(mUserOneId);
+
+        verify(mUserOneService).cancelBackups();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testCancelBackups_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.cancelBackups(mUserTwoId);
+
+        verify(mUserOneService, never()).cancelBackups();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testBeginFullBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, UserHandle.USER_SYSTEM, mUserOneService);
+        FullBackupJob job = new FullBackupJob();
+
+        backupManagerService.beginFullBackup(UserHandle.USER_SYSTEM, job);
+
+        verify(mUserOneService).beginFullBackup(job);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testBeginFullBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        FullBackupJob job = new FullBackupJob();
+
+        backupManagerService.beginFullBackup(UserHandle.USER_SYSTEM, job);
+
+        verify(mUserOneService, never()).beginFullBackup(job);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testEndFullBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, UserHandle.USER_SYSTEM, mUserOneService);
+
+        backupManagerService.endFullBackup(UserHandle.USER_SYSTEM);
+
+        verify(mUserOneService).endFullBackup();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testEndFullBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+
+        backupManagerService.endFullBackup(UserHandle.USER_SYSTEM);
+
+        verify(mUserOneService, never()).endFullBackup();
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testFullTransportBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        String[] packages = {TEST_PACKAGE};
+
+        backupManagerService.fullTransportBackup(mUserOneId, packages);
+
+        verify(mUserOneService).fullTransportBackup(packages);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testFullTransportBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        String[] packages = {TEST_PACKAGE};
+
+        backupManagerService.fullTransportBackup(mUserTwoId, packages);
+
+        verify(mUserOneService, never()).fullTransportBackup(packages);
+    }
+
+    // ---------------------------------------------
+    // Restore tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testRestoreAtInstall_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.restoreAtInstall(mUserOneId, TEST_PACKAGE, /* token */ 0);
+
+        verify(mUserOneService).restoreAtInstall(TEST_PACKAGE, /* token */ 0);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testRestoreAtInstall_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.restoreAtInstall(mUserTwoId, TEST_PACKAGE, /* token */ 0);
+
+        verify(mUserOneService, never()).restoreAtInstall(TEST_PACKAGE, /* token */ 0);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testBeginRestoreSession_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.beginRestoreSession(mUserOneId, TEST_PACKAGE, TEST_TRANSPORT);
+
+        verify(mUserOneService).beginRestoreSession(TEST_PACKAGE, TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testBeginRestoreSession_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.beginRestoreSession(mUserTwoId, TEST_PACKAGE, TEST_TRANSPORT);
+
+        verify(mUserOneService, never()).beginRestoreSession(TEST_PACKAGE, TEST_TRANSPORT);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testGetAvailableRestoreToken_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getAvailableRestoreToken(mUserOneId, TEST_PACKAGE);
+
+        verify(mUserOneService).getAvailableRestoreToken(TEST_PACKAGE);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testGetAvailableRestoreToken_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.getAvailableRestoreToken(mUserTwoId, TEST_PACKAGE);
+
+        verify(mUserOneService, never()).getAvailableRestoreToken(TEST_PACKAGE);
+    }
+
+    // ---------------------------------------------
+    // Adb backup/restore tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testSetBackupPassword_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, UserHandle.USER_SYSTEM, mUserOneService);
+        ShadowBinder.setCallingUserHandle(UserHandle.of(UserHandle.USER_SYSTEM));
+
+        backupManagerService.setBackupPassword("currentPassword", "newPassword");
+
+        verify(mUserOneService).setBackupPassword("currentPassword", "newPassword");
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testSetBackupPassword_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+
+        backupManagerService.setBackupPassword("currentPassword", "newPassword");
+
+        verify(mUserOneService, never()).setBackupPassword("currentPassword", "newPassword");
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testHasBackupPassword_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, UserHandle.USER_SYSTEM, mUserOneService);
+        ShadowBinder.setCallingUserHandle(UserHandle.of(UserHandle.USER_SYSTEM));
+
+        backupManagerService.hasBackupPassword();
+
+        verify(mUserOneService).hasBackupPassword();
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testHasBackupPassword_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createService();
+
+        backupManagerService.hasBackupPassword();
+
+        verify(mUserOneService, never()).hasBackupPassword();
+    }
+
+    /**
+     * Test that the backup services throws a {@link SecurityException} if the caller does not have
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testAdbBackup_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.adbBackup(
+                                mUserTwoId,
+                                /* parcelFileDescriptor*/ null,
+                                /* includeApks */ true,
+                                /* includeObbs */ true,
+                                /* includeShared */ true,
+                                /* doWidgets */ true,
+                                /* doAllApps */ true,
+                                /* includeSystem */ true,
+                                /* doCompress */ true,
+                                /* doKeyValue */ true,
+                                null));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testAdbBackup_withPermission_propagatesForNonCallingUser() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.adbBackup(
+                mUserTwoId,
+                parcelFileDescriptor,
+                /* includeApks */ true,
+                /* includeObbs */ true,
+                /* includeShared */ true,
+                /* doWidgets */ true,
+                /* doAllApps */ true,
+                /* includeSystem */ true,
+                /* doCompress */ true,
+                /* doKeyValue */ true,
+                ADB_TEST_PACKAGES);
+
+        verify(mUserTwoService)
+                .adbBackup(
+                        parcelFileDescriptor,
+                        /* includeApks */ true,
+                        /* includeObbs */ true,
+                        /* includeShared */ true,
+                        /* doWidgets */ true,
+                        /* doAllApps */ true,
+                        /* includeSystem */ true,
+                        /* doCompress */ true,
+                        /* doKeyValue */ true,
+                        ADB_TEST_PACKAGES);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testAdbBackup_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.adbBackup(
+                mUserOneId,
+                parcelFileDescriptor,
+                /* includeApks */ true,
+                /* includeObbs */ true,
+                /* includeShared */ true,
+                /* doWidgets */ true,
+                /* doAllApps */ true,
+                /* includeSystem */ true,
+                /* doCompress */ true,
+                /* doKeyValue */ true,
+                ADB_TEST_PACKAGES);
+
+        verify(mUserOneService)
+                .adbBackup(
+                        parcelFileDescriptor,
+                        /* includeApks */ true,
+                        /* includeObbs */ true,
+                        /* includeShared */ true,
+                        /* doWidgets */ true,
+                        /* doAllApps */ true,
+                        /* includeSystem */ true,
+                        /* doCompress */ true,
+                        /* doKeyValue */ true,
+                        ADB_TEST_PACKAGES);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testAdbBackup_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.adbBackup(
+                mUserTwoId,
+                parcelFileDescriptor,
+                /* includeApks */ true,
+                /* includeObbs */ true,
+                /* includeShared */ true,
+                /* doWidgets */ true,
+                /* doAllApps */ true,
+                /* includeSystem */ true,
+                /* doCompress */ true,
+                /* doKeyValue */ true,
+                ADB_TEST_PACKAGES);
+
+        verify(mUserOneService, never())
+                .adbBackup(
+                        parcelFileDescriptor,
+                        /* includeApks */ true,
+                        /* includeObbs */ true,
+                        /* includeShared */ true,
+                        /* doWidgets */ true,
+                        /* doAllApps */ true,
+                        /* includeSystem */ true,
+                        /* doCompress */ true,
+                        /* doKeyValue */ true,
+                        ADB_TEST_PACKAGES);
+    }
+
+    /**
+     * Test that the backup services throws a {@link SecurityException} if the caller does not have
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testAdbRestore_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        expectThrows(
+                SecurityException.class, () -> backupManagerService.adbRestore(mUserTwoId, null));
+    }
+
+    /**
+     * Test that the backup service does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testAdbRestore_withPermission_propagatesForNonCallingUser() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        registerUser(backupManagerService, mUserTwoId, mUserTwoService);
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ true);
+
+        backupManagerService.adbRestore(mUserTwoId, parcelFileDescriptor);
+
+        verify(mUserTwoService).adbRestore(parcelFileDescriptor);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testAdbRestore_onRegisteredUser_callsMethodForUser() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        backupManagerService.adbRestore(mUserOneId, parcelFileDescriptor);
+
+        verify(mUserOneService).adbRestore(parcelFileDescriptor);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testAdbRestore_onUnknownUser_doesNotPropagateCall() throws Exception {
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        ParcelFileDescriptor parcelFileDescriptor = getFileDescriptorForAdbTest();
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        backupManagerService.adbRestore(mUserTwoId, parcelFileDescriptor);
+
+        verify(mUserOneService, never()).adbRestore(parcelFileDescriptor);
+    }
+
+    private ParcelFileDescriptor getFileDescriptorForAdbTest() throws Exception {
+        File testFile = new File(mContext.getFilesDir(), "test");
+        testFile.createNewFile();
+        return ParcelFileDescriptor.open(testFile, ParcelFileDescriptor.MODE_READ_WRITE);
+    }
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testAcknowledgeAdbBackupOrRestore_onRegisteredUser_callsMethodForUser()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+        IFullBackupRestoreObserver observer = mock(IFullBackupRestoreObserver.class);
+
+        backupManagerService.acknowledgeAdbBackupOrRestore(
+                mUserOneId,
+                /* token */ 0,
+                /* allow */ true,
+                "currentPassword",
+                "encryptionPassword",
+                observer);
+
+        verify(mUserOneService)
+                .acknowledgeAdbBackupOrRestore(
+                        /* token */ 0,
+                        /* allow */ true,
+                        "currentPassword",
+                        "encryptionPassword",
+                        observer);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testAcknowledgeAdbBackupOrRestore_onUnknownUser_doesNotPropagateCall()
+            throws Exception {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+        IFullBackupRestoreObserver observer = mock(IFullBackupRestoreObserver.class);
+
+        backupManagerService.acknowledgeAdbBackupOrRestore(
+                mUserTwoId,
+                /* token */ 0,
+                /* allow */ true,
+                "currentPassword",
+                "encryptionPassword",
+                observer);
+
+        verify(mUserOneService, never())
+                .acknowledgeAdbBackupOrRestore(
+                        /* token */ 0,
+                        /* allow */ true,
+                        "currentPassword",
+                        "encryptionPassword",
+                        observer);
+    }
+
+    // ---------------------------------------------
+    //  Service tests
+    // ---------------------------------------------
+
+    /** Test that the backup service routes methods correctly to the user that requests it. */
+    @Test
+    public void testDump_onRegisteredUser_callsMethodForUser() throws Exception {
+        grantDumpPermissions();
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        File testFile = createTestFile();
+        FileDescriptor fileDescriptor = new FileDescriptor();
+        PrintWriter printWriter = new PrintWriter(testFile);
+        String[] args = {"1", "2"};
+        ShadowBinder.setCallingUserHandle(UserHandle.of(UserHandle.USER_SYSTEM));
+
+        backupManagerService.dump(fileDescriptor, printWriter, args);
+
+        verify(mUserSystemService).dump(fileDescriptor, printWriter, args);
+    }
+
+    /** Test that the backup service does not route methods for non-registered users. */
+    @Test
+    public void testDump_onUnknownUser_doesNotPropagateCall() throws Exception {
+        grantDumpPermissions();
+        BackupManagerService backupManagerService = createService();
+        File testFile = createTestFile();
+        FileDescriptor fileDescriptor = new FileDescriptor();
+        PrintWriter printWriter = new PrintWriter(testFile);
+        String[] args = {"1", "2"};
+
+        backupManagerService.dump(fileDescriptor, printWriter, args);
+
+        verify(mUserOneService, never()).dump(fileDescriptor, printWriter, args);
+    }
+
+    /** Test that 'dumpsys backup users' dumps the list of users registered in backup service*/
+    @Test
+    public void testDump_users_dumpsListOfRegisteredUsers() {
+        grantDumpPermissions();
+        BackupManagerService backupManagerService = createSystemRegisteredService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        StringWriter out = new StringWriter();
+        PrintWriter writer = new PrintWriter(out);
+        String[] args = {"users"};
+
+        backupManagerService.dump(null, writer, args);
+
+        writer.flush();
+        assertEquals(
+                String.format("%s %d %d\n", BackupManagerService.DUMP_RUNNING_USERS_MESSAGE,
+                        UserHandle.USER_SYSTEM, mUserOneId),
+                out.toString());
+    }
+
+    private File createTestFile() throws IOException {
+        File testFile = new File(mContext.getFilesDir(), "test");
+        testFile.createNewFile();
+        return testFile;
+    }
+
+    private void grantDumpPermissions() {
+        mShadowContext.grantPermissions(DUMP);
+        mShadowContext.grantPermissions(PACKAGE_USAGE_STATS);
+    }
+
+    /**
+     * Test that the backup services throws a {@link SecurityException} if the caller does not have
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testGetServiceForUser_withoutPermission_throwsSecurityExceptionForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.getServiceForUserIfCallerHasPermission(
+                                mUserOneId, "test"));
+    }
+
+    /**
+     * Test that the backup services does not throw a {@link SecurityException} if the caller has
+     * INTERACT_ACROSS_USERS_FULL permission and passes a different user id.
+     */
+    @Test
+    public void testGetServiceForUserIfCallerHasPermission_withPermission_worksForNonCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ true);
+
+        assertEquals(
+                mUserOneService,
+                backupManagerService.getServiceForUserIfCallerHasPermission(mUserOneId, "test"));
+    }
+
+    /**
+     * Test that the backup services does not throw a {@link SecurityException} if the caller does
+     * not have INTERACT_ACROSS_USERS_FULL permission and passes in the calling user id.
+     */
+    @Test
+    public void testGetServiceForUserIfCallerHasPermission_withoutPermission_worksForCallingUser() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, mUserOneId, mUserOneService);
+        setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
+
+        assertEquals(
+                mUserOneService,
+                backupManagerService.getServiceForUserIfCallerHasPermission(mUserOneId, "test"));
+    }
+
+    /**
+     * Test verifying that {@link BackupManagerService#MORE_DEBUG} is set to {@code false}. This is
+     * specifically to prevent overloading the logs in production.
+     */
+    @Test
+    public void testMoreDebug_isFalse() throws Exception {
+        boolean moreDebug = BackupManagerService.MORE_DEBUG;
+
+        assertThat(moreDebug).isFalse();
+    }
+
+    /** Test that the constructor handles {@code null} parameters. */
+    @Test
+    public void testConstructor_withNullContext_throws() throws Exception {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        new BackupManagerService(
+                                /* context */ null,
+                                new SparseArray<>()));
+    }
+
+    /** Test that the constructor does not create {@link UserBackupManagerService} instances. */
+    @Test
+    public void testConstructor_doesNotRegisterUsers() throws Exception {
+        BackupManagerService backupManagerService = createService();
+
+        assertThat(backupManagerService.getUserServices().size()).isEqualTo(0);
+    }
+
+    // ---------------------------------------------
+    //  Lifecycle tests
+    // ---------------------------------------------
+
+    /** testOnStart_publishesService */
+    @Test
+    public void testOnStart_publishesService() {
+        BackupManagerService backupManagerService = mock(BackupManagerService.class);
+        BackupManagerService.Lifecycle lifecycle =
+                spy(new BackupManagerService.Lifecycle(mContext, backupManagerService));
+        doNothing().when(lifecycle).publishService(anyString(), any());
+
+        lifecycle.onStart();
+
+        verify(lifecycle).publishService(Context.BACKUP_SERVICE, backupManagerService);
+    }
+
+    /** testOnUnlockUser_forwards */
+    @Test
+    public void testOnUnlockUser_forwards() {
+        BackupManagerService backupManagerService = mock(BackupManagerService.class);
+        BackupManagerService.Lifecycle lifecycle =
+                new BackupManagerService.Lifecycle(mContext, backupManagerService);
+
+        lifecycle.onUnlockUser(UserHandle.USER_SYSTEM);
+
+        verify(backupManagerService).onUnlockUser(UserHandle.USER_SYSTEM);
+    }
+
+    /** testOnStopUser_forwards */
+    @Test
+    public void testOnStopUser_forwards() {
+        BackupManagerService backupManagerService = mock(BackupManagerService.class);
+        BackupManagerService.Lifecycle lifecycle =
+                new BackupManagerService.Lifecycle(mContext, backupManagerService);
+
+        lifecycle.onStopUser(UserHandle.USER_SYSTEM);
+
+        verify(backupManagerService).onStopUser(UserHandle.USER_SYSTEM);
+    }
+
+    private BackupManagerService createService() {
+        return new BackupManagerService(mContext);
+    }
+
+    private BackupManagerService createSystemRegisteredService() {
+        BackupManagerService backupManagerService = createService();
+        registerUser(backupManagerService, UserHandle.USER_SYSTEM, mUserSystemService);
+        return backupManagerService;
+    }
+
+    private void registerUser(
+            BackupManagerService backupManagerService,
+            int userId,
+            UserBackupManagerService userBackupManagerService) {
+        backupManagerService.setBackupServiceActive(userId, true);
+        backupManagerService.startServiceForUser(userId, userBackupManagerService);
+    }
+
+    private BackupManagerService createServiceAndRegisterUser(
+            int userId, UserBackupManagerService userBackupManagerService) {
+        BackupManagerService backupManagerService = createService();
+        backupManagerService.setBackupServiceActive(userBackupManagerService.getUserId(), true);
+        backupManagerService.startServiceForUser(userId, userBackupManagerService);
+        return backupManagerService;
+    }
+
+    /**
+     * Sets the calling user to {@code userId} and grants the permission INTERACT_ACROSS_USERS_FULL
+     * to the caller if {@code shouldGrantPermission} is {@code true}, else it denies the
+     * permission.
+     */
+    private void setCallerAndGrantInteractUserPermission(
+            @UserIdInt int userId, boolean shouldGrantPermission) {
+        ShadowBinder.setCallingUserHandle(UserHandle.of(userId));
+        if (shouldGrantPermission) {
+            mShadowContext.grantPermissions(INTERACT_ACROSS_USERS_FULL);
+        } else {
+            mShadowContext.denyPermissions(INTERACT_ACROSS_USERS_FULL);
+        }
+    }
+}
diff --git a/com/android/server/backup/BackupPasswordManager.java b/com/android/server/backup/BackupPasswordManager.java
new file mode 100644
index 0000000..ee7651b
--- /dev/null
+++ b/com/android/server/backup/BackupPasswordManager.java
@@ -0,0 +1,307 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.utils.DataStreamFileCodec;
+import com.android.server.backup.utils.DataStreamCodec;
+import com.android.server.backup.utils.PasswordUtils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.security.SecureRandom;
+
+/**
+ * Manages persisting and verifying backup passwords.
+ *
+ * <p>Does not persist the password itself, but persists a PBKDF2 hash with a randomly chosen (also
+ * persisted) salt. Validation is performed by running the challenge text through the same
+ * PBKDF2 cycle with the persisted salt, and checking the hashes match.
+ *
+ * @see PasswordUtils for the hashing algorithm.
+ */
+public final class BackupPasswordManager {
+    private static final String TAG = "BackupPasswordManager";
+    private static final boolean DEBUG = false;
+
+    private static final int BACKUP_PW_FILE_VERSION = 2;
+    private static final int DEFAULT_PW_FILE_VERSION = 1;
+
+    private static final String PASSWORD_VERSION_FILE_NAME = "pwversion";
+    private static final String PASSWORD_HASH_FILE_NAME = "pwhash";
+
+    // See https://android-developers.googleblog.com/2013/12/changes-to-secretkeyfactory-api-in.html
+    public static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
+    public static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
+
+    private final SecureRandom mRng;
+    private final Context mContext;
+    private final File mBaseStateDir;
+
+    private String mPasswordHash;
+    private int mPasswordVersion;
+    private byte[] mPasswordSalt;
+
+    /**
+     * Creates an instance enforcing permissions using the {@code context} and persisting password
+     * data within the {@code baseStateDir}.
+     *
+     * @param context The context, for enforcing permissions around setting the password.
+     * @param baseStateDir A directory within which to persist password data.
+     * @param secureRandom Random number generator with which to generate password salts.
+     */
+    BackupPasswordManager(Context context, File baseStateDir, SecureRandom secureRandom) {
+        mContext = context;
+        mRng = secureRandom;
+        mBaseStateDir = baseStateDir;
+        loadStateFromFilesystem();
+    }
+
+    /**
+     * Returns {@code true} if a password for backup is set.
+     *
+     * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+     *   permission.
+     */
+    boolean hasBackupPassword() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "hasBackupPassword");
+        return mPasswordHash != null && mPasswordHash.length() > 0;
+    }
+
+    /**
+     * Returns {@code true} if {@code password} matches the persisted password.
+     *
+     * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+     *   permission.
+     */
+    boolean backupPasswordMatches(String password) {
+        if (hasBackupPassword() && !passwordMatchesSaved(password)) {
+            if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sets the new password, given a correct current password.
+     *
+     * @throws SecurityException If caller does not have {@link android.Manifest.permission#BACKUP}
+     *   permission.
+     * @return {@code true} if has permission to set the password, {@code currentPassword}
+     *   matches the currently persisted password, and is able to persist {@code newPassword}.
+     */
+    boolean setBackupPassword(String currentPassword, String newPassword) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "setBackupPassword");
+
+        if (!passwordMatchesSaved(currentPassword)) {
+            return false;
+        }
+
+        // Snap up to latest password file version.
+        try {
+            getPasswordVersionFileCodec().serialize(BACKUP_PW_FILE_VERSION);
+            mPasswordVersion = BACKUP_PW_FILE_VERSION;
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to write backup pw version; password not changed");
+            return false;
+        }
+
+        if (newPassword == null || newPassword.isEmpty()) {
+            return clearPassword();
+        }
+
+        try {
+            byte[] salt = randomSalt();
+            String newPwHash = PasswordUtils.buildPasswordHash(
+                    PBKDF_CURRENT, newPassword, salt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+
+            getPasswordHashFileCodec().serialize(new BackupPasswordHash(newPwHash, salt));
+            mPasswordHash = newPwHash;
+            mPasswordSalt = salt;
+            return true;
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to set backup password");
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if should try salting using the older PBKDF algorithm.
+     *
+     * <p>This is {@code true} for v1 files.
+     */
+    private boolean usePbkdf2Fallback() {
+        return mPasswordVersion < BACKUP_PW_FILE_VERSION;
+    }
+
+    /**
+     * Deletes the current backup password.
+     *
+     * @return {@code true} if successful.
+     */
+    private boolean clearPassword() {
+        File passwordHashFile = getPasswordHashFile();
+        if (passwordHashFile.exists() && !passwordHashFile.delete()) {
+            Slog.e(TAG, "Unable to clear backup password");
+            return false;
+        }
+
+        mPasswordHash = null;
+        mPasswordSalt = null;
+        return true;
+    }
+
+    /**
+     * Sets the password hash, salt, and version in the object from what has been persisted to the
+     * filesystem.
+     */
+    private void loadStateFromFilesystem() {
+        try {
+            mPasswordVersion = getPasswordVersionFileCodec().deserialize();
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to read backup pw version");
+            mPasswordVersion = DEFAULT_PW_FILE_VERSION;
+        }
+
+        try {
+            BackupPasswordHash hash = getPasswordHashFileCodec().deserialize();
+            mPasswordHash = hash.hash;
+            mPasswordSalt = hash.salt;
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to read saved backup pw hash");
+        }
+    }
+
+    /**
+     * Whether the candidate password matches the current password. If the persisted password is an
+     * older version, attempts hashing using the older algorithm.
+     *
+     * @param candidatePassword The password to try.
+     * @return {@code true} if the passwords match.
+     */
+    private boolean passwordMatchesSaved(String candidatePassword) {
+        return passwordMatchesSaved(PBKDF_CURRENT, candidatePassword)
+                || (usePbkdf2Fallback() && passwordMatchesSaved(PBKDF_FALLBACK, candidatePassword));
+    }
+
+    /**
+     * Returns {@code true} if the candidate password is correct.
+     *
+     * @param algorithm The algorithm used to hash passwords.
+     * @param candidatePassword The candidate password to compare to the current password.
+     * @return {@code true} if the candidate password matched the saved password.
+     */
+    private boolean passwordMatchesSaved(String algorithm, String candidatePassword) {
+        if (mPasswordHash == null) {
+            return candidatePassword == null || candidatePassword.equals("");
+        } else if (candidatePassword == null || candidatePassword.length() == 0) {
+            // The current password is not zero-length, but the candidate password is.
+            return false;
+        } else {
+            String candidatePasswordHash = PasswordUtils.buildPasswordHash(
+                    algorithm, candidatePassword, mPasswordSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+            return mPasswordHash.equalsIgnoreCase(candidatePasswordHash);
+        }
+    }
+
+    private byte[] randomSalt() {
+        int bitsPerByte = 8;
+        byte[] array = new byte[PasswordUtils.PBKDF2_SALT_SIZE / bitsPerByte];
+        mRng.nextBytes(array);
+        return array;
+    }
+
+    private DataStreamFileCodec<Integer> getPasswordVersionFileCodec() {
+        return new DataStreamFileCodec<>(
+                new File(mBaseStateDir, PASSWORD_VERSION_FILE_NAME),
+                new PasswordVersionFileCodec());
+    }
+
+    private DataStreamFileCodec<BackupPasswordHash> getPasswordHashFileCodec() {
+        return new DataStreamFileCodec<>(getPasswordHashFile(), new PasswordHashFileCodec());
+    }
+
+    private File getPasswordHashFile() {
+        return new File(mBaseStateDir, PASSWORD_HASH_FILE_NAME);
+    }
+
+    /**
+     * Container class for a PBKDF hash and the salt used to create the hash.
+     */
+    private static final class BackupPasswordHash {
+        public String hash;
+        public byte[] salt;
+
+        BackupPasswordHash(String hash, byte[] salt) {
+            this.hash = hash;
+            this.salt = salt;
+        }
+    }
+
+    /**
+     * The password version file contains a single 32-bit integer.
+     */
+    private static final class PasswordVersionFileCodec implements
+            DataStreamCodec<Integer> {
+        @Override
+        public void serialize(Integer integer, DataOutputStream dataOutputStream)
+                throws IOException {
+            dataOutputStream.write(integer);
+        }
+
+        @Override
+        public Integer deserialize(DataInputStream dataInputStream) throws IOException {
+            return dataInputStream.readInt();
+        }
+    }
+
+    /**
+     * The passwords hash file contains
+     *
+     * <ul>
+     *     <li>A 32-bit integer representing the number of bytes in the salt;
+     *     <li>The salt bytes;
+     *     <li>A UTF-8 string of the hash.
+     * </ul>
+     */
+    private static final class PasswordHashFileCodec implements
+            DataStreamCodec<BackupPasswordHash> {
+        @Override
+        public void serialize(BackupPasswordHash backupPasswordHash,
+                DataOutputStream dataOutputStream) throws IOException {
+            dataOutputStream.writeInt(backupPasswordHash.salt.length);
+            dataOutputStream.write(backupPasswordHash.salt);
+            dataOutputStream.writeUTF(backupPasswordHash.hash);
+        }
+
+        @Override
+        public BackupPasswordHash deserialize(
+                DataInputStream dataInputStream) throws IOException {
+            int saltLen = dataInputStream.readInt();
+            byte[] salt = new byte[saltLen];
+            dataInputStream.readFully(salt);
+            String hash = dataInputStream.readUTF();
+            return new BackupPasswordHash(hash, salt);
+        }
+    }
+}
diff --git a/com/android/server/backup/BackupRestoreTask.java b/com/android/server/backup/BackupRestoreTask.java
new file mode 100644
index 0000000..acaab0c
--- /dev/null
+++ b/com/android/server/backup/BackupRestoreTask.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Interface and methods used by the asynchronous-with-timeout backup/restore operations.
+ */
+public interface BackupRestoreTask {
+
+    // Execute one tick of whatever state machine the task implements
+    void execute();
+
+    // An operation that wanted a callback has completed
+    void operationComplete(long result);
+
+    // An operation that wanted a callback has timed out
+    void handleCancel(boolean cancelAll);
+}
diff --git a/com/android/server/backup/BackupUtils.java b/com/android/server/backup/BackupUtils.java
new file mode 100644
index 0000000..96c5621
--- /dev/null
+++ b/com/android/server/backup/BackupUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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 android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.util.Slog;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class BackupUtils {
+    private static final String TAG = "BackupUtils";
+
+    private static final boolean DEBUG = false;
+
+    public static boolean signaturesMatch(ArrayList<byte[]> storedSigHashes, PackageInfo target,
+            PackageManagerInternal pmi) {
+        if (target == null || target.packageName == null) {
+            return false;
+        }
+        // If the target resides on the system partition, we allow it to restore
+        // data from the like-named package in a restore set even if the signatures
+        // do not match.  (Unlike general applications, those flashed to the system
+        // partition will be signed with the device's platform certificate, so on
+        // different phones the same system app will have different signatures.)
+        if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            if (DEBUG) Slog.v(TAG, "System app " + target.packageName + " - skipping sig check");
+            return true;
+        }
+
+        // Don't allow unsigned apps on either end
+        if (ArrayUtils.isEmpty(storedSigHashes)) {
+            return false;
+        }
+
+        SigningInfo signingInfo = target.signingInfo;
+        if (signingInfo == null) {
+            Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
+                    " PackageManager#GET_SIGNING_CERTIFICATES was not specified");
+            return false;
+        }
+
+        if (DEBUG) {
+            Slog.v(TAG, "signaturesMatch(): stored=" + storedSigHashes
+                    + " device=" + signingInfo.getApkContentsSigners());
+        }
+
+        final int nStored = storedSigHashes.size();
+        if (nStored == 1) {
+            // if the app is only signed with one sig, it's possible it has rotated its key
+            // the checks with signing history are delegated to PackageManager
+            // TODO(b/73988180): address the case that app has declared restoreAnyVersion and is
+            // restoring from higher version to lower after having rotated the key (i.e. higher
+            // version has different sig than lower version that we want to restore to)
+            return pmi.isDataRestoreSafe(storedSigHashes.get(0), target.packageName);
+        } else {
+            // the app couldn't have rotated keys, since it was signed with multiple sigs - do
+            // a check to see if we find a match for all stored sigs
+            // since app hasn't rotated key, we only need to check with current signers
+            ArrayList<byte[]> deviceHashes =
+                    hashSignatureArray(signingInfo.getApkContentsSigners());
+            int nDevice = deviceHashes.size();
+            // ensure that each stored sig matches an on-device sig
+            for (int i = 0; i < nStored; i++) {
+                boolean match = false;
+                for (int j = 0; j < nDevice; j++) {
+                    if (Arrays.equals(storedSigHashes.get(i), deviceHashes.get(j))) {
+                        match = true;
+                        break;
+                    }
+                }
+                if (!match) {
+                    return false;
+                }
+            }
+            // we have found a match for all stored sigs
+            return true;
+        }
+    }
+
+    public static byte[] hashSignature(byte[] signature) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            digest.update(signature);
+            return digest.digest();
+        } catch (NoSuchAlgorithmException e) {
+            Slog.w(TAG, "No SHA-256 algorithm found!");
+        }
+        return null;
+    }
+
+    public static byte[] hashSignature(Signature signature) {
+        return hashSignature(signature.toByteArray());
+    }
+
+    public static ArrayList<byte[]> hashSignatureArray(Signature[] sigs) {
+        if (sigs == null) {
+            return null;
+        }
+
+        ArrayList<byte[]> hashes = new ArrayList<>(sigs.length);
+        for (Signature s : sigs) {
+            hashes.add(hashSignature(s));
+        }
+        return hashes;
+    }
+
+    public static ArrayList<byte[]> hashSignatureArray(List<byte[]> sigs) {
+        if (sigs == null) {
+            return null;
+        }
+
+        ArrayList<byte[]> hashes = new ArrayList<>(sigs.size());
+        for (byte[] s : sigs) {
+            hashes.add(hashSignature(s));
+        }
+        return hashes;
+    }
+}
diff --git a/com/android/server/backup/DataChangedJournal.java b/com/android/server/backup/DataChangedJournal.java
new file mode 100644
index 0000000..e75eb73
--- /dev/null
+++ b/com/android/server/backup/DataChangedJournal.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A journal of packages that have indicated that their data has changed (and therefore should be
+ * backed up in the next scheduled K/V backup pass).
+ *
+ * <p>This information is persisted to the filesystem so that it is not lost in the event of a
+ * reboot.
+ */
+public class DataChangedJournal {
+    private static final String TAG = "DataChangedJournal";
+    private static final String FILE_NAME_PREFIX = "journal";
+
+    /**
+     * Journals tend to be on the order of a few kilobytes, hence setting the buffer size to 8kb.
+     */
+    private static final int BUFFER_SIZE_BYTES = 8 * 1024;
+
+    private final File mFile;
+
+    /**
+     * Constructs an instance that reads from and writes to the given file.
+     */
+    DataChangedJournal(File file) {
+        mFile = file;
+    }
+
+    /**
+     * Adds the given package to the journal.
+     *
+     * @param packageName The name of the package whose data has changed.
+     * @throws IOException if there is an IO error writing to the journal file.
+     */
+    public void addPackage(String packageName) throws IOException {
+        try (RandomAccessFile out = new RandomAccessFile(mFile, "rws")) {
+            out.seek(out.length());
+            out.writeUTF(packageName);
+        }
+    }
+
+    /**
+     * Invokes {@link Consumer#accept(Object)} with every package name in the journal file.
+     *
+     * @param consumer The callback.
+     * @throws IOException If there is an IO error reading from the file.
+     */
+    public void forEach(Consumer<String> consumer) throws IOException {
+        try (
+            BufferedInputStream bufferedInputStream = new BufferedInputStream(
+                    new FileInputStream(mFile), BUFFER_SIZE_BYTES);
+            DataInputStream dataInputStream = new DataInputStream(bufferedInputStream)
+        ) {
+            while (dataInputStream.available() > 0) {
+                String packageName = dataInputStream.readUTF();
+                consumer.accept(packageName);
+            }
+        }
+    }
+
+    /**
+     * Returns a list with the packages in this journal.
+     *
+     * @throws IOException If there is an IO error reading from the file.
+     */
+    public List<String> getPackages() throws IOException {
+        List<String> packages = new ArrayList<>();
+        forEach(packages::add);
+        return packages;
+    }
+
+    /**
+     * Deletes the journal from the filesystem.
+     *
+     * @return {@code true} if successfully deleted journal.
+     */
+    public boolean delete() {
+        return mFile.delete();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object object) {
+        if (object instanceof DataChangedJournal) {
+            DataChangedJournal that = (DataChangedJournal) object;
+            try {
+                return this.mFile.getCanonicalPath().equals(that.mFile.getCanonicalPath());
+            } catch (IOException exception) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return mFile.toString();
+    }
+
+    /**
+     * Creates a new journal with a random file name in the given journal directory.
+     *
+     * @param journalDirectory The directory where journals are kept.
+     * @return The journal.
+     * @throws IOException if there is an IO error creating the file.
+     */
+    static DataChangedJournal newJournal(File journalDirectory) throws IOException {
+        return new DataChangedJournal(
+                File.createTempFile(FILE_NAME_PREFIX, null, journalDirectory));
+    }
+
+    /**
+     * Returns a list of journals in the given journal directory.
+     */
+    static ArrayList<DataChangedJournal> listJournals(File journalDirectory) {
+        ArrayList<DataChangedJournal> journals = new ArrayList<>();
+        File[] journalFiles = journalDirectory.listFiles();
+        if (journalFiles == null) {
+            Slog.w(TAG, "Failed to read journal files");
+            return journals;
+        }
+        for (File file : journalFiles) {
+            journals.add(new DataChangedJournal(file));
+        }
+        return journals;
+    }
+}
diff --git a/com/android/server/backup/FileMetadata.java b/com/android/server/backup/FileMetadata.java
new file mode 100644
index 0000000..fe75041
--- /dev/null
+++ b/com/android/server/backup/FileMetadata.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.app.backup.BackupAgent;
+import android.util.Slog;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Description of a file in the restore datastream.
+ */
+public class FileMetadata {
+    public String packageName;             // name of the owning app
+    public String installerPackageName;    // name of the market-type app that installed the owner
+    public int type;                       // e.g. BackupAgent.TYPE_DIRECTORY
+    public String domain;                  // e.g. FullBackup.DATABASE_TREE_TOKEN
+    public String path;                    // subpath within the semantic domain
+    public long mode;                      // e.g. 0666 (actually int)
+    public long mtime;                     // last mod time, UTC time_t (actually int)
+    public long size;                      // bytes of content
+    public long version;                   // App version.
+    public boolean hasApk;                 // Whether backup file contains apk.
+
+    @Override
+    public String toString() {
+        // TODO: Clean this up.
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("FileMetadata{");
+        sb.append(packageName);
+        sb.append(',');
+        sb.append(type);
+        sb.append(',');
+        sb.append(domain);
+        sb.append(':');
+        sb.append(path);
+        sb.append(',');
+        sb.append(size);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public void dump() {
+        StringBuilder b = new StringBuilder(128);
+
+        // mode string
+        b.append((type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-');
+        b.append(((mode & 0400) != 0) ? 'r' : '-');
+        b.append(((mode & 0200) != 0) ? 'w' : '-');
+        b.append(((mode & 0100) != 0) ? 'x' : '-');
+        b.append(((mode & 0040) != 0) ? 'r' : '-');
+        b.append(((mode & 0020) != 0) ? 'w' : '-');
+        b.append(((mode & 0010) != 0) ? 'x' : '-');
+        b.append(((mode & 0004) != 0) ? 'r' : '-');
+        b.append(((mode & 0002) != 0) ? 'w' : '-');
+        b.append(((mode & 0001) != 0) ? 'x' : '-');
+        b.append(String.format(" %9d ", size));
+
+        Date stamp = new Date(mtime);
+        b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp));
+
+        b.append(packageName);
+        b.append(" :: ");
+        b.append(domain);
+        b.append(" :: ");
+        b.append(path);
+
+        Slog.i(TAG, b.toString());
+    }
+
+}
diff --git a/com/android/server/backup/FullBackupJob.java b/com/android/server/backup/FullBackupJob.java
new file mode 100644
index 0000000..0bb25e3
--- /dev/null
+++ b/com/android/server/backup/FullBackupJob.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014 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 com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+public class FullBackupJob extends JobService {
+    private static final String USER_ID_EXTRA_KEY = "userId";
+
+    @VisibleForTesting
+    public static final int MIN_JOB_ID = 52418896;
+    @VisibleForTesting
+    public static final int MAX_JOB_ID = 52419896;
+
+    private static ComponentName sIdleService =
+            new ComponentName(PLATFORM_PACKAGE_NAME, FullBackupJob.class.getName());
+
+    @GuardedBy("mParamsForUser")
+    private final SparseArray<JobParameters> mParamsForUser = new SparseArray<>();
+
+    public static void schedule(int userId, Context ctx, long minDelay,
+            BackupManagerConstants constants) {
+        JobScheduler js = (JobScheduler) ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        JobInfo.Builder builder = new JobInfo.Builder(getJobIdForUserId(userId), sIdleService);
+        synchronized (constants) {
+            builder.setRequiresDeviceIdle(true)
+                    .setRequiredNetworkType(constants.getFullBackupRequiredNetworkType())
+                    .setRequiresCharging(constants.getFullBackupRequireCharging());
+        }
+        if (minDelay > 0) {
+            builder.setMinimumLatency(minDelay);
+        }
+
+        Bundle extraInfo = new Bundle();
+        extraInfo.putInt(USER_ID_EXTRA_KEY, userId);
+        builder.setTransientExtras(extraInfo);
+
+        js.schedule(builder.build());
+    }
+
+    public static void cancel(int userId, Context ctx) {
+        JobScheduler js = (JobScheduler) ctx.getSystemService(
+                Context.JOB_SCHEDULER_SERVICE);
+        js.cancel(getJobIdForUserId(userId));
+    }
+
+    // callback from the Backup Manager Service: it's finished its work for this pass
+    public void finishBackupPass(int userId) {
+        synchronized (mParamsForUser) {
+            JobParameters jobParameters = mParamsForUser.get(userId);
+            if (jobParameters != null) {
+                jobFinished(jobParameters, false);
+                mParamsForUser.remove(userId);
+            }
+        }
+    }
+
+    // ----- scheduled job interface -----
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        int userId = params.getTransientExtras().getInt(USER_ID_EXTRA_KEY);
+
+        synchronized (mParamsForUser) {
+            mParamsForUser.put(userId, params);
+        }
+
+        BackupManagerService service = BackupManagerService.getInstance();
+        return service.beginFullBackup(userId, this);
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        int userId = params.getTransientExtras().getInt(USER_ID_EXTRA_KEY);
+
+        synchronized (mParamsForUser) {
+            if (mParamsForUser.removeReturnOld(userId) == null) {
+                return false;
+            }
+        }
+
+        BackupManagerService service = BackupManagerService.getInstance();
+        service.endFullBackup(userId);
+
+        return false;
+    }
+
+    private static int getJobIdForUserId(int userId) {
+        return JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, userId);
+    }
+}
diff --git a/com/android/server/backup/FullBackupJobTest.java b/com/android/server/backup/FullBackupJobTest.java
new file mode 100644
index 0000000..dbc0da7
--- /dev/null
+++ b/com/android/server/backup/FullBackupJobTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2019 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 com.google.common.truth.Truth.assertThat;
+
+import android.annotation.UserIdInt;
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowJobScheduler;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowJobScheduler.class, ShadowSystemServiceRegistry.class})
+@Presubmit
+public class FullBackupJobTest {
+    private Context mContext;
+    private BackupManagerConstants mConstants;
+    private ShadowJobScheduler mShadowJobScheduler;
+
+    @UserIdInt private int mUserOneId;
+    @UserIdInt private int mUserTwoId;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = RuntimeEnvironment.application;
+        mConstants = new BackupManagerConstants(Handler.getMain(), mContext.getContentResolver());
+        mConstants.start();
+
+        mShadowJobScheduler = Shadows.shadowOf(mContext.getSystemService(JobScheduler.class));
+
+        mUserOneId = UserHandle.USER_SYSTEM;
+        mUserTwoId = mUserOneId + 1;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mConstants.stop();
+        FullBackupJob.cancel(mUserOneId, mContext);
+        FullBackupJob.cancel(mUserTwoId, mContext);
+    }
+
+    @Test
+    public void testSchedule_afterScheduling_jobExists() {
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNotNull();
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNotNull();
+    }
+
+    @Test
+    public void testCancel_afterCancelling_jobDoesntExist() {
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+        FullBackupJob.cancel(mUserOneId, mContext);
+        FullBackupJob.cancel(mUserTwoId, mContext);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNull();
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNull();
+    }
+//
+    @Test
+    public void testSchedule_onlySchedulesForRequestedUser() {
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNotNull();
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNull();
+    }
+//
+    @Test
+    public void testCancel_onlyCancelsForRequestedUser() {
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+        FullBackupJob.cancel(mUserOneId, mContext);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNull();
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNotNull();
+    }
+
+    private static int getJobIdForUserId(int userId) {
+        return JobIdManager.getJobIdForUserId(FullBackupJob.MIN_JOB_ID, FullBackupJob.MAX_JOB_ID,
+                userId);
+    }
+}
diff --git a/com/android/server/backup/JobIdManager.java b/com/android/server/backup/JobIdManager.java
new file mode 100644
index 0000000..2e834db
--- /dev/null
+++ b/com/android/server/backup/JobIdManager.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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;
+
+/**
+ * Allocates job IDs for {@link FullBackupJob} and {@link KeyValueBackupJob}
+ */
+public class JobIdManager {
+    public static int getJobIdForUserId(int minJobId, int maxJobId, int userId) {
+        if (minJobId + userId > maxJobId) {
+            throw new RuntimeException("No job IDs available in the given range");
+        }
+
+        return minJobId + userId;
+    }
+}
diff --git a/com/android/server/backup/JobIdManagerTest.java b/com/android/server/backup/JobIdManagerTest.java
new file mode 100644
index 0000000..f8bb1ee
--- /dev/null
+++ b/com/android/server/backup/JobIdManagerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 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 com.google.common.truth.Truth.assertThat;
+
+import android.annotation.UserIdInt;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import static org.testng.Assert.expectThrows;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class JobIdManagerTest {
+    private static final int MIN_JOB_ID = 10;
+    private static final int MAX_JOB_ID = 20;
+
+    @UserIdInt private int mUserOneId;
+    @UserIdInt private int mUserTwoId;
+
+    @Before
+    public void setUp() {
+        mUserOneId = UserHandle.USER_SYSTEM;
+        mUserTwoId = mUserOneId + 1;
+    }
+
+    @Test
+    public void testGetJobIdForUserId_returnsDifferentJobIdsForDifferentUsers() {
+        int jobIdOne = JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, mUserOneId);
+        int jobIdTwo = JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, mUserTwoId);
+
+        assertThat(jobIdOne).isNotEqualTo(jobIdTwo);
+    }
+
+    @Test
+    public void testGetJobIdForUserId_returnsSameJobIdForSameUser() {
+        int jobIdOne = JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, mUserOneId);
+        int jobIdTwo = JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, mUserOneId);
+
+        assertThat(jobIdOne).isEqualTo(jobIdTwo);
+    }
+
+    @Test
+    public void testGetJobIdForUserId_throwsExceptionIfRangeIsExceeded() {
+        expectThrows(
+                RuntimeException.class,
+                () -> JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID,
+                        MAX_JOB_ID + 1));
+    }
+}
diff --git a/com/android/server/backup/KeyValueAdbBackupEngine.java b/com/android/server/backup/KeyValueAdbBackupEngine.java
new file mode 100644
index 0000000..c9b09e3
--- /dev/null
+++ b/com/android/server/backup/KeyValueAdbBackupEngine.java
@@ -0,0 +1,305 @@
+package com.android.server.backup;
+
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
+
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
+
+import android.app.ApplicationThreadConstants;
+import android.app.IBackupAgent;
+import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
+import android.app.backup.IBackupCallback;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.SELinux;
+import android.util.Slog;
+
+import com.android.server.backup.fullbackup.AppMetadataBackupWriter;
+import com.android.server.backup.remote.ServiceBackupCallback;
+import com.android.server.backup.utils.FullBackupUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Objects;
+
+/**
+ * Used by BackupManagerService to perform adb backup for key-value packages. At the moment this
+ * class resembles what is done in the standard key-value code paths in BackupManagerService, and
+ * should be unified later.
+ *
+ * TODO: We should create unified backup/restore engines that can be used for both transport and
+ * adb backup/restore, and for fullbackup and key-value backup.
+ */
+public class KeyValueAdbBackupEngine {
+    private static final String TAG = "KeyValueAdbBackupEngine";
+    private static final boolean DEBUG = false;
+
+    private static final String BACKUP_KEY_VALUE_DIRECTORY_NAME = "key_value_dir";
+    private static final String BACKUP_KEY_VALUE_BLANK_STATE_FILENAME = "blank_state";
+    private static final String BACKUP_KEY_VALUE_BACKUP_DATA_FILENAME_SUFFIX = ".data";
+    private static final String BACKUP_KEY_VALUE_NEW_STATE_FILENAME_SUFFIX = ".new";
+
+    private UserBackupManagerService mBackupManagerService;
+    private final PackageManager mPackageManager;
+    private final OutputStream mOutput;
+    private final PackageInfo mCurrentPackage;
+    private final File mDataDir;
+    private final File mStateDir;
+    private final File mBlankStateName;
+    private final File mBackupDataName;
+    private final File mNewStateName;
+    private final File mManifestFile;
+    private ParcelFileDescriptor mSavedState;
+    private ParcelFileDescriptor mBackupData;
+    private ParcelFileDescriptor mNewState;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    public KeyValueAdbBackupEngine(OutputStream output, PackageInfo packageInfo,
+            UserBackupManagerService backupManagerService, PackageManager packageManager,
+            File baseStateDir, File dataDir) {
+        mOutput = output;
+        mCurrentPackage = packageInfo;
+        mBackupManagerService = backupManagerService;
+        mPackageManager = packageManager;
+
+        mDataDir = dataDir;
+        mStateDir = new File(baseStateDir, BACKUP_KEY_VALUE_DIRECTORY_NAME);
+        mStateDir.mkdirs();
+
+        String pkg = mCurrentPackage.packageName;
+
+        mBlankStateName = new File(mStateDir, BACKUP_KEY_VALUE_BLANK_STATE_FILENAME);
+        mBackupDataName = new File(mDataDir,
+                pkg + BACKUP_KEY_VALUE_BACKUP_DATA_FILENAME_SUFFIX);
+        mNewStateName = new File(mStateDir,
+                pkg + BACKUP_KEY_VALUE_NEW_STATE_FILENAME_SUFFIX);
+
+        mManifestFile = new File(mDataDir, BACKUP_MANIFEST_FILENAME);
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+    }
+
+    public void backupOnePackage() throws IOException {
+        ApplicationInfo targetApp = mCurrentPackage.applicationInfo;
+
+        try {
+            prepareBackupFiles(mCurrentPackage.packageName);
+
+            IBackupAgent agent = bindToAgent(targetApp);
+
+            if (agent == null) {
+                // We failed binding to the agent, so ignore this package
+                Slog.e(TAG, "Failed binding to BackupAgent for package "
+                        + mCurrentPackage.packageName);
+                return;
+            }
+
+            // We are bound to agent, initiate backup.
+            if (!invokeAgentForAdbBackup(mCurrentPackage.packageName, agent)) {
+                // Backup failed, skip package.
+                Slog.e(TAG, "Backup Failed for package " + mCurrentPackage.packageName);
+                return;
+            }
+
+            // Backup finished successfully. Copy the backup data to the output stream.
+            writeBackupData();
+        } catch (FileNotFoundException e) {
+            Slog.e(TAG, "Failed creating files for package " + mCurrentPackage.packageName
+                    + " will ignore package. " + e);
+        } finally {
+            // We are either done, failed or have timed out, so do cleanup and kill the agent.
+            cleanup();
+        }
+    }
+
+    private void  prepareBackupFiles(String packageName) throws FileNotFoundException {
+
+        // We pass a blank state to make sure we are getting the complete backup, not just an
+        // increment
+        mSavedState = ParcelFileDescriptor.open(mBlankStateName,
+                MODE_READ_ONLY | MODE_CREATE);  // Make an empty file if necessary
+
+        mBackupData = ParcelFileDescriptor.open(mBackupDataName,
+                MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
+
+        if (!SELinux.restorecon(mBackupDataName)) {
+            Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName);
+        }
+
+        mNewState = ParcelFileDescriptor.open(mNewStateName,
+                MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
+    }
+
+    private IBackupAgent bindToAgent(ApplicationInfo targetApp) {
+        try {
+            return mBackupManagerService.bindToAgentSynchronous(targetApp,
+                    ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL);
+        } catch (SecurityException e) {
+            Slog.e(TAG, "error in binding to agent for package " + targetApp.packageName
+                    + ". " + e);
+            return null;
+        }
+    }
+
+    // Return true on backup success, false otherwise
+    private boolean invokeAgentForAdbBackup(String packageName, IBackupAgent agent) {
+        int token = mBackupManagerService.generateRandomIntegerToken();
+        long kvBackupAgentTimeoutMillis = mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis();
+        try {
+            mBackupManagerService.prepareOperationTimeout(token, kvBackupAgentTimeoutMillis, null,
+                    OP_TYPE_BACKUP_WAIT);
+
+            IBackupCallback callback =
+                    new ServiceBackupCallback(
+                            mBackupManagerService.getBackupManagerBinder(), token);
+            // Start backup and wait for BackupManagerService to get callback for success or timeout
+            agent.doBackup(
+                    mSavedState,
+                    mBackupData,
+                    mNewState,
+                    /* quotaBytes */ Long.MAX_VALUE,
+                    callback,
+                    /* transportFlags */ 0);
+            if (!mBackupManagerService.waitUntilOperationComplete(token)) {
+                Slog.e(TAG, "Key-value backup failed on package " + packageName);
+                return false;
+            }
+            if (DEBUG) {
+                Slog.i(TAG, "Key-value backup success for package " + packageName);
+            }
+            return true;
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error invoking agent for backup on " + packageName + ". " + e);
+            return false;
+        }
+    }
+
+    class KeyValueAdbBackupDataCopier implements Runnable {
+        private final PackageInfo mPackage;
+        private final ParcelFileDescriptor mPipe;
+        private final int mToken;
+
+        KeyValueAdbBackupDataCopier(PackageInfo pack, ParcelFileDescriptor pipe,
+                int token)
+                throws IOException {
+            mPackage = pack;
+            mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor());
+            mToken = token;
+        }
+
+        @Override
+        public void run() {
+            try {
+                FullBackupDataOutput output = new FullBackupDataOutput(mPipe);
+                AppMetadataBackupWriter writer =
+                        new AppMetadataBackupWriter(output, mPackageManager);
+
+                if (DEBUG) {
+                    Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
+                }
+
+                writer.backupManifest(
+                        mPackage,
+                        mManifestFile,
+                        mDataDir,
+                        FullBackup.KEY_VALUE_DATA_TOKEN,
+                        /* linkDomain */ null,
+                        /* withApk */ false);
+                mManifestFile.delete();
+
+                if (DEBUG) {
+                    Slog.d(TAG, "Writing key-value package payload" + mPackage.packageName);
+                }
+                FullBackup.backupToTar(mPackage.packageName, FullBackup.KEY_VALUE_DATA_TOKEN, null,
+                        mDataDir.getAbsolutePath(),
+                        mBackupDataName.getAbsolutePath(),
+                        output);
+
+                // Write EOD marker
+                try {
+                    FileOutputStream out = new FileOutputStream(mPipe.getFileDescriptor());
+                    byte[] buf = new byte[4];
+                    out.write(buf);
+                } catch (IOException e) {
+                    Slog.e(TAG, "Unable to finalize backup stream!");
+                }
+
+                try {
+                    mBackupManagerService.getBackupManagerBinder().opComplete(mToken, 0);
+                } catch (RemoteException e) {
+                    // we'll time out anyway, so we're safe
+                }
+
+            } catch (IOException e) {
+                Slog.e(TAG, "Error running full backup for " + mPackage.packageName + ". " + e);
+            } finally {
+                IoUtils.closeQuietly(mPipe);
+            }
+        }
+    }
+
+    private void writeBackupData() throws IOException {
+        int token = mBackupManagerService.generateRandomIntegerToken();
+        long kvBackupAgentTimeoutMillis = mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis();
+
+        ParcelFileDescriptor[] pipes = null;
+        try {
+            pipes = ParcelFileDescriptor.createPipe();
+
+            mBackupManagerService.prepareOperationTimeout(token, kvBackupAgentTimeoutMillis, null,
+                    OP_TYPE_BACKUP_WAIT);
+
+            // We will have to create a runnable that will read the manifest and backup data we
+            // created, such that we can pipe the data into mOutput. The reason we do this is that
+            // internally FullBackup.backupToTar is used, which will create the necessary file
+            // header, but will also chunk the data. The method routeSocketDataToOutput in
+            // BackupManagerService will dechunk the data, and append it to the TAR outputstream.
+            KeyValueAdbBackupDataCopier runner = new KeyValueAdbBackupDataCopier(mCurrentPackage, pipes[1],
+                    token);
+            pipes[1].close();   // the runner has dup'd it
+            pipes[1] = null;
+            Thread t = new Thread(runner, "key-value-app-data-runner");
+            t.start();
+
+            // Now pull data from the app and stuff it into the output
+            FullBackupUtils.routeSocketDataToOutput(pipes[0], mOutput);
+
+            if (!mBackupManagerService.waitUntilOperationComplete(token)) {
+                Slog.e(TAG, "Full backup failed on package " + mCurrentPackage.packageName);
+            } else {
+                if (DEBUG) {
+                    Slog.d(TAG, "Full package backup success: " + mCurrentPackage.packageName);
+                }
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Error backing up " + mCurrentPackage.packageName + ": " + e);
+        } finally {
+            // flush after every package
+            mOutput.flush();
+            if (pipes != null) {
+                IoUtils.closeQuietly(pipes[0]);
+                IoUtils.closeQuietly(pipes[1]);
+            }
+        }
+    }
+
+    private void cleanup() {
+        mBackupManagerService.tearDownAgentAndKill(mCurrentPackage.applicationInfo);
+        mBlankStateName.delete();
+        mNewStateName.delete();
+        mBackupDataName.delete();
+    }
+}
diff --git a/com/android/server/backup/KeyValueAdbRestoreEngine.java b/com/android/server/backup/KeyValueAdbRestoreEngine.java
new file mode 100644
index 0000000..3184bd8
--- /dev/null
+++ b/com/android/server/backup/KeyValueAdbRestoreEngine.java
@@ -0,0 +1,146 @@
+package com.android.server.backup;
+
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
+
+import android.app.IBackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackup;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Used by BackupManagerService to perform adb restore for key-value packages. At the moment this
+ * class resembles what is done in the standard key-value code paths in BackupManagerService, and
+ * should be unified later.
+ *
+ * TODO: We should create unified backup/restore engines that can be used for both transport and
+ * adb backup/restore, and for fullbackup and key-value backup.
+ */
+public class KeyValueAdbRestoreEngine implements Runnable {
+    private static final String TAG = "KeyValueAdbRestoreEngine";
+    private static final boolean DEBUG = false;
+
+    private final UserBackupManagerService mBackupManagerService;
+    private final File mDataDir;
+
+    private final FileMetadata mInfo;
+    private final ParcelFileDescriptor mInFD;
+    private final IBackupAgent mAgent;
+    private final int mToken;
+
+    public KeyValueAdbRestoreEngine(UserBackupManagerService backupManagerService,
+            File dataDir, FileMetadata info, ParcelFileDescriptor inFD, IBackupAgent agent,
+            int token) {
+        mBackupManagerService = backupManagerService;
+        mDataDir = dataDir;
+        mInfo = info;
+        mInFD = inFD;
+        mAgent = agent;
+        mToken = token;
+    }
+
+    @Override
+    public void run() {
+        try {
+            File restoreData = prepareRestoreData(mInfo, mInFD);
+
+            invokeAgentForAdbRestore(mAgent, mInfo, restoreData);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private File prepareRestoreData(FileMetadata info, ParcelFileDescriptor inFD) throws IOException {
+        String pkg = info.packageName;
+        File restoreDataName = new File(mDataDir, pkg + ".restore");
+        File sortedDataName = new File(mDataDir, pkg + ".sorted");
+
+        FullBackup.restoreFile(inFD, info.size, info.type, info.mode, info.mtime, restoreDataName);
+
+        // Sort the keys, as the BackupAgent expect them to come in lexicographical order
+        sortKeyValueData(restoreDataName, sortedDataName);
+        return sortedDataName;
+    }
+
+    private void invokeAgentForAdbRestore(IBackupAgent agent, FileMetadata info, File restoreData)
+            throws IOException {
+        String pkg = info.packageName;
+        File newStateName = new File(mDataDir, pkg + ".new");
+        try {
+            ParcelFileDescriptor backupData =
+                    ParcelFileDescriptor.open(restoreData, MODE_READ_ONLY);
+            ParcelFileDescriptor newState = ParcelFileDescriptor.open(newStateName,
+                    MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
+
+            if (DEBUG) {
+                Slog.i(TAG, "Starting restore of package " + pkg + " for version code "
+                        + info.version);
+            }
+            agent.doRestore(backupData, info.version, newState, mToken,
+                    mBackupManagerService.getBackupManagerBinder());
+        } catch (IOException e) {
+            Slog.e(TAG, "Exception opening file. " + e);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Exception calling doRestore on agent: " + e);
+        }
+    }
+
+    private void sortKeyValueData (File restoreData, File sortedData) throws IOException {
+        FileInputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        try {
+            inputStream = new FileInputStream(restoreData);
+            outputStream = new FileOutputStream(sortedData);
+            BackupDataInput reader = new BackupDataInput(inputStream.getFD());
+            BackupDataOutput writer = new BackupDataOutput(outputStream.getFD());
+            copyKeysInLexicalOrder(reader, writer);
+        } finally {
+            if (inputStream != null) {
+                IoUtils.closeQuietly(inputStream);
+            }
+            if (outputStream != null) {
+                IoUtils.closeQuietly(outputStream);
+            }
+        }
+    }
+
+    private void copyKeysInLexicalOrder(BackupDataInput in, BackupDataOutput out)
+            throws IOException {
+        Map<String, byte[]> data = new HashMap<>();
+        while (in.readNextHeader()) {
+            String key = in.getKey();
+            int size = in.getDataSize();
+            if (size < 0) {
+                in.skipEntityData();
+                continue;
+            }
+            byte[] value = new byte[size];
+            in.readEntityData(value, 0, size);
+            data.put(key, value);
+        }
+        List<String> keys = new ArrayList<>(data.keySet());
+        Collections.sort(keys);
+        for (String key : keys) {
+            byte[] value = data.get(key);
+            out.writeEntityHeader(key, value.length);
+            out.writeEntityData(value, value.length);
+        }
+    }
+}
diff --git a/com/android/server/backup/KeyValueBackupJob.java b/com/android/server/backup/KeyValueBackupJob.java
new file mode 100644
index 0000000..058dcae
--- /dev/null
+++ b/com/android/server/backup/KeyValueBackupJob.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 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 com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+import android.util.SparseLongArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Random;
+
+/**
+ * Job for scheduling key/value backup work.  This module encapsulates all
+ * of the policy around when those backup passes are executed.
+ */
+public class KeyValueBackupJob extends JobService {
+    private static final String TAG = "KeyValueBackupJob";
+    private static ComponentName sKeyValueJobService =
+            new ComponentName(PLATFORM_PACKAGE_NAME, KeyValueBackupJob.class.getName());
+
+    private static final String USER_ID_EXTRA_KEY = "userId";
+
+    // Once someone asks for a backup, this is how long we hold off until we find
+    // an on-charging opportunity.  If we hit this max latency we will run the operation
+    // regardless.  Privileged callers can always trigger an immediate pass via
+    // BackupManager.backupNow().
+    private static final long MAX_DEFERRAL = AlarmManager.INTERVAL_DAY;
+
+    @GuardedBy("KeyValueBackupJob.class")
+    private static final SparseBooleanArray sScheduledForUserId = new SparseBooleanArray();
+    @GuardedBy("KeyValueBackupJob.class")
+    private static final SparseLongArray sNextScheduledForUserId = new SparseLongArray();
+
+    @VisibleForTesting
+    public static final int MIN_JOB_ID = 52417896;
+    @VisibleForTesting
+    public static final int MAX_JOB_ID = 52418896;
+
+    public static void schedule(int userId, Context ctx, BackupManagerConstants constants) {
+        schedule(userId, ctx, 0, constants);
+    }
+
+    public static void schedule(int userId, Context ctx, long delay,
+            BackupManagerConstants constants) {
+        synchronized (KeyValueBackupJob.class) {
+            if (sScheduledForUserId.get(userId)) {
+                return;
+            }
+
+            final long interval;
+            final long fuzz;
+            final int networkType;
+            final boolean needsCharging;
+
+            synchronized (constants) {
+                interval = constants.getKeyValueBackupIntervalMilliseconds();
+                fuzz = constants.getKeyValueBackupFuzzMilliseconds();
+                networkType = constants.getKeyValueBackupRequiredNetworkType();
+                needsCharging = constants.getKeyValueBackupRequireCharging();
+            }
+            if (delay <= 0) {
+                delay = interval + new Random().nextInt((int) fuzz);
+            }
+            if (DEBUG_SCHEDULING) {
+                Slog.v(TAG, "Scheduling k/v pass in " + (delay / 1000 / 60) + " minutes");
+            }
+
+            JobInfo.Builder builder = new JobInfo.Builder(getJobIdForUserId(userId),
+                    sKeyValueJobService)
+                    .setMinimumLatency(delay)
+                    .setRequiredNetworkType(networkType)
+                    .setRequiresCharging(needsCharging)
+                    .setOverrideDeadline(MAX_DEFERRAL);
+
+            Bundle extraInfo = new Bundle();
+            extraInfo.putInt(USER_ID_EXTRA_KEY, userId);
+            builder.setTransientExtras(extraInfo);
+
+            JobScheduler js = (JobScheduler) ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+            js.schedule(builder.build());
+
+            sScheduledForUserId.put(userId, true);
+            sNextScheduledForUserId.put(userId, System.currentTimeMillis() + delay);
+        }
+    }
+
+    public static void cancel(int userId, Context ctx) {
+        synchronized (KeyValueBackupJob.class) {
+            JobScheduler js = (JobScheduler) ctx.getSystemService(
+                    Context.JOB_SCHEDULER_SERVICE);
+            js.cancel(getJobIdForUserId(userId));
+
+            clearScheduledForUserId(userId);
+        }
+    }
+
+    public static long nextScheduled(int userId) {
+        synchronized (KeyValueBackupJob.class) {
+            return sNextScheduledForUserId.get(userId);
+        }
+    }
+
+    @VisibleForTesting
+    public static boolean isScheduled(int userId) {
+        synchronized (KeyValueBackupJob.class) {
+            return sScheduledForUserId.get(userId);
+        }
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        int userId = params.getTransientExtras().getInt(USER_ID_EXTRA_KEY);
+
+        synchronized (KeyValueBackupJob.class) {
+            clearScheduledForUserId(userId);
+        }
+
+        // Time to run a key/value backup!
+        BackupManagerService service = BackupManagerService.getInstance();
+        try {
+            service.backupNowForUser(userId);
+        } catch (RemoteException e) {}
+
+        // This was just a trigger; ongoing wakelock management is done by the
+        // rest of the backup system.
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        // Intentionally empty; the job starting was just a trigger
+        return false;
+    }
+
+    @GuardedBy("KeyValueBackupJob.class")
+    private static void clearScheduledForUserId(int userId) {
+        sScheduledForUserId.delete(userId);
+        sNextScheduledForUserId.delete(userId);
+    }
+
+    private static int getJobIdForUserId(int userId) {
+        return JobIdManager.getJobIdForUserId(MIN_JOB_ID, MAX_JOB_ID, userId);
+    }
+}
diff --git a/com/android/server/backup/KeyValueBackupJobTest.java b/com/android/server/backup/KeyValueBackupJobTest.java
new file mode 100644
index 0000000..1c5fac2
--- /dev/null
+++ b/com/android/server/backup/KeyValueBackupJobTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSystemServiceRegistry.class})
+@Presubmit
+public class KeyValueBackupJobTest {
+    private Context mContext;
+    private BackupManagerConstants mConstants;
+
+    @UserIdInt private int mUserOneId;
+    @UserIdInt private int mUserTwoId;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = RuntimeEnvironment.application;
+        mConstants = new BackupManagerConstants(Handler.getMain(), mContext.getContentResolver());
+        mConstants.start();
+
+        mUserOneId = UserHandle.USER_SYSTEM;
+        mUserTwoId = mUserOneId + 1;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mConstants.stop();
+        KeyValueBackupJob.cancel(mUserOneId, mContext);
+        KeyValueBackupJob.cancel(mUserTwoId, mContext);
+    }
+
+    @Test
+    public void testIsScheduled_beforeScheduling_returnsFalse() {
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
+        assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isFalse();
+    }
+
+    @Test
+    public void testIsScheduled_afterScheduling_returnsTrue() {
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isTrue();
+        assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isTrue();
+    }
+
+    @Test
+    public void testIsScheduled_afterCancelling_returnsFalse() {
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+        KeyValueBackupJob.cancel(mUserOneId, mContext);
+        KeyValueBackupJob.cancel(mUserTwoId, mContext);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
+        assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isFalse();
+    }
+
+    @Test
+    public void testIsScheduled_afterScheduling_returnsTrueOnlyForScheduledUser() {
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isTrue();
+        assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isFalse();
+    }
+
+    @Test
+    public void testIsScheduled_afterCancelling_returnsFalseOnlyForCancelledUser() {
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+        KeyValueBackupJob.cancel(mUserOneId, mContext);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
+        assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isTrue();
+    }
+}
diff --git a/com/android/server/backup/NotificationBackupHelper.java b/com/android/server/backup/NotificationBackupHelper.java
new file mode 100644
index 0000000..7d4f8f7
--- /dev/null
+++ b/com/android/server/backup/NotificationBackupHelper.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 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 android.app.INotificationManager;
+import android.app.backup.BlobBackupHelper;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Slog;
+
+public class NotificationBackupHelper extends BlobBackupHelper {
+    static final String TAG = "NotifBackupHelper";   // must be < 23 chars
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // Current version of the blob schema
+    static final int BLOB_VERSION = 1;
+
+    // Key under which the payload blob is stored
+    static final String KEY_NOTIFICATIONS = "notifications";
+
+    private final int mUserId;
+
+    public NotificationBackupHelper(int userId) {
+        super(BLOB_VERSION, KEY_NOTIFICATIONS);
+        mUserId = userId;
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        byte[] newPayload = null;
+        if (KEY_NOTIFICATIONS.equals(key)) {
+            try {
+                INotificationManager nm = INotificationManager.Stub.asInterface(
+                        ServiceManager.getService("notification"));
+                newPayload = nm.getBackupPayload(mUserId);
+            } catch (Exception e) {
+                // Treat as no data
+                Slog.e(TAG, "Couldn't communicate with notification manager");
+                newPayload = null;
+            }
+        }
+        return newPayload;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (DEBUG) {
+            Slog.v(TAG, "Got restore of " + key);
+        }
+
+        if (KEY_NOTIFICATIONS.equals(key)) {
+            try {
+                INotificationManager nm = INotificationManager.Stub.asInterface(
+                        ServiceManager.getService("notification"));
+                nm.applyRestore(payload, mUserId);
+            } catch (Exception e) {
+                Slog.e(TAG, "Couldn't communicate with notification manager");
+            }
+        }
+    }
+
+}
diff --git a/com/android/server/backup/PackageManagerBackupAgent.java b/com/android/server/backup/PackageManagerBackupAgent.java
new file mode 100644
index 0000000..2241569
--- /dev/null
+++ b/com/android/server/backup/PackageManagerBackupAgent.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2009 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 android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.backup.utils.AppBackupUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * We back up the signatures of each package so that during a system restore,
+ * we can verify that the app whose data we think we have matches the app
+ * actually resident on the device.
+ *
+ * Since the Package Manager isn't a proper "application" we just provide a
+ * direct IBackupAgent implementation and hand-construct it at need.
+ */
+public class PackageManagerBackupAgent extends BackupAgent {
+    private static final String TAG = "PMBA";
+    private static final boolean DEBUG = false;
+
+    // key under which we store global metadata (individual app metadata
+    // is stored using the package name as a key)
+    private static final String GLOBAL_METADATA_KEY = "@meta@";
+
+    // key under which we store the identity of the user's chosen default home app
+    private static final String DEFAULT_HOME_KEY = "@home@";
+
+    // Sentinel: start of state file, followed by a version number
+    // Note that STATE_FILE_VERSION=2 is tied to UNDEFINED_ANCESTRAL_RECORD_VERSION=-1 *as well as*
+    // ANCESTRAL_RECORD_VERSION=1 (introduced Android P).
+    // Should the ANCESTRAL_RECORD_VERSION be bumped up in the future, STATE_FILE_VERSION will also
+    // need bumping up, assuming more data needs saving to the state file.
+    private static final String STATE_FILE_HEADER = "=state=";
+    private static final int STATE_FILE_VERSION = 2;
+
+    // key under which we store the saved ancestral-dataset format (starting from Android P)
+    // IMPORTANT: this key needs to come first in the restore data stream (to find out
+    // whether this version of Android knows how to restore the incoming data set), so it needs
+    // to be always the first one in alphabetical order of all the keys
+    private static final String ANCESTRAL_RECORD_KEY = "@ancestral_record@";
+
+    // Current version of the saved ancestral-dataset format
+    // Note that this constant was not used until Android P, and started being used
+    // to version @pm@ data for forwards-compatibility.
+    private static final int ANCESTRAL_RECORD_VERSION = 1;
+
+    // Undefined version of the saved ancestral-dataset file format means that the restore data
+    // is coming from pre-Android P device.
+    private static final int UNDEFINED_ANCESTRAL_RECORD_VERSION = -1;
+
+    private int mUserId;
+    private List<PackageInfo> mAllPackages;
+    private PackageManager mPackageManager;
+    // version & signature info of each app in a restore set
+    private HashMap<String, Metadata> mRestoredSignatures;
+    // The version info of each backed-up app as read from the state file
+    private HashMap<String, Metadata> mStateVersions = new HashMap<String, Metadata>();
+
+    private final HashSet<String> mExisting = new HashSet<String>();
+    private int mStoredSdkVersion;
+    private String mStoredIncrementalVersion;
+    private ComponentName mStoredHomeComponent;
+    private long mStoredHomeVersion;
+    private ArrayList<byte[]> mStoredHomeSigHashes;
+
+    private boolean mHasMetadata;
+    private ComponentName mRestoredHome;
+    private long mRestoredHomeVersion;
+    private String mRestoredHomeInstaller;
+    private ArrayList<byte[]> mRestoredHomeSigHashes;
+
+    // For compactness we store the SHA-256 hash of each app's Signatures
+    // rather than the Signature blocks themselves.
+    public class Metadata {
+        public long versionCode;
+        public ArrayList<byte[]> sigHashes;
+
+        Metadata(long version, ArrayList<byte[]> hashes) {
+            versionCode = version;
+            sigHashes = hashes;
+        }
+    }
+
+    // We're constructed with the set of applications that are participating
+    // in backup.  This set changes as apps are installed & removed.
+    public PackageManagerBackupAgent(
+            PackageManager packageMgr, List<PackageInfo> packages, int userId) {
+        init(packageMgr, packages, userId);
+    }
+
+    public PackageManagerBackupAgent(PackageManager packageMgr, int userId) {
+        init(packageMgr, null, userId);
+
+        evaluateStorablePackages();
+    }
+
+    private void init(PackageManager packageMgr, List<PackageInfo> packages, int userId) {
+        mPackageManager = packageMgr;
+        mAllPackages = packages;
+        mRestoredSignatures = null;
+        mHasMetadata = false;
+
+        mStoredSdkVersion = Build.VERSION.SDK_INT;
+        mStoredIncrementalVersion = Build.VERSION.INCREMENTAL;
+        mUserId = userId;
+    }
+
+    // We will need to refresh our understanding of what is eligible for
+    // backup periodically; this entry point serves that purpose.
+    public void evaluateStorablePackages() {
+        mAllPackages = getStorableApplications(mPackageManager, mUserId);
+    }
+
+    /** Gets all packages installed on user {@code userId} eligible for backup. */
+    public static List<PackageInfo> getStorableApplications(PackageManager pm, int userId) {
+        List<PackageInfo> pkgs =
+                pm.getInstalledPackagesAsUser(PackageManager.GET_SIGNING_CERTIFICATES, userId);
+        int N = pkgs.size();
+        for (int a = N-1; a >= 0; a--) {
+            PackageInfo pkg = pkgs.get(a);
+            if (!AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo, userId)) {
+                pkgs.remove(a);
+            }
+        }
+        return pkgs;
+    }
+
+    public boolean hasMetadata() {
+        return mHasMetadata;
+    }
+
+    public Metadata getRestoredMetadata(String packageName) {
+        if (mRestoredSignatures == null) {
+            Slog.w(TAG, "getRestoredMetadata() before metadata read!");
+            return null;
+        }
+
+        return mRestoredSignatures.get(packageName);
+    }
+
+    public Set<String> getRestoredPackages() {
+        if (mRestoredSignatures == null) {
+            Slog.w(TAG, "getRestoredPackages() before metadata read!");
+            return null;
+        }
+
+        // This is technically the set of packages on the originating handset
+        // that had backup agents at all, not limited to the set of packages
+        // that had actually contributed a restore dataset, but it's a
+        // close enough approximation for our purposes and does not require any
+        // additional involvement by the transport to obtain.
+        return mRestoredSignatures.keySet();
+    }
+
+    // The backed up data is the signature block for each app, keyed by the package name.
+    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+            ParcelFileDescriptor newState) {
+        if (DEBUG) Slog.v(TAG, "onBackup()");
+
+        ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();  // we'll reuse these
+        DataOutputStream outputBufferStream = new DataOutputStream(outputBuffer);
+        parseStateFile(oldState);
+
+        // If the stored version string differs, we need to re-backup all
+        // of the metadata.  We force this by removing everything from the
+        // "already backed up" map built by parseStateFile().
+        if (mStoredIncrementalVersion == null
+                || !mStoredIncrementalVersion.equals(Build.VERSION.INCREMENTAL)) {
+            Slog.i(TAG, "Previous metadata " + mStoredIncrementalVersion + " mismatch vs "
+                    + Build.VERSION.INCREMENTAL + " - rewriting");
+            mExisting.clear();
+        }
+
+        /*
+         * Ancestral record version:
+         *
+         * int ancestralRecordVersion -- the version of the format in which this backup set is
+         *                               produced
+         */
+        try {
+            if (DEBUG) Slog.v(TAG, "Storing ancestral record version key");
+            outputBufferStream.writeInt(ANCESTRAL_RECORD_VERSION);
+            writeEntity(data, ANCESTRAL_RECORD_KEY, outputBuffer.toByteArray());
+        } catch (IOException e) {
+            // Real error writing data
+            Slog.e(TAG, "Unable to write package backup data file!");
+            return;
+        }
+
+        long homeVersion = 0;
+        ArrayList<byte[]> homeSigHashes = null;
+        PackageInfo homeInfo = null;
+        String homeInstaller = null;
+        ComponentName home = getPreferredHomeComponent();
+        if (home != null) {
+            try {
+                homeInfo = mPackageManager.getPackageInfoAsUser(home.getPackageName(),
+                        PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+                homeInstaller = mPackageManager.getInstallerPackageName(home.getPackageName());
+                homeVersion = homeInfo.getLongVersionCode();
+                SigningInfo signingInfo = homeInfo.signingInfo;
+                if (signingInfo == null) {
+                    Slog.e(TAG, "Home app has no signing information");
+                } else {
+                    // retrieve the newest sigs to back up
+                    // TODO (b/73988180) use entire signing history in case of rollbacks
+                    Signature[] homeInfoSignatures = signingInfo.getApkContentsSigners();
+                    homeSigHashes = BackupUtils.hashSignatureArray(homeInfoSignatures);
+                }
+            } catch (NameNotFoundException e) {
+                Slog.w(TAG, "Can't access preferred home info");
+                // proceed as though there were no preferred home set
+                home = null;
+            }
+        }
+
+        try {
+            // We need to push a new preferred-home-app record if:
+            //    1. the version of the home app has changed since our last backup;
+            //    2. the home app [or absence] we now use differs from the prior state,
+            // OR 3. it looks like we use the same home app + version as before, but
+            //       the signatures don't match so we treat them as different apps.
+            PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+            final boolean needHomeBackup = (homeVersion != mStoredHomeVersion)
+                    || !Objects.equals(home, mStoredHomeComponent)
+                    || (home != null
+                        && !BackupUtils.signaturesMatch(mStoredHomeSigHashes, homeInfo, pmi));
+            if (needHomeBackup) {
+                if (DEBUG) {
+                    Slog.i(TAG, "Home preference changed; backing up new state " + home);
+                }
+                if (home != null) {
+                    outputBuffer.reset();
+                    outputBufferStream.writeUTF(home.flattenToString());
+                    outputBufferStream.writeLong(homeVersion);
+                    outputBufferStream.writeUTF(homeInstaller != null ? homeInstaller : "" );
+                    writeSignatureHashArray(outputBufferStream, homeSigHashes);
+                    writeEntity(data, DEFAULT_HOME_KEY, outputBuffer.toByteArray());
+                } else {
+                    data.writeEntityHeader(DEFAULT_HOME_KEY, -1);
+                }
+            }
+
+            /*
+             * Global metadata:
+             *
+             * int SDKversion -- the SDK version of the OS itself on the device
+             *                   that produced this backup set. Before Android P it was used to
+             *                   reject backups from later OSes onto earlier ones.
+             * String incremental -- the incremental release name of the OS stored in
+             *                       the backup set.
+             */
+            outputBuffer.reset();
+            if (!mExisting.contains(GLOBAL_METADATA_KEY)) {
+                if (DEBUG) Slog.v(TAG, "Storing global metadata key");
+                outputBufferStream.writeInt(Build.VERSION.SDK_INT);
+                outputBufferStream.writeUTF(Build.VERSION.INCREMENTAL);
+                writeEntity(data, GLOBAL_METADATA_KEY, outputBuffer.toByteArray());
+            } else {
+                if (DEBUG) Slog.v(TAG, "Global metadata key already stored");
+                // don't consider it to have been skipped/deleted
+                mExisting.remove(GLOBAL_METADATA_KEY);
+            }
+
+            // For each app we have on device, see if we've backed it up yet.  If not,
+            // write its signature block to the output, keyed on the package name.
+            for (PackageInfo pkg : mAllPackages) {
+                String packName = pkg.packageName;
+                if (packName.equals(GLOBAL_METADATA_KEY)) {
+                    // We've already handled the metadata key; skip it here
+                    continue;
+                } else {
+                    PackageInfo info = null;
+                    try {
+                        info = mPackageManager.getPackageInfoAsUser(packName,
+                                PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+                    } catch (NameNotFoundException e) {
+                        // Weird; we just found it, and now are told it doesn't exist.
+                        // Treat it as having been removed from the device.
+                        mExisting.add(packName);
+                        continue;
+                    }
+
+                    if (mExisting.contains(packName)) {
+                        // We have backed up this app before.  Check whether the version
+                        // of the backup matches the version of the current app; if they
+                        // don't match, the app has been updated and we need to store its
+                        // metadata again.  In either case, take it out of mExisting so that
+                        // we don't consider it deleted later.
+                        mExisting.remove(packName);
+                        if (info.getLongVersionCode() == mStateVersions.get(packName).versionCode) {
+                            continue;
+                        }
+                    }
+
+                    SigningInfo signingInfo = info.signingInfo;
+                    if (signingInfo == null) {
+                        Slog.w(TAG, "Not backing up package " + packName
+                                + " since it appears to have no signatures.");
+                        continue;
+                    }
+
+                    // We need to store this app's metadata
+                    /*
+                     * Metadata for each package:
+                     *
+                     * int version       -- [4] the package's versionCode
+                     * byte[] signatures -- [len] flattened signature hash array of the package
+                     */
+
+                    // marshal the version code in a canonical form
+                    outputBuffer.reset();
+                    if (info.versionCodeMajor != 0) {
+                        outputBufferStream.writeInt(Integer.MIN_VALUE);
+                        outputBufferStream.writeLong(info.getLongVersionCode());
+                    } else {
+                        outputBufferStream.writeInt(info.versionCode);
+                    }
+                    // retrieve the newest sigs to back up
+                    Signature[] infoSignatures = signingInfo.getApkContentsSigners();
+                    writeSignatureHashArray(outputBufferStream,
+                            BackupUtils.hashSignatureArray(infoSignatures));
+
+                    if (DEBUG) {
+                        Slog.v(TAG, "+ writing metadata for " + packName
+                                + " version=" + info.getLongVersionCode()
+                                + " entityLen=" + outputBuffer.size());
+                    }
+                    
+                    // Now we can write the backup entity for this package
+                    writeEntity(data, packName, outputBuffer.toByteArray());
+                }
+            }
+
+            // At this point, the only entries in 'existing' are apps that were
+            // mentioned in the saved state file, but appear to no longer be present
+            // on the device.  We want to preserve the entry for them, however,
+            // because we want the right thing to happen if the user goes through
+            // a backup / uninstall / backup / reinstall sequence.
+            if (DEBUG) {
+                if (mExisting.size() > 0) {
+                    StringBuilder sb = new StringBuilder(64);
+                    sb.append("Preserving metadata for deleted packages:");
+                    for (String app : mExisting) {
+                        sb.append(' ');
+                        sb.append(app);
+                    }
+                    Slog.v(TAG, sb.toString());
+                }
+            }
+        } catch (IOException e) {
+            // Real error writing data
+            Slog.e(TAG, "Unable to write package backup data file!");
+            return;
+        }
+
+        // Finally, write the new state blob -- just the list of all apps we handled
+        writeStateFile(mAllPackages, home, homeVersion, homeSigHashes, newState);
+    }
+
+    private static void writeEntity(BackupDataOutput data, String key, byte[] bytes)
+            throws IOException {
+        data.writeEntityHeader(key, bytes.length);
+        data.writeEntityData(bytes, bytes.length);
+    }
+
+    // "Restore" here is a misnomer.  What we're really doing is reading back the
+    // set of app signatures associated with each backed-up app in this restore
+    // image.  We'll use those later to determine what we can legitimately restore.
+    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+            throws IOException {
+        if (DEBUG) Slog.v(TAG, "onRestore()");
+
+        // we expect the ANCESTRAL_RECORD_KEY ("@ancestral_record@") to always come first in the
+        // restore set - based on that value we use different mechanisms to consume the data;
+        // if the ANCESTRAL_RECORD_KEY is missing in the restore set, it means that the data is
+        // is coming from a pre-Android P device, and we consume the header data in the legacy way
+        // TODO: add a CTS test to verify that backups of PMBA generated on Android P+ always
+        //       contain the ANCESTRAL_RECORD_KEY, and it's always the first key
+        int ancestralRecordVersion = getAncestralRecordVersionValue(data);
+
+        RestoreDataConsumer consumer = getRestoreDataConsumer(ancestralRecordVersion);
+        if (consumer == null) {
+            Slog.w(TAG, "Ancestral restore set version is unknown"
+                    + " to this Android version; not restoring");
+            return;
+        } else {
+            consumer.consumeRestoreData(data);
+        }
+    }
+
+    private int getAncestralRecordVersionValue(BackupDataInput data) throws IOException {
+        int ancestralRecordVersionValue = UNDEFINED_ANCESTRAL_RECORD_VERSION;
+        if (data.readNextHeader()) {
+            String key = data.getKey();
+            int dataSize = data.getDataSize();
+
+            if (DEBUG) Slog.v(TAG, "   got key=" + key + " dataSize=" + dataSize);
+
+            if (ANCESTRAL_RECORD_KEY.equals(key)) {
+                // generic setup to parse any entity data
+                byte[] inputBytes = new byte[dataSize];
+                data.readEntityData(inputBytes, 0, dataSize);
+                ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+                DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
+
+                ancestralRecordVersionValue = inputBufferStream.readInt();
+            }
+        }
+        return ancestralRecordVersionValue;
+    }
+
+    private RestoreDataConsumer getRestoreDataConsumer(int ancestralRecordVersion) {
+        switch (ancestralRecordVersion) {
+            case UNDEFINED_ANCESTRAL_RECORD_VERSION:
+                return new LegacyRestoreDataConsumer();
+            case 1:
+                return new AncestralVersion1RestoreDataConsumer();
+            default:
+                Slog.e(TAG, "Unrecognized ANCESTRAL_RECORD_VERSION: " + ancestralRecordVersion);
+                return null;
+        }
+    }
+
+    private static void writeSignatureHashArray(DataOutputStream out, ArrayList<byte[]> hashes)
+            throws IOException {
+        // the number of entries in the array
+        out.writeInt(hashes.size());
+
+        // the hash arrays themselves as length + contents
+        for (byte[] buffer : hashes) {
+            out.writeInt(buffer.length);
+            out.write(buffer);
+        }
+    }
+
+    private static ArrayList<byte[]> readSignatureHashArray(DataInputStream in) {
+        try {
+            int num;
+            try {
+                num = in.readInt();
+            } catch (EOFException e) {
+                // clean termination
+                Slog.w(TAG, "Read empty signature block");
+                return null;
+            }
+            
+            if (DEBUG) Slog.v(TAG, " ... unflatten read " + num);
+            
+            // Sensical?
+            if (num > 20) {
+                Slog.e(TAG, "Suspiciously large sig count in restore data; aborting");
+                throw new IllegalStateException("Bad restore state");
+            }
+
+            // This could be a "legacy" block of actual signatures rather than their hashes.
+            // If this is the case, convert them now.  We judge based on the payload size:
+            // if the blocks are all 256 bits (32 bytes) then we take them to be SHA-256 hashes;
+            // otherwise we take them to be Signatures.
+            boolean nonHashFound = false;
+            ArrayList<byte[]> sigs = new ArrayList<byte[]>(num);
+            for (int i = 0; i < num; i++) {
+                int len = in.readInt();
+                byte[] readHash = new byte[len];
+                in.read(readHash);
+                sigs.add(readHash);
+                if (len != 32) {
+                    nonHashFound = true;
+                }
+            }
+
+            if (nonHashFound) {
+                // Replace with the hashes.
+                sigs = BackupUtils.hashSignatureArray(sigs);
+            }
+
+            return sigs;
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to read signatures");
+            return null;
+        }
+    }
+
+    // Util: parse out an existing state file into a usable structure
+    private void parseStateFile(ParcelFileDescriptor stateFile) {
+        mExisting.clear();
+        mStateVersions.clear();
+        mStoredSdkVersion = 0;
+        mStoredIncrementalVersion = null;
+        mStoredHomeComponent = null;
+        mStoredHomeVersion = 0;
+        mStoredHomeSigHashes = null;
+
+        // The state file is just the list of app names we have stored signatures for
+        // with the exception of the metadata block, to which is also appended the
+        // version numbers corresponding with the last time we wrote this PM block.
+        // If they mismatch the current system, we'll re-store the metadata key.
+        FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor());
+        BufferedInputStream inbuffer = new BufferedInputStream(instream);
+        DataInputStream in = new DataInputStream(inbuffer);
+
+        try {
+            boolean ignoreExisting = false;
+            String pkg = in.readUTF();
+
+            // Validate the state file version is sensical to us
+            if (pkg.equals(STATE_FILE_HEADER)) {
+                int stateVersion = in.readInt();
+                if (stateVersion > STATE_FILE_VERSION) {
+                    Slog.w(TAG, "Unsupported state file version " + stateVersion
+                            + ", redoing from start");
+                    return;
+                }
+                pkg = in.readUTF();
+            } else {
+                // This is an older version of the state file in which the lead element
+                // is not a STATE_FILE_VERSION string.  If that's the case, we want to
+                // make sure to write our full backup dataset when given an opportunity.
+                // We trigger that by simply not marking the restored package metadata
+                // as known-to-exist-in-archive.
+                Slog.i(TAG, "Older version of saved state - rewriting");
+                ignoreExisting = true;
+            }
+
+            // First comes the preferred home app data, if any, headed by the DEFAULT_HOME_KEY tag
+            if (pkg.equals(DEFAULT_HOME_KEY)) {
+                // flattened component name, version, signature of the home app
+                mStoredHomeComponent = ComponentName.unflattenFromString(in.readUTF());
+                mStoredHomeVersion = in.readLong();
+                mStoredHomeSigHashes = readSignatureHashArray(in);
+
+                pkg = in.readUTF(); // set up for the next block of state
+            } else {
+                // else no preferred home app on the ancestral device - fall through to the rest
+            }
+
+            // After (possible) home app data comes the global metadata block
+            if (pkg.equals(GLOBAL_METADATA_KEY)) {
+                mStoredSdkVersion = in.readInt();
+                mStoredIncrementalVersion = in.readUTF();
+                if (!ignoreExisting) {
+                    mExisting.add(GLOBAL_METADATA_KEY);
+                }
+            } else {
+                Slog.e(TAG, "No global metadata in state file!");
+                return;
+            }
+
+            // The global metadata was last; now read all the apps
+            while (true) {
+                pkg = in.readUTF();
+                int versionCodeInt = in.readInt();
+                long versionCode;
+                if (versionCodeInt == Integer.MIN_VALUE) {
+                    versionCode = in.readLong();
+                } else {
+                    versionCode = versionCodeInt;
+                }
+
+                if (!ignoreExisting) {
+                    mExisting.add(pkg);
+                }
+                mStateVersions.put(pkg, new Metadata(versionCode, null));
+            }
+        } catch (EOFException eof) {
+            // safe; we're done
+        } catch (IOException e) {
+            // whoops, bad state file.  abort.
+            Slog.e(TAG, "Unable to read Package Manager state file: " + e);
+        }
+    }
+
+    private ComponentName getPreferredHomeComponent() {
+        return mPackageManager.getHomeActivities(new ArrayList<ResolveInfo>());
+    }
+
+    // Util: write out our new backup state file
+    private void writeStateFile(List<PackageInfo> pkgs, ComponentName preferredHome,
+            long homeVersion, ArrayList<byte[]> homeSigHashes, ParcelFileDescriptor stateFile) {
+        FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
+        BufferedOutputStream outbuf = new BufferedOutputStream(outstream);
+        DataOutputStream out = new DataOutputStream(outbuf);
+
+        // by the time we get here we know we've done all our backing up
+        try {
+            // state file version header
+            out.writeUTF(STATE_FILE_HEADER);
+            out.writeInt(STATE_FILE_VERSION);
+
+            // If we remembered a preferred home app, record that
+            if (preferredHome != null) {
+                out.writeUTF(DEFAULT_HOME_KEY);
+                out.writeUTF(preferredHome.flattenToString());
+                out.writeLong(homeVersion);
+                writeSignatureHashArray(out, homeSigHashes);
+            }
+
+            // Conclude with the metadata block
+            out.writeUTF(GLOBAL_METADATA_KEY);
+            out.writeInt(Build.VERSION.SDK_INT);
+            out.writeUTF(Build.VERSION.INCREMENTAL);
+
+            // now write all the app names + versions
+            for (PackageInfo pkg : pkgs) {
+                out.writeUTF(pkg.packageName);
+                if (pkg.versionCodeMajor != 0) {
+                    out.writeInt(Integer.MIN_VALUE);
+                    out.writeLong(pkg.getLongVersionCode());
+                } else {
+                    out.writeInt(pkg.versionCode);
+                }
+            }
+
+            out.flush();
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to write package manager state file!");
+        }
+    }
+
+    interface RestoreDataConsumer {
+        void consumeRestoreData(BackupDataInput data) throws IOException;
+    }
+
+    private class LegacyRestoreDataConsumer implements RestoreDataConsumer {
+
+        public void consumeRestoreData(BackupDataInput data) throws IOException {
+            List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
+            HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>();
+            int storedSystemVersion = -1;
+
+            if (DEBUG) Slog.i(TAG, "Using LegacyRestoreDataConsumer");
+            // we already have the first header read and "cached", since ANCESTRAL_RECORD_KEY
+            // was missing
+            while (true) {
+                String key = data.getKey();
+                int dataSize = data.getDataSize();
+
+                if (DEBUG) Slog.v(TAG, "   got key=" + key + " dataSize=" + dataSize);
+
+                // generic setup to parse any entity data
+                byte[] inputBytes = new byte[dataSize];
+                data.readEntityData(inputBytes, 0, dataSize);
+                ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+                DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
+
+                if (key.equals(GLOBAL_METADATA_KEY)) {
+                    int storedSdkVersion = inputBufferStream.readInt();
+                    if (DEBUG) Slog.v(TAG, "   storedSystemVersion = " + storedSystemVersion);
+                    mStoredSdkVersion = storedSdkVersion;
+                    mStoredIncrementalVersion = inputBufferStream.readUTF();
+                    mHasMetadata = true;
+                    if (DEBUG) {
+                        Slog.i(TAG, "Restore set version " + storedSystemVersion
+                                + " is compatible with OS version " + Build.VERSION.SDK_INT
+                                + " (" + mStoredIncrementalVersion + " vs "
+                                + Build.VERSION.INCREMENTAL + ")");
+                    }
+                } else if (key.equals(DEFAULT_HOME_KEY)) {
+                    String cn = inputBufferStream.readUTF();
+                    mRestoredHome = ComponentName.unflattenFromString(cn);
+                    mRestoredHomeVersion = inputBufferStream.readLong();
+                    mRestoredHomeInstaller = inputBufferStream.readUTF();
+                    mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream);
+                    if (DEBUG) {
+                        Slog.i(TAG, "   read preferred home app " + mRestoredHome
+                                + " version=" + mRestoredHomeVersion
+                                + " installer=" + mRestoredHomeInstaller
+                                + " sig=" + mRestoredHomeSigHashes);
+                    }
+                } else {
+                    // it's a file metadata record
+                    int versionCodeInt = inputBufferStream.readInt();
+                    long versionCode;
+                    if (versionCodeInt == Integer.MIN_VALUE) {
+                        versionCode = inputBufferStream.readLong();
+                    } else {
+                        versionCode = versionCodeInt;
+                    }
+                    ArrayList<byte[]> sigs = readSignatureHashArray(inputBufferStream);
+                    if (DEBUG) {
+                        Slog.i(TAG, "   read metadata for " + key
+                                + " dataSize=" + dataSize
+                                + " versionCode=" + versionCode + " sigs=" + sigs);
+                    }
+
+                    if (sigs == null || sigs.size() == 0) {
+                        Slog.w(TAG, "Not restoring package " + key
+                                + " since it appears to have no signatures.");
+                        continue;
+                    }
+
+                    ApplicationInfo app = new ApplicationInfo();
+                    app.packageName = key;
+                    restoredApps.add(app);
+                    sigMap.put(key, new Metadata(versionCode, sigs));
+                }
+
+                boolean readNextHeader = data.readNextHeader();
+                if (!readNextHeader) {
+                    if (DEBUG) Slog.v(TAG, "LegacyRestoreDataConsumer:"
+                            + " we're done reading all the headers");
+                    break;
+                }
+            }
+
+            // On successful completion, cache the signature map for the Backup Manager to use
+            mRestoredSignatures = sigMap;
+        }
+    }
+
+    private class AncestralVersion1RestoreDataConsumer implements RestoreDataConsumer {
+
+        public void consumeRestoreData(BackupDataInput data) throws IOException {
+            List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
+            HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>();
+            int storedSystemVersion = -1;
+
+            if (DEBUG) Slog.i(TAG, "Using AncestralVersion1RestoreDataConsumer");
+            while (data.readNextHeader()) {
+                String key = data.getKey();
+                int dataSize = data.getDataSize();
+
+                if (DEBUG) Slog.v(TAG, "   got key=" + key + " dataSize=" + dataSize);
+
+                // generic setup to parse any entity data
+                byte[] inputBytes = new byte[dataSize];
+                data.readEntityData(inputBytes, 0, dataSize);
+                ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+                DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
+
+                if (key.equals(GLOBAL_METADATA_KEY)) {
+                    int storedSdkVersion = inputBufferStream.readInt();
+                    if (DEBUG) Slog.v(TAG, "   storedSystemVersion = " + storedSystemVersion);
+                    mStoredSdkVersion = storedSdkVersion;
+                    mStoredIncrementalVersion = inputBufferStream.readUTF();
+                    mHasMetadata = true;
+                    if (DEBUG) {
+                        Slog.i(TAG, "Restore set version " + storedSystemVersion
+                                + " is compatible with OS version " + Build.VERSION.SDK_INT
+                                + " (" + mStoredIncrementalVersion + " vs "
+                                + Build.VERSION.INCREMENTAL + ")");
+                    }
+                } else if (key.equals(DEFAULT_HOME_KEY)) {
+                    String cn = inputBufferStream.readUTF();
+                    mRestoredHome = ComponentName.unflattenFromString(cn);
+                    mRestoredHomeVersion = inputBufferStream.readLong();
+                    mRestoredHomeInstaller = inputBufferStream.readUTF();
+                    mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream);
+                    if (DEBUG) {
+                        Slog.i(TAG, "   read preferred home app " + mRestoredHome
+                                + " version=" + mRestoredHomeVersion
+                                + " installer=" + mRestoredHomeInstaller
+                                + " sig=" + mRestoredHomeSigHashes);
+                    }
+                } else {
+                    // it's a file metadata record
+                    int versionCodeInt = inputBufferStream.readInt();
+                    long versionCode;
+                    if (versionCodeInt == Integer.MIN_VALUE) {
+                        versionCode = inputBufferStream.readLong();
+                    } else {
+                        versionCode = versionCodeInt;
+                    }
+                    ArrayList<byte[]> sigs = readSignatureHashArray(inputBufferStream);
+                    if (DEBUG) {
+                        Slog.i(TAG, "   read metadata for " + key
+                                + " dataSize=" + dataSize
+                                + " versionCode=" + versionCode + " sigs=" + sigs);
+                    }
+
+                    if (sigs == null || sigs.size() == 0) {
+                        Slog.w(TAG, "Not restoring package " + key
+                                + " since it appears to have no signatures.");
+                        continue;
+                    }
+
+                    ApplicationInfo app = new ApplicationInfo();
+                    app.packageName = key;
+                    restoredApps.add(app);
+                    sigMap.put(key, new Metadata(versionCode, sigs));
+                }
+            }
+
+            // On successful completion, cache the signature map for the Backup Manager to use
+            mRestoredSignatures = sigMap;
+        }
+    }
+}
diff --git a/com/android/server/backup/PeopleBackupHelper.java b/com/android/server/backup/PeopleBackupHelper.java
new file mode 100644
index 0000000..555e006
--- /dev/null
+++ b/com/android/server/backup/PeopleBackupHelper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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 android.app.backup.BlobBackupHelper;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.people.PeopleServiceInternal;
+
+class PeopleBackupHelper extends BlobBackupHelper {
+
+    private static final String TAG = PeopleBackupHelper.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    // Current schema of the backup state blob.
+    private static final int STATE_VERSION = 1;
+
+    // Key under which conversation infos state blob is committed to backup.
+    private static final String KEY_CONVERSATIONS = "people_conversation_infos";
+
+    private final int mUserId;
+
+    PeopleBackupHelper(int userId) {
+        super(STATE_VERSION, KEY_CONVERSATIONS);
+        mUserId = userId;
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        if (!KEY_CONVERSATIONS.equals(key)) {
+            Slog.w(TAG, "Unexpected backup key " + key);
+            return new byte[0];
+        }
+        PeopleServiceInternal ps = LocalServices.getService(PeopleServiceInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling backup of " + key);
+        }
+        return ps.getBackupPayload(mUserId);
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (!KEY_CONVERSATIONS.equals(key)) {
+            Slog.w(TAG, "Unexpected restore key " + key);
+            return;
+        }
+        PeopleServiceInternal ps = LocalServices.getService(PeopleServiceInternal.class);
+        if (DEBUG) {
+            Slog.d(TAG, "Handling restore of " + key);
+        }
+        ps.restore(mUserId, payload);
+    }
+}
diff --git a/com/android/server/backup/PermissionBackupHelper.java b/com/android/server/backup/PermissionBackupHelper.java
new file mode 100644
index 0000000..c7c423b
--- /dev/null
+++ b/com/android/server/backup/PermissionBackupHelper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 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 android.annotation.NonNull;
+import android.app.backup.BlobBackupHelper;
+import android.os.UserHandle;
+import android.permission.PermissionManagerInternal;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+
+public class PermissionBackupHelper extends BlobBackupHelper {
+    private static final String TAG = "PermissionBackup";
+    private static final boolean DEBUG = false;
+
+    // current schema of the backup state blob
+    private static final int STATE_VERSION = 1;
+
+    // key under which the permission-grant state blob is committed to backup
+    private static final String KEY_PERMISSIONS = "permissions";
+
+    private final @NonNull UserHandle mUser;
+
+    private final @NonNull PermissionManagerInternal mPermissionManager;
+
+    public PermissionBackupHelper(int userId) {
+        super(STATE_VERSION, KEY_PERMISSIONS);
+
+        mUser = UserHandle.of(userId);
+        mPermissionManager = LocalServices.getService(PermissionManagerInternal.class);
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        if (DEBUG) {
+            Slog.d(TAG, "Handling backup of " + key);
+        }
+        try {
+            switch (key) {
+                case KEY_PERMISSIONS:
+                    return mPermissionManager.backupRuntimePermissions(mUser);
+
+                default:
+                    Slog.w(TAG, "Unexpected backup key " + key);
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Unable to store payload " + key);
+        }
+        return null;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (DEBUG) {
+            Slog.d(TAG, "Handling restore of " + key);
+        }
+        try {
+            switch (key) {
+                case KEY_PERMISSIONS:
+                    mPermissionManager.restoreRuntimePermissions(payload, mUser);
+                    break;
+
+                default:
+                    Slog.w(TAG, "Unexpected restore key " + key);
+            }
+        } catch (Exception e) {
+            Slog.w(TAG, "Unable to restore key " + key);
+        }
+    }
+}
diff --git a/com/android/server/backup/PreferredActivityBackupHelper.java b/com/android/server/backup/PreferredActivityBackupHelper.java
new file mode 100644
index 0000000..8063670
--- /dev/null
+++ b/com/android/server/backup/PreferredActivityBackupHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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 android.app.AppGlobals;
+import android.app.backup.BlobBackupHelper;
+import android.content.pm.IPackageManager;
+import android.os.UserHandle;
+import android.util.Slog;
+
+public class PreferredActivityBackupHelper extends BlobBackupHelper {
+    private static final String TAG = "PreferredBackup";
+    private static final boolean DEBUG = false;
+
+    // current schema of the backup state blob
+    private static final int STATE_VERSION = 3;
+
+    // key under which the preferred-activity state blob is committed to backup
+    private static final String KEY_PREFERRED = "preferred-activity";
+
+    // key for default-browser [etc] state
+    private static final String KEY_DEFAULT_APPS = "default-apps";
+
+    // intent-filter verification state
+    private static final String KEY_INTENT_VERIFICATION = "intent-verification";
+
+    public PreferredActivityBackupHelper() {
+        super(STATE_VERSION,
+                KEY_PREFERRED,
+                KEY_DEFAULT_APPS,
+                KEY_INTENT_VERIFICATION);
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        IPackageManager pm = AppGlobals.getPackageManager();
+        if (DEBUG) {
+            Slog.d(TAG, "Handling backup of " + key);
+        }
+        try {
+            // TODO: http://b/22388012
+            switch (key) {
+                case KEY_PREFERRED:
+                    return pm.getPreferredActivityBackup(UserHandle.USER_SYSTEM);
+                case KEY_DEFAULT_APPS:
+                    return pm.getDefaultAppsBackup(UserHandle.USER_SYSTEM);
+                case KEY_INTENT_VERIFICATION:
+                    return pm.getIntentFilterVerificationBackup(UserHandle.USER_SYSTEM);
+                default:
+                    Slog.w(TAG, "Unexpected backup key " + key);
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Unable to store payload " + key);
+        }
+        return null;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        IPackageManager pm = AppGlobals.getPackageManager();
+        if (DEBUG) {
+            Slog.d(TAG, "Handling restore of " + key);
+        }
+        try {
+            // TODO: http://b/22388012
+            switch (key) {
+                case KEY_PREFERRED:
+                    pm.restorePreferredActivities(payload, UserHandle.USER_SYSTEM);
+                    break;
+                case KEY_DEFAULT_APPS:
+                    pm.restoreDefaultApps(payload, UserHandle.USER_SYSTEM);
+                    break;
+                case KEY_INTENT_VERIFICATION:
+                    pm.restoreIntentFilterVerification(payload, UserHandle.USER_SYSTEM);
+                    break;
+                default:
+                    Slog.w(TAG, "Unexpected restore key " + key);
+            }
+        } catch (Exception e) {
+            Slog.w(TAG, "Unable to restore key " + key);
+        }
+    }
+}
diff --git a/com/android/server/backup/ProcessedPackagesJournal.java b/com/android/server/backup/ProcessedPackagesJournal.java
new file mode 100644
index 0000000..edc2379
--- /dev/null
+++ b/com/android/server/backup/ProcessedPackagesJournal.java
@@ -0,0 +1,150 @@
+/*
+ * 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;
+
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Records which apps have been backed up on this device, persisting it to disk so that it can be
+ * read at subsequent boots. This class is threadsafe.
+ *
+ * <p>This is used to decide, when restoring a package at install time, whether it has been
+ * previously backed up on the current device. If it has been previously backed up it should
+ * restore from the same restore set that the current device has been backing up to. If it has not
+ * been previously backed up, it should restore from the ancestral restore set (i.e., the restore
+ * set that the user's previous device was backing up to).
+ *
+ * <p>NB: this is always backed by the same files within the state directory supplied at
+ * construction.
+ */
+final class ProcessedPackagesJournal {
+    private static final String TAG = "ProcessedPackagesJournal";
+    private static final String JOURNAL_FILE_NAME = "processed";
+    private static final boolean DEBUG = BackupManagerService.DEBUG;
+
+    // using HashSet instead of ArraySet since we expect 100-500 elements range
+    @GuardedBy("mProcessedPackages")
+    private final Set<String> mProcessedPackages = new HashSet<>();
+    // TODO: at some point consider splitting the bookkeeping to be per-transport
+    private final File mStateDirectory;
+
+    /**
+     * Constructs a new journal.
+     *
+     * After constructing the object one should call {@link #init()} to load state from disk if
+     * it has been previously persisted.
+     *
+     * @param stateDirectory The directory in which backup state (including journals) is stored.
+     */
+    ProcessedPackagesJournal(File stateDirectory) {
+        mStateDirectory = stateDirectory;
+    }
+
+    /**
+     * Loads state from disk if it has been previously persisted.
+     */
+    void init() {
+        synchronized (mProcessedPackages) {
+            loadFromDisk();
+        }
+    }
+
+    /**
+     * Returns {@code true} if {@code packageName} has previously been backed up.
+     */
+    boolean hasBeenProcessed(String packageName) {
+        synchronized (mProcessedPackages) {
+            return mProcessedPackages.contains(packageName);
+        }
+    }
+
+    void addPackage(String packageName) {
+        synchronized (mProcessedPackages) {
+            if (!mProcessedPackages.add(packageName)) {
+                // This package has already been processed - no need to add it to the journal.
+                return;
+            }
+
+            File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
+
+            try (RandomAccessFile out = new RandomAccessFile(journalFile, "rws")) {
+                out.seek(out.length());
+                out.writeUTF(packageName);
+            } catch (IOException e) {
+                Slog.e(TAG, "Can't log backup of " + packageName + " to " + journalFile);
+            }
+        }
+    }
+
+    /**
+     * A copy of the current state of the journal.
+     *
+     * <p>Used only for dumping out information for logging. {@link #hasBeenProcessed(String)}
+     * should be used for efficiently checking whether a package has been backed up before by this
+     * device.
+     *
+     * @return The current set of packages that have been backed up previously.
+     */
+    Set<String> getPackagesCopy() {
+        synchronized (mProcessedPackages) {
+            return new HashSet<>(mProcessedPackages);
+        }
+    }
+
+    void reset() {
+        synchronized (mProcessedPackages) {
+            mProcessedPackages.clear();
+            File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
+            journalFile.delete();
+        }
+    }
+
+    private void loadFromDisk() {
+        File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
+
+        if (!journalFile.exists()) {
+            return;
+        }
+
+        try (DataInputStream oldJournal = new DataInputStream(
+                new BufferedInputStream(new FileInputStream(journalFile)))) {
+            while (true) {
+                String packageName = oldJournal.readUTF();
+                if (DEBUG) {
+                    Slog.v(TAG, "   + " + packageName);
+                }
+                mProcessedPackages.add(packageName);
+            }
+        } catch (EOFException e) {
+            // Successfully loaded journal file
+        } catch (IOException e) {
+            Slog.e(TAG, "Error reading processed packages journal", e);
+        }
+    }
+}
diff --git a/com/android/server/backup/ShortcutBackupHelper.java b/com/android/server/backup/ShortcutBackupHelper.java
new file mode 100644
index 0000000..0b3f2ae
--- /dev/null
+++ b/com/android/server/backup/ShortcutBackupHelper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 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 android.app.backup.BlobBackupHelper;
+import android.content.Context;
+import android.content.pm.IShortcutService;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Slog;
+
+public class ShortcutBackupHelper extends BlobBackupHelper {
+    private static final String TAG = "ShortcutBackupAgent";
+    private static final int BLOB_VERSION = 1;
+
+    private static final String KEY_USER_FILE = "shortcutuser.xml";
+
+    public ShortcutBackupHelper() {
+        super(BLOB_VERSION, KEY_USER_FILE);
+    }
+
+    private IShortcutService getShortcutService() {
+        return IShortcutService.Stub.asInterface(
+                ServiceManager.getService(Context.SHORTCUT_SERVICE));
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        switch (key) {
+            case KEY_USER_FILE:
+                try {
+                    return getShortcutService().getBackupPayload(UserHandle.USER_SYSTEM);
+                } catch (Exception e) {
+                    Slog.wtf(TAG, "Backup failed", e);
+                }
+                break;
+            default:
+                Slog.w(TAG, "Unknown key: " + key);
+        }
+        return null;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        switch (key) {
+            case KEY_USER_FILE:
+                try {
+                    getShortcutService().applyRestore(payload, UserHandle.USER_SYSTEM);
+                } catch (Exception e) {
+                    Slog.wtf(TAG, "Restore failed", e);
+                }
+                break;
+            default:
+                Slog.w(TAG, "Unknown key: " + key);
+        }
+    }
+}
diff --git a/com/android/server/backup/SliceBackupHelper.java b/com/android/server/backup/SliceBackupHelper.java
new file mode 100644
index 0000000..8e5a5ee
--- /dev/null
+++ b/com/android/server/backup/SliceBackupHelper.java
@@ -0,0 +1,74 @@
+/*
+ * 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 android.app.backup.BlobBackupHelper;
+import android.app.slice.ISliceManager;
+import android.content.Context;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+public class SliceBackupHelper extends BlobBackupHelper {
+    static final String TAG = "SliceBackupHelper";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // Current version of the blob schema
+    static final int BLOB_VERSION = 1;
+
+    // Key under which the payload blob is stored
+    static final String KEY_SLICES = "slices";
+
+    public SliceBackupHelper(Context context) {
+        super(BLOB_VERSION, KEY_SLICES);
+        // context is currently unused
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        byte[] newPayload = null;
+        if (KEY_SLICES.equals(key)) {
+            try {
+                ISliceManager sm = ISliceManager.Stub.asInterface(
+                        ServiceManager.getService(Context.SLICE_SERVICE));
+                // TODO: http://b/22388012
+                newPayload = sm.getBackupPayload(UserHandle.USER_SYSTEM);
+            } catch (Exception e) {
+                // Treat as no data
+                Slog.e(TAG, "Couldn't communicate with slice manager");
+                newPayload = null;
+            }
+        }
+        return newPayload;
+    }
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload) {
+        if (DEBUG) Slog.v(TAG, "Got restore of " + key);
+
+        if (KEY_SLICES.equals(key)) {
+            try {
+                ISliceManager sm = ISliceManager.Stub.asInterface(
+                        ServiceManager.getService(Context.SLICE_SERVICE));
+                // TODO: http://b/22388012
+                sm.applyRestore(payload, UserHandle.USER_SYSTEM);
+            } catch (Exception e) {
+                Slog.e(TAG, "Couldn't communicate with slice manager");
+            }
+        }
+    }
+
+}
diff --git a/com/android/server/backup/SystemBackupAgent.java b/com/android/server/backup/SystemBackupAgent.java
new file mode 100644
index 0000000..0a30b76
--- /dev/null
+++ b/com/android/server/backup/SystemBackupAgent.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2009 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 android.app.IWallpaperManager;
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupHelper;
+import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
+import android.app.backup.WallpaperBackupHelper;
+import android.content.Context;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.google.android.collect.Sets;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Backup agent for various system-managed data.  Wallpapers are now handled by a
+ * separate package, but we still process restores from legacy datasets here.
+ */
+public class SystemBackupAgent extends BackupAgentHelper {
+    private static final String TAG = "SystemBackupAgent";
+
+    // Names of the helper tags within the dataset.  Changing one of these names will
+    // break the ability to restore from datasets that predate the change.
+    private static final String WALLPAPER_HELPER = "wallpaper";
+    private static final String SYNC_SETTINGS_HELPER = "account_sync_settings";
+    private static final String PREFERRED_HELPER = "preferred_activities";
+    private static final String NOTIFICATION_HELPER = "notifications";
+    private static final String PERMISSION_HELPER = "permissions";
+    private static final String USAGE_STATS_HELPER = "usage_stats";
+    private static final String SHORTCUT_MANAGER_HELPER = "shortcut_manager";
+    private static final String ACCOUNT_MANAGER_HELPER = "account_manager";
+    private static final String SLICES_HELPER = "slices";
+    private static final String PEOPLE_HELPER = "people";
+
+    // These paths must match what the WallpaperManagerService uses.  The leaf *_FILENAME
+    // are also used in the full-backup file format, so must not change unless steps are
+    // taken to support the legacy backed-up datasets.
+    private static final String WALLPAPER_IMAGE_FILENAME = "wallpaper";
+    private static final String WALLPAPER_INFO_FILENAME = "wallpaper_info.xml";
+
+    // TODO: Will need to change if backing up non-primary user's wallpaper
+    // TODO: http://b/22388012
+    private static final String WALLPAPER_IMAGE_DIR =
+            Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM).getAbsolutePath();
+    public static final String WALLPAPER_IMAGE =
+            new File(Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM),
+                    "wallpaper").getAbsolutePath();
+
+    // TODO: Will need to change if backing up non-primary user's wallpaper
+    // TODO: http://b/22388012
+    private static final String WALLPAPER_INFO_DIR =
+            Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM).getAbsolutePath();
+    public static final String WALLPAPER_INFO =
+            new File(Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM),
+                    "wallpaper_info.xml").getAbsolutePath();
+    // Use old keys to keep legacy data compatibility and avoid writing two wallpapers
+    private static final String WALLPAPER_IMAGE_KEY = WallpaperBackupHelper.WALLPAPER_IMAGE_KEY;
+
+    private static final Set<String> sEligibleForMultiUser = Sets.newArraySet(
+            PERMISSION_HELPER, NOTIFICATION_HELPER, SYNC_SETTINGS_HELPER);
+
+    private int mUserId = UserHandle.USER_SYSTEM;
+
+    @Override
+    public void onCreate(UserHandle user) {
+        super.onCreate(user);
+
+        mUserId = user.getIdentifier();
+
+        addHelper(SYNC_SETTINGS_HELPER, new AccountSyncSettingsBackupHelper(this, mUserId));
+        addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper());
+        addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(mUserId));
+        addHelper(PERMISSION_HELPER, new PermissionBackupHelper(mUserId));
+        addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this));
+        addHelper(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper());
+        addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper());
+        addHelper(SLICES_HELPER, new SliceBackupHelper(this));
+        addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
+    }
+
+    @Override
+    public void onFullBackup(FullBackupDataOutput data) throws IOException {
+        // At present we don't back up anything
+    }
+
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+            throws IOException {
+        // Slot in a restore helper for the older wallpaper backup schema to support restore
+        // from devices still generating data in that format.
+        //TODO(b/147732386): Add multi-display support for wallpaper backup.
+        addHelper(WALLPAPER_HELPER, new WallpaperBackupHelper(this,
+                new String[] { WALLPAPER_IMAGE_KEY}));
+
+        // On restore, we also support a long-ago wallpaper data schema "system_files"
+        addHelper("system_files", new WallpaperBackupHelper(this,
+                new String[] { WALLPAPER_IMAGE_KEY} ));
+
+        super.onRestore(data, appVersionCode, newState);
+    }
+
+    @Override
+    public void addHelper(String keyPrefix, BackupHelper helper) {
+        if (mUserId != UserHandle.USER_SYSTEM && !sEligibleForMultiUser.contains(keyPrefix)) {
+            return;
+        }
+
+        super.addHelper(keyPrefix, helper);
+    }
+
+    /**
+     * Support for 'adb restore' of legacy archives
+     */
+    @Override
+    public void onRestoreFile(ParcelFileDescriptor data, long size,
+            int type, String domain, String path, long mode, long mtime)
+            throws IOException {
+        Slog.i(TAG, "Restoring file domain=" + domain + " path=" + path);
+
+        // Bits to indicate postprocessing we may need to perform
+        boolean restoredWallpaper = false;
+
+        File outFile = null;
+        // Various domain+files we understand a priori
+        if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) {
+            if (path.equals(WALLPAPER_INFO_FILENAME)) {
+                outFile = new File(WALLPAPER_INFO);
+                restoredWallpaper = true;
+            } else if (path.equals(WALLPAPER_IMAGE_FILENAME)) {
+                outFile = new File(WALLPAPER_IMAGE);
+                restoredWallpaper = true;
+            }
+        }
+
+        try {
+            if (outFile == null) {
+                Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]");
+            }
+            FullBackup.restoreFile(data, size, type, mode, mtime, outFile);
+
+            if (restoredWallpaper) {
+                IWallpaperManager wallpaper =
+                        (IWallpaperManager)ServiceManager.getService(
+                                Context.WALLPAPER_SERVICE);
+                if (wallpaper != null) {
+                    try {
+                        wallpaper.settingsRestored();
+                    } catch (RemoteException re) {
+                        Slog.e(TAG, "Couldn't restore settings\n" + re);
+                    }
+                }
+            }
+        } catch (IOException e) {
+            if (restoredWallpaper) {
+                // Make sure we wind up in a good state
+                (new File(WALLPAPER_IMAGE)).delete();
+                (new File(WALLPAPER_INFO)).delete();
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/TransportManager.java b/com/android/server/backup/TransportManager.java
new file mode 100644
index 0000000..30ce4cf
--- /dev/null
+++ b/com/android/server/backup/TransportManager.java
@@ -0,0 +1,727 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupTransport;
+import android.content.ComponentName;
+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.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.util.Preconditions;
+import com.android.server.backup.transport.OnTransportRegisteredListener;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportClientManager;
+import com.android.server.backup.transport.TransportConnectionListener;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.backup.transport.TransportStats;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/** Handles in-memory bookkeeping of all BackupTransport objects. */
+public class TransportManager {
+    private static final String TAG = "BackupTransportManager";
+
+    @VisibleForTesting
+    public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
+
+    private final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
+    private final @UserIdInt int mUserId;
+    private final PackageManager mPackageManager;
+    private final Set<ComponentName> mTransportWhitelist;
+    private final TransportClientManager mTransportClientManager;
+    private final TransportStats mTransportStats;
+    private OnTransportRegisteredListener mOnTransportRegisteredListener = (c, n) -> {};
+
+    /**
+     * Lock for registered transports and currently selected transport.
+     *
+     * <p><b>Warning:</b> No calls to {@link IBackupTransport} or calls that result in transport
+     * code being executed such as {@link TransportClient#connect(String)}} and its variants should
+     * be made with this lock held, risk of deadlock.
+     */
+    private final Object mTransportLock = new Object();
+
+    /** @see #getRegisteredTransportNames() */
+    @GuardedBy("mTransportLock")
+    private final Map<ComponentName, TransportDescription> mRegisteredTransportsDescriptionMap =
+            new ArrayMap<>();
+
+    @GuardedBy("mTransportLock")
+    @Nullable
+    private volatile String mCurrentTransportName;
+
+    TransportManager(@UserIdInt int userId, Context context, Set<ComponentName> whitelist,
+            String selectedTransport) {
+        mUserId = userId;
+        mPackageManager = context.getPackageManager();
+        mTransportWhitelist = Preconditions.checkNotNull(whitelist);
+        mCurrentTransportName = selectedTransport;
+        mTransportStats = new TransportStats();
+        mTransportClientManager = new TransportClientManager(mUserId, context, mTransportStats);
+    }
+
+    @VisibleForTesting
+    TransportManager(
+            @UserIdInt int userId,
+            Context context,
+            Set<ComponentName> whitelist,
+            String selectedTransport,
+            TransportClientManager transportClientManager) {
+        mUserId = userId;
+        mPackageManager = context.getPackageManager();
+        mTransportWhitelist = Preconditions.checkNotNull(whitelist);
+        mCurrentTransportName = selectedTransport;
+        mTransportStats = new TransportStats();
+        mTransportClientManager = transportClientManager;
+    }
+
+    /* Sets a listener to be called whenever a transport is registered. */
+    public void setOnTransportRegisteredListener(OnTransportRegisteredListener listener) {
+        mOnTransportRegisteredListener = listener;
+    }
+
+    @WorkerThread
+    void onPackageAdded(String packageName) {
+        registerTransportsFromPackage(packageName, transportComponent -> true);
+    }
+
+    void onPackageRemoved(String packageName) {
+        synchronized (mTransportLock) {
+            mRegisteredTransportsDescriptionMap.keySet().removeIf(fromPackageFilter(packageName));
+        }
+    }
+
+    @WorkerThread
+    void onPackageChanged(String packageName, String... components) {
+        // Unfortunately this can't be atomic because we risk a deadlock if
+        // registerTransportsFromPackage() is put inside the synchronized block
+        Set<ComponentName> transportComponents = new ArraySet<>(components.length);
+        for (String componentName : components) {
+            transportComponents.add(new ComponentName(packageName, componentName));
+        }
+        synchronized (mTransportLock) {
+            mRegisteredTransportsDescriptionMap.keySet().removeIf(transportComponents::contains);
+        }
+        registerTransportsFromPackage(packageName, transportComponents::contains);
+    }
+
+    /**
+     * Returns the {@link ComponentName}s of the registered transports.
+     *
+     * <p>A *registered* transport is a transport that satisfies intent with action
+     * android.backup.TRANSPORT_HOST, returns true for {@link #isTransportTrusted(ComponentName)}
+     * and that we have successfully connected to once.
+     */
+    ComponentName[] getRegisteredTransportComponents() {
+        synchronized (mTransportLock) {
+            return mRegisteredTransportsDescriptionMap
+                    .keySet()
+                    .toArray(new ComponentName[mRegisteredTransportsDescriptionMap.size()]);
+        }
+    }
+
+    /**
+     * Returns the names of the registered transports.
+     *
+     * @see #getRegisteredTransportComponents()
+     */
+    String[] getRegisteredTransportNames() {
+        synchronized (mTransportLock) {
+            String[] transportNames = new String[mRegisteredTransportsDescriptionMap.size()];
+            int i = 0;
+            for (TransportDescription description : mRegisteredTransportsDescriptionMap.values()) {
+                transportNames[i] = description.name;
+                i++;
+            }
+            return transportNames;
+        }
+    }
+
+    /** Returns a set with the whitelisted transports. */
+    Set<ComponentName> getTransportWhitelist() {
+        return mTransportWhitelist;
+    }
+
+    /** Returns the name of the selected transport or {@code null} if no transport selected. */
+    @Nullable
+    public String getCurrentTransportName() {
+        return mCurrentTransportName;
+    }
+
+    /**
+     * Returns the {@link ComponentName} of the host service of the selected transport or
+     * {@code null} if no transport selected.
+     *
+     * @throws TransportNotRegisteredException if the selected transport is not registered.
+     */
+    @Nullable
+    public ComponentName getCurrentTransportComponent()
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            if (mCurrentTransportName == null) {
+                return null;
+            }
+            return getRegisteredTransportComponentOrThrowLocked(mCurrentTransportName);
+        }
+    }
+
+    /**
+     * Returns the transport name associated with {@code transportComponent}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    public String getTransportName(ComponentName transportComponent)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportComponent).name;
+        }
+    }
+
+    /**
+     * Retrieves the transport dir name of {@code transportComponent}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    public String getTransportDirName(ComponentName transportComponent)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportComponent)
+                    .transportDirName;
+        }
+    }
+
+    /**
+     * Retrieves the transport dir name of {@code transportName}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    public String getTransportDirName(String transportName) throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportName).transportDirName;
+        }
+    }
+
+    /**
+     * Retrieves the configuration intent of {@code transportName}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    @Nullable
+    public Intent getTransportConfigurationIntent(String transportName)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+                    .configurationIntent;
+        }
+    }
+
+    /**
+     * Retrieves the current destination string of {@code transportName}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    public String getTransportCurrentDestinationString(String transportName)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+                    .currentDestinationString;
+        }
+    }
+
+    /**
+     * Retrieves the data management intent of {@code transportName}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    @Nullable
+    public Intent getTransportDataManagementIntent(String transportName)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+                    .dataManagementIntent;
+        }
+    }
+
+    /**
+     * Retrieves the data management label of {@code transportName}.
+     *
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    @Nullable
+    public CharSequence getTransportDataManagementLabel(String transportName)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+                    .dataManagementLabel;
+        }
+    }
+
+    /* Returns true if the transport identified by {@code transportName} is registered. */
+    public boolean isTransportRegistered(String transportName) {
+        synchronized (mTransportLock) {
+            return getRegisteredTransportEntryLocked(transportName) != null;
+        }
+    }
+
+    /**
+     * Execute {@code transportConsumer} for each registered transport passing the transport name.
+     * This is called with an internal lock held, ensuring that the transport will remain registered
+     * while {@code transportConsumer} is being executed. Don't do heavy operations in {@code
+     * transportConsumer}.
+     *
+     * <p><b>Warning:</b> Do NOT make any calls to {@link IBackupTransport} or call any variants of
+     * {@link TransportClient#connect(String)} here, otherwise you risk deadlock.
+     */
+    public void forEachRegisteredTransport(Consumer<String> transportConsumer) {
+        synchronized (mTransportLock) {
+            for (TransportDescription transportDescription :
+                    mRegisteredTransportsDescriptionMap.values()) {
+                transportConsumer.accept(transportDescription.name);
+            }
+        }
+    }
+
+    /**
+     * Updates given values for the transport already registered and identified with {@param
+     * transportComponent}. If the transport is not registered it will log and return.
+     */
+    public void updateTransportAttributes(
+            ComponentName transportComponent,
+            String name,
+            @Nullable Intent configurationIntent,
+            String currentDestinationString,
+            @Nullable Intent dataManagementIntent,
+            @Nullable CharSequence dataManagementLabel) {
+        synchronized (mTransportLock) {
+            TransportDescription description =
+                    mRegisteredTransportsDescriptionMap.get(transportComponent);
+            if (description == null) {
+                Slog.e(TAG, "Transport " + name + " not registered tried to change description");
+                return;
+            }
+            description.name = name;
+            description.configurationIntent = configurationIntent;
+            description.currentDestinationString = currentDestinationString;
+            description.dataManagementIntent = dataManagementIntent;
+            description.dataManagementLabel = dataManagementLabel;
+            Slog.d(TAG, "Transport " + name + " updated its attributes");
+        }
+    }
+
+    @GuardedBy("mTransportLock")
+    private ComponentName getRegisteredTransportComponentOrThrowLocked(String transportName)
+            throws TransportNotRegisteredException {
+        ComponentName transportComponent = getRegisteredTransportComponentLocked(transportName);
+        if (transportComponent == null) {
+            throw new TransportNotRegisteredException(transportName);
+        }
+        return transportComponent;
+    }
+
+    @GuardedBy("mTransportLock")
+    private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
+            ComponentName transportComponent) throws TransportNotRegisteredException {
+        TransportDescription description =
+                mRegisteredTransportsDescriptionMap.get(transportComponent);
+        if (description == null) {
+            throw new TransportNotRegisteredException(transportComponent);
+        }
+        return description;
+    }
+
+    @GuardedBy("mTransportLock")
+    private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
+            String transportName) throws TransportNotRegisteredException {
+        TransportDescription description = getRegisteredTransportDescriptionLocked(transportName);
+        if (description == null) {
+            throw new TransportNotRegisteredException(transportName);
+        }
+        return description;
+    }
+
+    @GuardedBy("mTransportLock")
+    @Nullable
+    private ComponentName getRegisteredTransportComponentLocked(String transportName) {
+        Map.Entry<ComponentName, TransportDescription> entry =
+                getRegisteredTransportEntryLocked(transportName);
+        return (entry == null) ? null : entry.getKey();
+    }
+
+    @GuardedBy("mTransportLock")
+    @Nullable
+    private TransportDescription getRegisteredTransportDescriptionLocked(String transportName) {
+        Map.Entry<ComponentName, TransportDescription> entry =
+                getRegisteredTransportEntryLocked(transportName);
+        return (entry == null) ? null : entry.getValue();
+    }
+
+    @GuardedBy("mTransportLock")
+    @Nullable
+    private Map.Entry<ComponentName, TransportDescription> getRegisteredTransportEntryLocked(
+            String transportName) {
+        for (Map.Entry<ComponentName, TransportDescription> entry :
+                mRegisteredTransportsDescriptionMap.entrySet()) {
+            TransportDescription description = entry.getValue();
+            if (transportName.equals(description.name)) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a {@link TransportClient} for {@code transportName} or {@code null} if not
+     * registered.
+     *
+     * @param transportName The name of the transport.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient} or null if not registered.
+     */
+    @Nullable
+    public TransportClient getTransportClient(String transportName, String caller) {
+        try {
+            return getTransportClientOrThrow(transportName, caller);
+        } catch (TransportNotRegisteredException e) {
+            Slog.w(TAG, "Transport " + transportName + " not registered");
+            return null;
+        }
+    }
+
+    /**
+     * Returns a {@link TransportClient} for {@code transportName} or throws if not registered.
+     *
+     * @param transportName The name of the transport.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient}.
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     */
+    public TransportClient getTransportClientOrThrow(String transportName, String caller)
+            throws TransportNotRegisteredException {
+        synchronized (mTransportLock) {
+            ComponentName component = getRegisteredTransportComponentLocked(transportName);
+            if (component == null) {
+                throw new TransportNotRegisteredException(transportName);
+            }
+            return mTransportClientManager.getTransportClient(component, caller);
+        }
+    }
+
+    /**
+     * Returns a {@link TransportClient} for the current transport or {@code null} if not
+     * registered.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient} or null if not registered.
+     * @throws IllegalStateException if no transport is selected.
+     */
+    @Nullable
+    public TransportClient getCurrentTransportClient(String caller) {
+        if (mCurrentTransportName == null) {
+            throw new IllegalStateException("No transport selected");
+        }
+        synchronized (mTransportLock) {
+            return getTransportClient(mCurrentTransportName, caller);
+        }
+    }
+
+    /**
+     * Returns a {@link TransportClient} for the current transport or throws if not registered.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient}.
+     * @throws TransportNotRegisteredException if the transport is not registered.
+     * @throws IllegalStateException if no transport is selected.
+     */
+    public TransportClient getCurrentTransportClientOrThrow(String caller)
+            throws TransportNotRegisteredException {
+        if (mCurrentTransportName == null) {
+            throw new IllegalStateException("No transport selected");
+        }
+        synchronized (mTransportLock) {
+            return getTransportClientOrThrow(mCurrentTransportName, caller);
+        }
+    }
+
+    /**
+     * Disposes of the {@link TransportClient}.
+     *
+     * @param transportClient The {@link TransportClient} to be disposed of.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     */
+    public void disposeOfTransportClient(TransportClient transportClient, String caller) {
+        mTransportClientManager.disposeOfTransportClient(transportClient, caller);
+    }
+
+    /**
+     * Sets {@code transportName} as selected transport and returns previously selected transport
+     * name. If there was no previous transport it returns null.
+     *
+     * <p>You should NOT call this method in new code. This won't make any checks against {@code
+     * transportName}, putting any operation at risk of a {@link TransportNotRegisteredException} or
+     * another error at the time it's being executed.
+     *
+     * <p>{@link Deprecated} as public, this method can be used as private.
+     */
+    @Deprecated
+    @Nullable
+    String selectTransport(String transportName) {
+        synchronized (mTransportLock) {
+            String prevTransport = mCurrentTransportName;
+            mCurrentTransportName = transportName;
+            return prevTransport;
+        }
+    }
+
+    /**
+     * Tries to register the transport if not registered. If successful also selects the transport.
+     *
+     * @param transportComponent Host of the transport.
+     * @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
+     *     or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
+     */
+    @WorkerThread
+    public int registerAndSelectTransport(ComponentName transportComponent) {
+        // If it's already registered we select and return
+        synchronized (mTransportLock) {
+            try {
+                selectTransport(getTransportName(transportComponent));
+                return BackupManager.SUCCESS;
+            } catch (TransportNotRegisteredException e) {
+                // Fall through and release lock
+            }
+        }
+
+        // We can't call registerTransport() with the transport lock held
+        int result = registerTransport(transportComponent);
+        if (result != BackupManager.SUCCESS) {
+            return result;
+        }
+        synchronized (mTransportLock) {
+            try {
+                selectTransport(getTransportName(transportComponent));
+                return BackupManager.SUCCESS;
+            } catch (TransportNotRegisteredException e) {
+                Slog.wtf(TAG, "Transport got unregistered");
+                return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+            }
+        }
+    }
+
+    @WorkerThread
+    public void registerTransports() {
+        registerTransportsForIntent(mTransportServiceIntent, transportComponent -> true);
+    }
+
+    @WorkerThread
+    private void registerTransportsFromPackage(
+            String packageName, Predicate<ComponentName> transportComponentFilter) {
+        try {
+            mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId);
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Trying to register transports from package not found " + packageName);
+            return;
+        }
+
+        registerTransportsForIntent(
+                new Intent(mTransportServiceIntent).setPackage(packageName),
+                transportComponentFilter.and(fromPackageFilter(packageName)));
+    }
+
+    @WorkerThread
+    private void registerTransportsForIntent(
+            Intent intent, Predicate<ComponentName> transportComponentFilter) {
+        List<ResolveInfo> hosts =
+                mPackageManager.queryIntentServicesAsUser(intent, 0, mUserId);
+        if (hosts == null) {
+            return;
+        }
+        for (ResolveInfo host : hosts) {
+            ComponentName transportComponent = host.serviceInfo.getComponentName();
+            if (transportComponentFilter.test(transportComponent)
+                    && isTransportTrusted(transportComponent)) {
+                registerTransport(transportComponent);
+            }
+        }
+    }
+
+    /** Transport has to be whitelisted and privileged. */
+    private boolean isTransportTrusted(ComponentName transport) {
+        if (!mTransportWhitelist.contains(transport)) {
+            Slog.w(
+                    TAG,
+                    "BackupTransport " + transport.flattenToShortString() + " not whitelisted.");
+            return false;
+        }
+        try {
+            PackageInfo packInfo =
+                    mPackageManager.getPackageInfoAsUser(transport.getPackageName(), 0, mUserId);
+            if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
+                    == 0) {
+                Slog.w(TAG, "Transport package " + transport.getPackageName() + " not privileged");
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "Package not found.", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Tries to register transport represented by {@code transportComponent}.
+     *
+     * <p><b>Warning:</b> Don't call this with the transport lock held.
+     *
+     * @param transportComponent Host of the transport that we want to register.
+     * @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
+     *     or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
+     */
+    @WorkerThread
+    private int registerTransport(ComponentName transportComponent) {
+        checkCanUseTransport();
+
+        if (!isTransportTrusted(transportComponent)) {
+            return BackupManager.ERROR_TRANSPORT_INVALID;
+        }
+
+        String transportString = transportComponent.flattenToShortString();
+        String callerLogString = "TransportManager.registerTransport()";
+
+        Bundle extras = new Bundle();
+        extras.putBoolean(BackupTransport.EXTRA_TRANSPORT_REGISTRATION, true);
+
+        TransportClient transportClient =
+                mTransportClientManager.getTransportClient(
+                        transportComponent, extras, callerLogString);
+        final IBackupTransport transport;
+        try {
+            transport = transportClient.connectOrThrow(callerLogString);
+        } catch (TransportNotAvailableException e) {
+            Slog.e(TAG, "Couldn't connect to transport " + transportString + " for registration");
+            mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
+            return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+        }
+
+        int result;
+        try {
+            String transportName = transport.name();
+            String transportDirName = transport.transportDirName();
+            registerTransport(transportComponent, transport);
+            // If registerTransport() hasn't thrown...
+            Slog.d(TAG, "Transport " + transportString + " registered");
+            mOnTransportRegisteredListener.onTransportRegistered(transportName, transportDirName);
+            result = BackupManager.SUCCESS;
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Transport " + transportString + " died while registering");
+            result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+        }
+
+        mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
+        return result;
+    }
+
+    /** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */
+    private void registerTransport(ComponentName transportComponent, IBackupTransport transport)
+            throws RemoteException {
+        checkCanUseTransport();
+
+        TransportDescription description =
+                new TransportDescription(
+                        transport.name(),
+                        transport.transportDirName(),
+                        transport.configurationIntent(),
+                        transport.currentDestinationString(),
+                        transport.dataManagementIntent(),
+                        transport.dataManagementIntentLabel());
+        synchronized (mTransportLock) {
+            mRegisteredTransportsDescriptionMap.put(transportComponent, description);
+        }
+    }
+
+    private void checkCanUseTransport() {
+        Preconditions.checkState(
+                !Thread.holdsLock(mTransportLock), "Can't call transport with transport lock held");
+    }
+
+    public void dumpTransportClients(PrintWriter pw) {
+        mTransportClientManager.dump(pw);
+    }
+
+    public void dumpTransportStats(PrintWriter pw) {
+        mTransportStats.dump(pw);
+    }
+
+    private static Predicate<ComponentName> fromPackageFilter(String packageName) {
+        return transportComponent -> packageName.equals(transportComponent.getPackageName());
+    }
+
+    private static class TransportDescription {
+        private String name;
+        private final String transportDirName;
+        @Nullable private Intent configurationIntent;
+        private String currentDestinationString;
+        @Nullable private Intent dataManagementIntent;
+        @Nullable private CharSequence dataManagementLabel;
+
+        private TransportDescription(
+                String name,
+                String transportDirName,
+                @Nullable Intent configurationIntent,
+                String currentDestinationString,
+                @Nullable Intent dataManagementIntent,
+                @Nullable CharSequence dataManagementLabel) {
+            this.name = name;
+            this.transportDirName = transportDirName;
+            this.configurationIntent = configurationIntent;
+            this.currentDestinationString = currentDestinationString;
+            this.dataManagementIntent = dataManagementIntent;
+            this.dataManagementLabel = dataManagementLabel;
+        }
+    }
+}
diff --git a/com/android/server/backup/TransportManagerTest.java b/com/android/server/backup/TransportManagerTest.java
new file mode 100644
index 0000000..42115d4
--- /dev/null
+++ b/com/android/server/backup/TransportManagerTest.java
@@ -0,0 +1,735 @@
+/*
+ * 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;
+
+import static com.android.server.backup.testing.TransportData.genericTransport;
+import static com.android.server.backup.testing.TransportTestUtils.mockTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransportsForTransportManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Stream.concat;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupTransport;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils.TransportMock;
+import com.android.server.backup.transport.OnTransportRegisteredListener;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportClientManager;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowApplicationPackageManager.class})
+@Presubmit
+public class TransportManagerTest {
+    private static final String PACKAGE_A = "some.package.a";
+    private static final String PACKAGE_B = "some.package.b";
+
+    @Mock private OnTransportRegisteredListener mListener;
+    @Mock private TransportClientManager mTransportClientManager;
+    private TransportData mTransportA1;
+    private TransportData mTransportA2;
+    private TransportData mTransportB1;
+    private ShadowPackageManager mShadowPackageManager;
+    private @UserIdInt int mUserId;
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mUserId = UserHandle.USER_SYSTEM;
+        mContext = RuntimeEnvironment.application;
+        mShadowPackageManager = shadowOf(mContext.getPackageManager());
+
+        mTransportA1 = genericTransport(PACKAGE_A, "TransportFoo");
+        mTransportA2 = genericTransport(PACKAGE_A, "TransportBar");
+        mTransportB1 = genericTransport(PACKAGE_B, "TransportBaz");
+    }
+
+    /** Reset shadow state. */
+    @After
+    public void tearDown() throws Exception {
+        ShadowApplicationPackageManager.reset();
+    }
+
+    @Test
+    public void testRegisterTransports() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpPackage(PACKAGE_B, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1, mTransportA2, mTransportB1);
+        TransportManager transportManager =
+                createTransportManager(mTransportA1, mTransportA2, mTransportB1);
+
+        transportManager.registerTransports();
+
+        assertRegisteredTransports(
+                transportManager, asList(mTransportA1, mTransportA2, mTransportB1));
+
+        verify(mListener)
+                .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+        verify(mListener)
+                .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
+        verify(mListener)
+                .onTransportRegistered(mTransportB1.transportName, mTransportB1.transportDirName);
+    }
+
+    @Test
+    public void
+            testRegisterTransports_whenOneTransportUnavailable_doesNotRegisterUnavailableTransport()
+                    throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        TransportData transport1 = mTransportA1.unavailable();
+        TransportData transport2 = mTransportA2;
+        setUpTransports(transport1, transport2);
+        TransportManager transportManager = createTransportManager(transport1, transport2);
+
+        transportManager.registerTransports();
+
+        assertRegisteredTransports(transportManager, singletonList(transport2));
+        verify(mListener, never())
+                .onTransportRegistered(transport1.transportName, transport1.transportDirName);
+        verify(mListener)
+                .onTransportRegistered(transport2.transportName, transport2.transportDirName);
+    }
+
+    @Test
+    public void testRegisterTransports_whenWhitelistIsEmpty_doesNotRegisterTransports()
+            throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1, mTransportA2);
+        TransportManager transportManager = createTransportManager(null);
+
+        transportManager.registerTransports();
+
+        assertRegisteredTransports(transportManager, emptyList());
+        verify(mListener, never()).onTransportRegistered(any(), any());
+    }
+
+    @Test
+    public void
+            testRegisterTransports_whenOnlyOneTransportWhitelisted_onlyRegistersWhitelistedTransport()
+                    throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1, mTransportA2);
+        TransportManager transportManager = createTransportManager(null, mTransportA1);
+
+        transportManager.registerTransports();
+
+        assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+        verify(mListener)
+                .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+        verify(mListener, never())
+                .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
+    }
+
+    @Test
+    public void testRegisterTransports_whenAppIsNotPrivileged_doesNotRegisterTransports()
+            throws Exception {
+        // Note ApplicationInfo.PRIVATE_FLAG_PRIVILEGED is missing from flags
+        setUpPackage(PACKAGE_A, 0);
+        setUpTransports(mTransportA1, mTransportA2);
+        TransportManager transportManager =
+                createTransportManager(null, mTransportA1, mTransportA2);
+
+        transportManager.registerTransports();
+
+        assertRegisteredTransports(transportManager, emptyList());
+        verify(mListener, never()).onTransportRegistered(any(), any());
+    }
+
+    @Test
+    public void testRegisterTransports_passesRegistrationExtraToGetTransportClient()
+            throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1);
+        TransportManager transportManager = createTransportManager(mTransportA1);
+
+        transportManager.registerTransports();
+
+        verify(mTransportClientManager)
+                .getTransportClient(
+                        eq(mTransportA1.getTransportComponent()),
+                        argThat(
+                                bundle ->
+                                        bundle.getBoolean(
+                                                BackupTransport.EXTRA_TRANSPORT_REGISTRATION)),
+                        anyString());
+    }
+
+    @Test
+    public void testOnPackageAdded_registerTransports() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1);
+        TransportManager transportManager = createTransportManager(mTransportA1);
+
+        transportManager.onPackageAdded(PACKAGE_A);
+
+        assertRegisteredTransports(transportManager, asList(mTransportA1));
+        verify(mListener)
+                .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+    }
+
+    @Test
+    public void testOnPackageRemoved_unregisterTransports() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportB1);
+
+        transportManager.onPackageRemoved(PACKAGE_A);
+
+        assertRegisteredTransports(transportManager, singletonList(mTransportB1));
+    }
+
+    @Test
+    public void testOnPackageRemoved_whenUnknownPackage_nothingHappens() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+
+        transportManager.onPackageRemoved(PACKAGE_A + "unknown");
+
+        assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+    }
+
+    @Test
+    public void testOnPackageChanged_whenOneComponentChanged_onlyOneTransportReRegistered()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        // Reset listener to verify calls after registerTransports() above
+        reset(mListener);
+
+        transportManager.onPackageChanged(
+                PACKAGE_A, mTransportA1.getTransportComponent().getClassName());
+
+        assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportA2));
+        verify(mListener)
+                .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+        verify(mListener, never())
+                .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
+    }
+
+    @Test
+    public void testOnPackageChanged_whenNoComponentsChanged_doesNotRegisterTransports()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+        reset(mListener);
+
+        transportManager.onPackageChanged(PACKAGE_A);
+
+        assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+        verify(mListener, never()).onTransportRegistered(any(), any());
+    }
+
+    @Test
+    public void testOnPackageChanged_whenUnknownComponentChanged_noTransportsRegistered()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+        reset(mListener);
+
+        transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A + ".UnknownComponent");
+
+        assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+        verify(mListener, never()).onTransportRegistered(any(), any());
+    }
+
+    @Test
+    public void testOnPackageChanged_reRegisterTransports() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        reset(mListener);
+
+        transportManager.onPackageChanged(
+                PACKAGE_A,
+                mTransportA1.getTransportComponent().getClassName(),
+                mTransportA2.getTransportComponent().getClassName());
+
+        assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportA2));
+        verify(mListener)
+                .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+        verify(mListener)
+                .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
+    }
+
+    @Test
+    public void testRegisterAndSelectTransport_whenTransportRegistered() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(null, mTransportA1);
+        ComponentName transportComponent = mTransportA1.getTransportComponent();
+
+        int result = transportManager.registerAndSelectTransport(transportComponent);
+
+        assertThat(result).isEqualTo(BackupManager.SUCCESS);
+        assertThat(transportManager.getRegisteredTransportComponents())
+                .asList()
+                .contains(transportComponent);
+        assertThat(transportManager.getCurrentTransportName())
+                .isEqualTo(mTransportA1.transportName);
+    }
+
+    @Test
+    public void testRegisterAndSelectTransport_whenTransportNotRegistered() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1);
+        TransportManager transportManager = createTransportManager(null, mTransportA1);
+        ComponentName transportComponent = mTransportA1.getTransportComponent();
+
+        int result = transportManager.registerAndSelectTransport(transportComponent);
+
+        assertThat(result).isEqualTo(BackupManager.SUCCESS);
+        assertThat(transportManager.getRegisteredTransportComponents())
+                .asList()
+                .contains(transportComponent);
+        assertThat(transportManager.getTransportDirName(mTransportA1.transportName))
+                .isEqualTo(mTransportA1.transportDirName);
+        assertThat(transportManager.getCurrentTransportName())
+                .isEqualTo(mTransportA1.transportName);
+    }
+
+    @Test
+    public void testGetCurrentTransportName_whenSelectTransportNotCalled_returnsDefaultTransport()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+
+        String currentTransportName = transportManager.getCurrentTransportName();
+
+        assertThat(currentTransportName).isEqualTo(mTransportA1.transportName);
+    }
+
+    @Test
+    public void testGetCurrentTransport_whenSelectTransportCalled_returnsSelectedTransport()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        transportManager.selectTransport(mTransportA2.transportName);
+
+        String currentTransportName = transportManager.getCurrentTransportName();
+
+        assertThat(currentTransportName).isEqualTo(mTransportA2.transportName);
+    }
+
+    @Test
+    public void testGetTransportWhitelist() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1, mTransportA2);
+        TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+
+        Set<ComponentName> transportWhitelist = transportManager.getTransportWhitelist();
+
+        assertThat(transportWhitelist)
+                .containsExactlyElementsIn(
+                        asList(
+                                mTransportA1.getTransportComponent(),
+                                mTransportA2.getTransportComponent()));
+    }
+
+    @Test
+    public void testSelectTransport() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1, mTransportA2);
+        TransportManager transportManager =
+                createTransportManager(null, mTransportA1, mTransportA2);
+
+        String transport1 = transportManager.selectTransport(mTransportA1.transportName);
+        String transport2 = transportManager.selectTransport(mTransportA2.transportName);
+
+        assertThat(transport1).isNull();
+        assertThat(transport2).isEqualTo(mTransportA1.transportName);
+    }
+
+    @Test
+    public void testGetCurrentTransportComponent() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+
+        ComponentName transportComponent = transportManager.getCurrentTransportComponent();
+
+        assertThat(transportComponent).isEqualTo(mTransportA1.getTransportComponent());
+    }
+
+    @Test
+    public void testGetCurrentTransportComponent_whenNoTransportSelected() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(null, mTransportA1);
+
+        ComponentName transportComponent = transportManager.getCurrentTransportComponent();
+
+        assertThat(transportComponent).isNull();
+    }
+
+    @Test
+    public void testGetCurrentTransportComponent_whenTransportNotRegistered() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1.unregistered());
+
+        expectThrows(
+                TransportNotRegisteredException.class,
+                transportManager::getCurrentTransportComponent);
+    }
+
+    @Test
+    public void testGetTransportClient_forRegisteredTransport() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+
+        TransportClient transportClient =
+                transportManager.getTransportClient(mTransportA1.transportName, "caller");
+
+        assertThat(transportClient.getTransportComponent())
+                .isEqualTo(mTransportA1.getTransportComponent());
+    }
+
+    @Test
+    public void testGetTransportClient_forOldNameOfTransportThatChangedName_returnsNull()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        transportManager.updateTransportAttributes(
+                mTransportA1.getTransportComponent(),
+                "newName",
+                null,
+                "destinationString",
+                null,
+                null);
+
+        TransportClient transportClient =
+                transportManager.getTransportClient(mTransportA1.transportName, "caller");
+
+        assertThat(transportClient).isNull();
+    }
+
+    @Test
+    public void testGetTransportClient_forNewNameOfTransportThatChangedName() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        transportManager.updateTransportAttributes(
+                mTransportA1.getTransportComponent(),
+                "newName",
+                null,
+                "destinationString",
+                null,
+                null);
+
+        TransportClient transportClient = transportManager.getTransportClient("newName", "caller");
+
+        assertThat(transportClient.getTransportComponent())
+                .isEqualTo(mTransportA1.getTransportComponent());
+    }
+
+    @Test
+    public void testGetCurrentTransportClient() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+
+        TransportClient transportClient = transportManager.getCurrentTransportClient("caller");
+
+        assertThat(transportClient.getTransportComponent())
+                .isEqualTo(mTransportA1.getTransportComponent());
+    }
+
+    @Test
+    public void testGetCurrentTransportClient_whenNoTransportSelected() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(null, mTransportA1);
+
+        expectThrows(
+                IllegalStateException.class,
+                () -> transportManager.getCurrentTransportClient("caller"));
+    }
+
+    @Test
+    public void testGetCurrentTransportClientOrThrow_whenNoTransportSelected() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(null, mTransportA1);
+
+        expectThrows(
+                IllegalStateException.class,
+                () -> transportManager.getCurrentTransportClientOrThrow("caller"));
+    }
+
+    @Test
+    public void testGetTransportName_forTransportThatChangedName_returnsNewName() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1, mTransportA2);
+        transportManager.updateTransportAttributes(
+                mTransportA1.getTransportComponent(),
+                "newName",
+                null,
+                "destinationString",
+                null,
+                null);
+
+        String transportName =
+                transportManager.getTransportName(mTransportA1.getTransportComponent());
+
+        assertThat(transportName).isEqualTo("newName");
+    }
+
+    @Test
+    public void testIsTransportRegistered() throws Exception {
+        setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        setUpTransports(mTransportA1);
+        TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+        transportManager.registerTransports();
+
+        boolean isTransportA1Registered =
+                transportManager.isTransportRegistered(mTransportA1.transportName);
+        boolean isTransportA2Registered =
+                transportManager.isTransportRegistered(mTransportA2.transportName);
+
+        assertThat(isTransportA1Registered).isTrue();
+        assertThat(isTransportA2Registered).isFalse();
+    }
+
+    @Test
+    public void testGetTransportAttributes_forRegisteredTransport_returnsCorrectValues()
+            throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+
+        Intent configurationIntent =
+                transportManager.getTransportConfigurationIntent(mTransportA1.transportName);
+        String destinationString =
+                transportManager.getTransportCurrentDestinationString(mTransportA1.transportName);
+        Intent dataManagementIntent =
+                transportManager.getTransportDataManagementIntent(mTransportA1.transportName);
+        CharSequence dataManagementLabel =
+                transportManager.getTransportDataManagementLabel(mTransportA1.transportName);
+        String transportDirName = transportManager.getTransportDirName(mTransportA1.transportName);
+        String transportDirNameByComponent =
+                transportManager.getTransportDirName(mTransportA1.getTransportComponent());
+
+        assertThat(configurationIntent).isEqualTo(mTransportA1.configurationIntent);
+        assertThat(destinationString).isEqualTo(mTransportA1.currentDestinationString);
+        assertThat(dataManagementIntent).isEqualTo(mTransportA1.dataManagementIntent);
+        assertThat(dataManagementLabel).isEqualTo(mTransportA1.dataManagementLabel);
+        assertThat(transportDirName).isEqualTo(mTransportA1.transportDirName);
+        assertThat(transportDirNameByComponent).isEqualTo(mTransportA1.transportDirName);
+    }
+
+    @Test
+    public void testGetTransportAttributes_forUnregisteredTransport_throws() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(mTransportA1);
+
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () -> transportManager.getTransportConfigurationIntent(mTransportA2.transportName));
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () ->
+                        transportManager.getTransportCurrentDestinationString(
+                                mTransportA2.transportName));
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () ->
+                        transportManager.getTransportDataManagementIntent(
+                                mTransportA2.transportName));
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () -> transportManager.getTransportDataManagementLabel(mTransportA2.transportName));
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () -> transportManager.getTransportDirName(mTransportA2.transportName));
+        expectThrows(
+                TransportNotRegisteredException.class,
+                () -> transportManager.getTransportDirName(mTransportA2.getTransportComponent()));
+    }
+
+    @Test
+    public void testGetRegisteredTransportNames() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(
+                        mTransportA1, mTransportA2, mTransportB1);
+
+        String[] transportNames = transportManager.getRegisteredTransportNames();
+
+        assertThat(transportNames)
+                .asList()
+                .containsExactlyElementsIn(
+                        Stream.of(mTransportA1, mTransportA2, mTransportB1)
+                                .map(transportData -> transportData.transportName)
+                                .collect(toList()));
+    }
+
+    @Test
+    public void testGetRegisteredTransportComponents() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(
+                        mTransportA1, mTransportA2, mTransportB1);
+
+        ComponentName[] transportNames = transportManager.getRegisteredTransportComponents();
+
+        assertThat(transportNames)
+                .asList()
+                .containsExactlyElementsIn(
+                        Stream.of(mTransportA1, mTransportA2, mTransportB1)
+                                .map(TransportData::getTransportComponent)
+                                .collect(toList()));
+    }
+
+    @Test
+    public void testForEachRegisteredTransport() throws Exception {
+        TransportManager transportManager =
+                createTransportManagerWithRegisteredTransports(
+                        mTransportA1, mTransportA2, mTransportB1);
+        List<String> calls = new ArrayList<>();
+
+        transportManager.forEachRegisteredTransport(calls::add);
+
+        assertThat(calls)
+                .containsExactly(
+                        mTransportA1.transportName,
+                        mTransportA2.transportName,
+                        mTransportB1.transportName);
+    }
+
+    @Test
+    public void testForEachRegisteredTransport_whenNoTransportsRegistered() throws Exception {
+        TransportManager transportManager = createTransportManagerWithRegisteredTransports(null);
+        List<String> calls = new ArrayList<>();
+
+        transportManager.forEachRegisteredTransport(calls::add);
+
+        assertThat(calls).isEmpty();
+    }
+
+    private List<TransportMock> setUpTransports(TransportData... transports) throws Exception {
+        setUpTransportsForTransportManager(mShadowPackageManager, transports);
+        List<TransportMock> transportMocks = new ArrayList<>(transports.length);
+        for (TransportData transport : transports) {
+            TransportMock transportMock = mockTransport(transport);
+            when(mTransportClientManager.getTransportClient(
+                            eq(transport.getTransportComponent()), any()))
+                    .thenReturn(transportMock.transportClient);
+            when(mTransportClientManager.getTransportClient(
+                            eq(transport.getTransportComponent()), any(), any()))
+                    .thenReturn(transportMock.transportClient);
+            transportMocks.add(transportMock);
+        }
+        return transportMocks;
+    }
+
+    private void setUpPackage(String packageName, int flags) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.privateFlags = flags;
+        mShadowPackageManager.installPackage(packageInfo);
+        ShadowApplicationPackageManager.addInstalledPackage(packageName, packageInfo);
+    }
+
+    private TransportManager createTransportManagerWithRegisteredTransports(
+            @Nullable TransportData selectedTransport, TransportData... transports)
+            throws Exception {
+        Set<TransportData> transportSet = merge(selectedTransport, transports);
+        for (TransportData transport : transportSet) {
+            setUpPackage(
+                    transport.getTransportComponent().getPackageName(),
+                    ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+        }
+        setUpTransports(transportSet.toArray(new TransportData[0]));
+        TransportManager transportManager = createTransportManager(selectedTransport, transports);
+        transportManager.registerTransports();
+        return transportManager;
+    }
+
+    private TransportManager createTransportManager(
+            @Nullable TransportData selectedTransport, TransportData... transports) {
+        TransportManager transportManager =
+                new TransportManager(
+                        mUserId,
+                        mContext,
+                        merge(selectedTransport, transports)
+                                .stream()
+                                .map(TransportData::getTransportComponent)
+                                .collect(toSet()),
+                        selectedTransport != null ? selectedTransport.transportName : null,
+                        mTransportClientManager);
+        transportManager.setOnTransportRegisteredListener(mListener);
+        return transportManager;
+    }
+
+    private Set<TransportData> merge(
+            @Nullable TransportData selectedTransport, TransportData... transports) {
+        return concat(Stream.of(selectedTransport), Stream.of(transports))
+                .filter(Objects::nonNull)
+                .collect(toSet());
+    }
+
+    private void assertRegisteredTransports(
+            TransportManager transportManager, List<TransportData> transports) {
+        assertThat(transportManager.getRegisteredTransportComponents())
+                .asList()
+                .containsExactlyElementsIn(
+                        transports
+                                .stream()
+                                .map(TransportData::getTransportComponent)
+                                .collect(toList()));
+        assertThat(transportManager.getRegisteredTransportNames())
+                .asList()
+                .containsExactlyElementsIn(
+                        transports.stream().map(t -> t.transportName).collect(toList()));
+    }
+}
diff --git a/com/android/server/backup/UsageStatsBackupHelper.java b/com/android/server/backup/UsageStatsBackupHelper.java
new file mode 100644
index 0000000..d6a70d3
--- /dev/null
+++ b/com/android/server/backup/UsageStatsBackupHelper.java
@@ -0,0 +1,69 @@
+package com.android.server.backup;
+
+
+import android.app.backup.BlobBackupHelper;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.Context;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.server.LocalServices;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public class UsageStatsBackupHelper extends BlobBackupHelper {
+    static final String TAG = "UsgStatsBackupHelper";   // must be < 23 chars
+    static final boolean DEBUG = false;
+
+    // Current version of the blob schema
+    static final int BLOB_VERSION = 1;
+
+    // Key under which the payload blob is stored
+    // same as UsageStatsDatabase.KEY_USAGE_STATS
+    static final String KEY_USAGE_STATS = "usage_stats";
+
+    public UsageStatsBackupHelper(Context context) {
+        super(BLOB_VERSION, KEY_USAGE_STATS);
+    }
+
+    @Override
+    protected byte[] getBackupPayload(String key) {
+        if (KEY_USAGE_STATS.equals(key)) {
+            UsageStatsManagerInternal localUsageStatsManager =
+                    LocalServices.getService(UsageStatsManagerInternal.class);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            DataOutputStream out  = new DataOutputStream(baos);
+            try {
+                out.writeInt(UserHandle.USER_SYSTEM);
+                out.write(localUsageStatsManager.getBackupPayload(UserHandle.USER_SYSTEM, key));
+            } catch (IOException ioe) {
+                if (DEBUG) Log.e(TAG, "Failed to backup Usage Stats", ioe);
+                baos.reset();
+            }
+            return baos.toByteArray();
+        }
+        return null;
+    }
+
+
+    @Override
+    protected void applyRestoredPayload(String key, byte[] payload)  {
+        if (KEY_USAGE_STATS.equals(key)) {
+            UsageStatsManagerInternal localUsageStatsManager =
+                    LocalServices.getService(UsageStatsManagerInternal.class);
+            DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
+            try {
+                int user = in.readInt();
+                byte[] restoreData = new byte[payload.length - 4];
+                in.read(restoreData, 0, restoreData.length);
+                localUsageStatsManager.applyRestoredPayload(user, key, restoreData);
+            } catch (IOException ioe) {
+                if (DEBUG) Log.e(TAG, "Failed to restore Usage Stats", ioe);
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/UserBackupManagerFilePersistedSettings.java b/com/android/server/backup/UserBackupManagerFilePersistedSettings.java
new file mode 100644
index 0000000..205b7dd
--- /dev/null
+++ b/com/android/server/backup/UserBackupManagerFilePersistedSettings.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.android.server.backup.BackupManagerService.TAG;
+
+import android.util.Slog;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** User settings which are persisted across reboots. */
+final class UserBackupManagerFilePersistedSettings {
+    // File containing backup-enabled state. Contains a single byte to denote enabled status.
+    // Nonzero is enabled; file missing or a zero byte is disabled.
+    private static final String BACKUP_ENABLE_FILE = "backup_enabled";
+
+    static boolean readBackupEnableState(int userId) {
+        boolean enabled = readBackupEnableState(UserBackupManagerFiles.getBaseStateDir(userId));
+        Slog.d(TAG, "user:" + userId + " readBackupEnableState enabled:" + enabled);
+        return enabled;
+    }
+
+    static void writeBackupEnableState(int userId, boolean enable) {
+        Slog.d(TAG, "user:" + userId + " writeBackupEnableState enable:" + enable);
+        writeBackupEnableState(UserBackupManagerFiles.getBaseStateDir(userId), enable);
+    }
+
+    private static boolean readBackupEnableState(File baseDir) {
+        File enableFile = new File(baseDir, BACKUP_ENABLE_FILE);
+        if (enableFile.exists()) {
+            try (FileInputStream fin = new FileInputStream(enableFile)) {
+                int state = fin.read();
+                if (state != 0 && state != 1) {
+                    // TODO (b/148587496) handle instead of only logging
+                    Slog.e(TAG, "Unexpected enabled state:" + state);
+                }
+                return state != 0;
+            } catch (IOException e) {
+                // can't read the file; fall through to assume disabled
+                Slog.e(TAG, "Cannot read enable state; assuming disabled");
+            }
+        } else {
+            Slog.i(TAG, "isBackupEnabled() => false due to absent settings file");
+        }
+        return false;
+    }
+
+    private static void writeBackupEnableState(File baseDir, boolean enable) {
+        File enableFile = new File(baseDir, BACKUP_ENABLE_FILE);
+        File stage = new File(baseDir, BACKUP_ENABLE_FILE + "-stage");
+        try (FileOutputStream fout = new FileOutputStream(stage)) {
+            fout.write(enable ? 1 : 0);
+            fout.close();
+            boolean renamed = stage.renameTo(enableFile);
+            if (!renamed) {
+                // TODO (b/148587496) handle instead of only logging
+                Slog.e(TAG, "Write enable failed as could not rename staging file to actual");
+            }
+            // will be synced immediately by the try-with-resources call to close()
+        } catch (IOException | RuntimeException e) {
+            Slog.e(
+                    TAG,
+                    "Unable to record backup enable state; reverting to disabled: "
+                            + e.getMessage());
+            enableFile.delete();
+            stage.delete();
+        }
+    }
+}
diff --git a/com/android/server/backup/UserBackupManagerFiles.java b/com/android/server/backup/UserBackupManagerFiles.java
new file mode 100644
index 0000000..8e60542
--- /dev/null
+++ b/com/android/server/backup/UserBackupManagerFiles.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.os.Environment;
+import android.os.UserHandle;
+
+import java.io.File;
+
+/** Directories used for user specific backup/restore persistent state and book-keeping. */
+final class UserBackupManagerFiles {
+    // Name of the directories the service stores bookkeeping data under.
+    private static final String BACKUP_PERSISTENT_DIR = "backup";
+    private static final String BACKUP_STAGING_DIR = "backup_stage";
+
+    private static File getBaseDir(int userId) {
+        return Environment.getDataSystemCeDirectory(userId);
+    }
+
+    static File getBaseStateDir(int userId) {
+        if (userId != UserHandle.USER_SYSTEM) {
+            return new File(getBaseDir(userId), BACKUP_PERSISTENT_DIR);
+        }
+        // TODO (b/120424138) remove if clause above and use same logic for system user.
+        // simultaneously, copy below dir to new system user dir
+        return new File(Environment.getDataDirectory(), BACKUP_PERSISTENT_DIR);
+    }
+
+    static File getDataDir(int userId) {
+        if (userId != UserHandle.USER_SYSTEM) {
+            return new File(getBaseDir(userId), BACKUP_STAGING_DIR);
+        }
+        // TODO (b/120424138) remove if clause above and use same logic for system user. Since this
+        // is a staging dir, we dont need to copy below dir to new system user dir
+        return new File(Environment.getDownloadCacheDirectory(), BACKUP_STAGING_DIR);
+    }
+
+    /** A user specific dir within the system user's directory. */
+    static File getStateDirInSystemDir(int userId) {
+        return new File(getBaseStateDir(UserHandle.USER_SYSTEM), "" + userId);
+    }
+
+    /** Stored in a user specific dir within the system user's directory. */
+    static File getStateFileInSystemDir(String filename, int userId) {
+        return new File(getStateDirInSystemDir(userId), filename);
+    }
+}
diff --git a/com/android/server/backup/UserBackupManagerService.java b/com/android/server/backup/UserBackupManagerService.java
new file mode 100644
index 0000000..dc35c77
--- /dev/null
+++ b/com/android/server/backup/UserBackupManagerService.java
@@ -0,0 +1,4231 @@
+/*
+ * 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;
+
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_OPERATION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_FULL_CONFIRMATION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_OP_COMPLETE;
+import static com.android.server.backup.internal.BackupHandler.MSG_REQUEST_BACKUP;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_CLEAR;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_BACKUP;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_RESTORE;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_BACKUP;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_CLEAR;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
+import static com.android.server.backup.internal.BackupHandler.MSG_SCHEDULE_BACKUP_PACKAGE;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
+import android.app.AppGlobals;
+import android.app.IActivityManager;
+import android.app.IBackupAgent;
+import android.app.PendingIntent;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.FullBackup;
+import android.app.backup.IBackupManager;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.app.backup.IFullBackupRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.PowerManager;
+import android.os.PowerManager.ServiceType;
+import android.os.PowerSaveState;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SELinux;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.os.storage.IStorageManager;
+import android.os.storage.StorageManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.EventLog;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.util.Preconditions;
+import com.android.server.AppWidgetBackupBridge;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+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.ClearDataObserver;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.internal.Operation;
+import com.android.server.backup.internal.PerformInitializeTask;
+import com.android.server.backup.internal.RunInitializeReceiver;
+import com.android.server.backup.internal.SetupObserver;
+import com.android.server.backup.keyvalue.BackupRequest;
+import com.android.server.backup.params.AdbBackupParams;
+import com.android.server.backup.params.AdbParams;
+import com.android.server.backup.params.AdbRestoreParams;
+import com.android.server.backup.params.BackupParams;
+import com.android.server.backup.params.ClearParams;
+import com.android.server.backup.params.ClearRetryParams;
+import com.android.server.backup.params.RestoreParams;
+import com.android.server.backup.restore.ActiveRestoreSession;
+import com.android.server.backup.restore.PerformUnifiedRestoreTask;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.backup.utils.AppBackupUtils;
+import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupObserverUtils;
+import com.android.server.backup.utils.FileUtils;
+import com.android.server.backup.utils.SparseArrayUtils;
+
+import com.google.android.collect.Sets;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** System service that performs backup/restore operations. */
+public class UserBackupManagerService {
+    /**
+     * Wrapper over {@link PowerManager.WakeLock} to prevent double-free exceptions on release()
+     * after quit().
+     */
+    public static class BackupWakeLock {
+        private final PowerManager.WakeLock mPowerManagerWakeLock;
+        private boolean mHasQuit = false;
+        private int mUserId;
+
+        public BackupWakeLock(PowerManager.WakeLock powerManagerWakeLock, int userId) {
+            mPowerManagerWakeLock = powerManagerWakeLock;
+            mUserId = userId;
+        }
+
+        /** Acquires the {@link PowerManager.WakeLock} if hasn't been quit. */
+        public synchronized void acquire() {
+            if (mHasQuit) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Ignore wakelock acquire after quit: "
+                                        + mPowerManagerWakeLock.getTag()));
+                return;
+            }
+            mPowerManagerWakeLock.acquire();
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Acquired wakelock:" + mPowerManagerWakeLock.getTag()));
+        }
+
+        /** Releases the {@link PowerManager.WakeLock} if hasn't been quit. */
+        public synchronized void release() {
+            if (mHasQuit) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Ignore wakelock release after quit: "
+                                        + mPowerManagerWakeLock.getTag()));
+                return;
+            }
+            mPowerManagerWakeLock.release();
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Released wakelock:" + mPowerManagerWakeLock.getTag()));
+        }
+
+        /**
+         * Returns true if the {@link PowerManager.WakeLock} has been acquired but not yet released.
+         */
+        public synchronized boolean isHeld() {
+            return mPowerManagerWakeLock.isHeld();
+        }
+
+        /** Release the {@link PowerManager.WakeLock} till it isn't held. */
+        public synchronized void quit() {
+            while (mPowerManagerWakeLock.isHeld()) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Releasing wakelock: " + mPowerManagerWakeLock.getTag()));
+                mPowerManagerWakeLock.release();
+            }
+            mHasQuit = true;
+        }
+    }
+
+    // Persistently track the need to do a full init.
+    private static final String INIT_SENTINEL_FILE_NAME = "_need_init_";
+
+    // System-private key used for backing up an app's widget state.  Must
+    // begin with U+FFxx by convention (we reserve all keys starting
+    // with U+FF00 or higher for system use).
+    public static final String KEY_WIDGET_STATE = "\uffed\uffedwidget";
+
+    // Name and current contents version of the full-backup manifest file
+    //
+    // Manifest version history:
+    //
+    // 1 : initial release
+    public static final String BACKUP_MANIFEST_FILENAME = "_manifest";
+    public static final int BACKUP_MANIFEST_VERSION = 1;
+
+    // External archive format version history:
+    //
+    // 1 : initial release
+    // 2 : no format change per se; version bump to facilitate PBKDF2 version skew detection
+    // 3 : introduced "_meta" metadata file; no other format change per se
+    // 4 : added support for new device-encrypted storage locations
+    // 5 : added support for key-value packages
+    public static final int BACKUP_FILE_VERSION = 5;
+    public static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
+    public static final String BACKUP_METADATA_FILENAME = "_meta";
+    public static final int BACKUP_METADATA_VERSION = 1;
+    public static final int BACKUP_WIDGET_METADATA_TOKEN = 0x01FFED01;
+
+    private static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1;
+
+    // Round-robin queue for scheduling full backup passes.
+    private static final int SCHEDULE_FILE_VERSION = 1;
+
+    public static final String SETTINGS_PACKAGE = "com.android.providers.settings";
+    public static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup";
+
+    // Pseudoname that we use for the Package Manager metadata "package".
+    public static final String PACKAGE_MANAGER_SENTINEL = "@pm@";
+
+    // Retry interval for clear/init when the transport is unavailable
+    private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR;
+
+    public static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT";
+    private static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED";
+    private static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName";
+
+    // Bookkeeping of in-flight operations. The operation token is the index of the entry in the
+    // pending operations list.
+    public static final int OP_PENDING = 0;
+    private static final int OP_ACKNOWLEDGED = 1;
+    private static final int OP_TIMEOUT = -1;
+
+    // Waiting for backup agent to respond during backup operation.
+    public static final int OP_TYPE_BACKUP_WAIT = 0;
+
+    // Waiting for backup agent to respond during restore operation.
+    public static final int OP_TYPE_RESTORE_WAIT = 1;
+
+    // An entire backup operation spanning multiple packages.
+    public static final int OP_TYPE_BACKUP = 2;
+
+    // Time delay for initialization operations that can be delayed so as not to consume too much
+    // CPU on bring-up and increase time-to-UI.
+    private static final long INITIALIZATION_DELAY_MILLIS = 3000;
+
+    // Timeout interval for deciding that a bind has taken too long.
+    private static final long BIND_TIMEOUT_INTERVAL = 10 * 1000;
+    // Timeout interval for deciding that a clear-data has taken too long.
+    private static final long CLEAR_DATA_TIMEOUT_INTERVAL = 30 * 1000;
+
+    // User confirmation timeout for a full backup/restore operation.  It's this long in
+    // order to give them time to enter the backup password.
+    private static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000;
+
+    // If an app is busy when we want to do a full-data backup, how long to defer the retry.
+    // This is fuzzed, so there are two parameters; backoff_min + Rand[0, backoff_fuzz)
+    private static final long BUSY_BACKOFF_MIN_MILLIS = 1000 * 60 * 60;  // one hour
+    private static final int BUSY_BACKOFF_FUZZ = 1000 * 60 * 60 * 2;  // two hours
+
+    private static final String SERIAL_ID_FILE = "serial_id";
+
+    private static final String SKIP_USER_FACING_DATA = "backup_skip_user_facing_data";
+    private static final String WALLPAPER_PACKAGE = "com.android.wallpaperbackup";
+
+    private final @UserIdInt int mUserId;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+    private final TransportManager mTransportManager;
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final IPackageManager mPackageManagerBinder;
+    private final IActivityManager mActivityManager;
+    private final ActivityManagerInternal mActivityManagerInternal;
+    private PowerManager mPowerManager;
+    private final AlarmManager mAlarmManager;
+    private final IStorageManager mStorageManager;
+    private final BackupManagerConstants mConstants;
+    private final BackupWakeLock mWakelock;
+    private final BackupHandler mBackupHandler;
+
+    private final IBackupManager mBackupManagerBinder;
+
+    private boolean mEnabled;   // access to this is synchronized on 'this'
+    private boolean mSetupComplete;
+    private boolean mAutoRestore;
+
+    private final PendingIntent mRunInitIntent;
+
+    private final ArraySet<String> mPendingInits = new ArraySet<>();  // transport names
+
+    // map UIDs to the set of participating packages under that UID
+    private final SparseArray<HashSet<String>> mBackupParticipants = new SparseArray<>();
+
+    // Backups that we haven't started yet.  Keys are package names.
+    private final HashMap<String, BackupRequest> mPendingBackups = new HashMap<>();
+
+    // locking around the pending-backup management
+    private final Object mQueueLock = new Object();
+
+    private final UserBackupPreferences mBackupPreferences;
+
+    // The thread performing the sequence of queued backups binds to each app's agent
+    // in succession.  Bind notifications are asynchronously delivered through the
+    // Activity Manager; use this lock object to signal when a requested binding has
+    // completed.
+    private final Object mAgentConnectLock = new Object();
+    private IBackupAgent mConnectedAgent;
+    private volatile boolean mConnecting;
+
+    private volatile boolean mBackupRunning;
+    private volatile long mLastBackupPass;
+
+    // A similar synchronization mechanism around clearing apps' data for restore
+    private final Object mClearDataLock = new Object();
+    private volatile boolean mClearingData;
+
+    // Used by ADB.
+    private final BackupPasswordManager mBackupPasswordManager;
+    private final SparseArray<AdbParams> mAdbBackupRestoreConfirmations = new SparseArray<>();
+    private final SecureRandom mRng = new SecureRandom();
+
+    // Time when we post the transport registration operation
+    private final long mRegisterTransportsRequestedTime;
+
+    @GuardedBy("mQueueLock")
+    private PerformFullTransportBackupTask mRunningFullBackupTask;
+
+    @GuardedBy("mQueueLock")
+    private ArrayList<FullBackupEntry> mFullBackupQueue;
+
+    @GuardedBy("mPendingRestores")
+    private boolean mIsRestoreInProgress;
+
+    @GuardedBy("mPendingRestores")
+    private final Queue<PerformUnifiedRestoreTask> mPendingRestores = new ArrayDeque<>();
+
+    private ActiveRestoreSession mActiveRestoreSession;
+
+    /**
+     * mCurrentOperations contains the list of currently active operations.
+     *
+     * If type of operation is OP_TYPE_WAIT, it are waiting for an ack or timeout.
+     * An operation wraps a BackupRestoreTask within it.
+     * It's the responsibility of this task to remove the operation from this array.
+     *
+     * A BackupRestore task gets notified of ack/timeout for the operation via
+     * BackupRestoreTask#handleCancel, BackupRestoreTask#operationComplete and notifyAll called
+     * on the mCurrentOpLock.
+     * {@link UserBackupManagerService#waitUntilOperationComplete(int)} is
+     * used in various places to 'wait' for notifyAll and detect change of pending state of an
+     * operation. So typically, an operation will be removed from this array by:
+     * - BackupRestoreTask#handleCancel and
+     * - BackupRestoreTask#operationComplete OR waitUntilOperationComplete. Do not remove at both
+     * these places because waitUntilOperationComplete relies on the operation being present to
+     * determine its completion status.
+     *
+     * If type of operation is OP_BACKUP, it is a task running backups. It provides a handle to
+     * cancel backup tasks.
+     */
+    @GuardedBy("mCurrentOpLock")
+    private final SparseArray<Operation> mCurrentOperations = new SparseArray<>();
+    private final Object mCurrentOpLock = new Object();
+    private final Random mTokenGenerator = new Random();
+    private final AtomicInteger mNextToken = new AtomicInteger();
+
+    // Where we keep our journal files and other bookkeeping.
+    private final File mBaseStateDir;
+    private final File mDataDir;
+    private final File mJournalDir;
+    @Nullable
+    private DataChangedJournal mJournal;
+    private final File mFullBackupScheduleFile;
+
+    // Keep a log of all the apps we've ever backed up.
+    private ProcessedPackagesJournal mProcessedPackagesJournal;
+
+    private File mTokenFile;
+    private Set<String> mAncestralPackages = null;
+    private long mAncestralToken = 0;
+    private long mCurrentToken = 0;
+    @Nullable private File mAncestralSerialNumberFile;
+
+    private final ContentObserver mSetupObserver;
+    private final BroadcastReceiver mRunInitReceiver;
+
+    /**
+     * Creates an instance of {@link UserBackupManagerService} and initializes state for it. This
+     * includes setting up the directories where we keep our bookkeeping and transport management.
+     *
+     * @see #createAndInitializeService(int, Context, BackupManagerService, HandlerThread, File,
+     * File, TransportManager)
+     */
+    static UserBackupManagerService createAndInitializeService(
+            @UserIdInt int userId,
+            Context context,
+            BackupManagerService backupManagerService,
+            Set<ComponentName> transportWhitelist) {
+        String currentTransport =
+                Settings.Secure.getStringForUser(
+                        context.getContentResolver(), Settings.Secure.BACKUP_TRANSPORT, userId);
+        if (TextUtils.isEmpty(currentTransport)) {
+            currentTransport = null;
+        }
+
+        if (DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(userId, "Starting with transport " + currentTransport));
+        }
+        TransportManager transportManager =
+                new TransportManager(userId, context, transportWhitelist, currentTransport);
+
+        File baseStateDir = UserBackupManagerFiles.getBaseStateDir(userId);
+        File dataDir = UserBackupManagerFiles.getDataDir(userId);
+
+        HandlerThread userBackupThread =
+                new HandlerThread("backup-" + userId, Process.THREAD_PRIORITY_BACKGROUND);
+        userBackupThread.start();
+        if (DEBUG) {
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(userId, "Started thread " + userBackupThread.getName()));
+        }
+
+        return createAndInitializeService(
+                userId,
+                context,
+                backupManagerService,
+                userBackupThread,
+                baseStateDir,
+                dataDir,
+                transportManager);
+    }
+
+    /**
+     * Creates an instance of {@link UserBackupManagerService}.
+     *
+     * @param userId The user which this service is for.
+     * @param context The system server context.
+     * @param backupManagerService A reference to the proxy to {@link BackupManagerService}.
+     * @param userBackupThread The thread running backup/restore operations for the user.
+     * @param baseStateDir The directory we store the user's persistent bookkeeping data.
+     * @param dataDir The directory we store the user's temporary staging data.
+     * @param transportManager The {@link TransportManager} responsible for handling the user's
+     *     transports.
+     */
+    @VisibleForTesting
+    public static UserBackupManagerService createAndInitializeService(
+            @UserIdInt int userId,
+            Context context,
+            BackupManagerService backupManagerService,
+            HandlerThread userBackupThread,
+            File baseStateDir,
+            File dataDir,
+            TransportManager transportManager) {
+        return new UserBackupManagerService(
+                userId,
+                context,
+                backupManagerService,
+                userBackupThread,
+                baseStateDir,
+                dataDir,
+                transportManager);
+    }
+
+    /**
+     * Returns the value of {@link Settings.Secure#USER_SETUP_COMPLETE} for the specified user
+     * {@code userId} as a {@code boolean}.
+     */
+    public static boolean getSetupCompleteSettingForUser(Context context, int userId) {
+        return Settings.Secure.getIntForUser(
+                context.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE,
+                0,
+                userId)
+                != 0;
+    }
+
+    private UserBackupManagerService(
+            @UserIdInt int userId,
+            Context context,
+            BackupManagerService parent,
+            HandlerThread userBackupThread,
+            File baseStateDir,
+            File dataDir,
+            TransportManager transportManager) {
+        mUserId = userId;
+        mContext = Objects.requireNonNull(context, "context cannot be null");
+        mPackageManager = context.getPackageManager();
+        mPackageManagerBinder = AppGlobals.getPackageManager();
+        mActivityManager = ActivityManager.getService();
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
+
+        Objects.requireNonNull(parent, "parent cannot be null");
+        mBackupManagerBinder = BackupManagerService.asInterface(parent.asBinder());
+
+        mAgentTimeoutParameters = new
+                BackupAgentTimeoutParameters(Handler.getMain(), mContext.getContentResolver());
+        mAgentTimeoutParameters.start();
+
+        Objects.requireNonNull(userBackupThread, "userBackupThread cannot be null");
+        mBackupHandler = new BackupHandler(this, userBackupThread);
+
+        // Set up our bookkeeping
+        final ContentResolver resolver = context.getContentResolver();
+        mSetupComplete = getSetupCompleteSettingForUser(context, userId);
+        mAutoRestore = Settings.Secure.getIntForUser(resolver,
+                Settings.Secure.BACKUP_AUTO_RESTORE, 1, userId) != 0;
+
+        mSetupObserver = new SetupObserver(this, mBackupHandler);
+        resolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
+                /* notifyForDescendents */ false,
+                mSetupObserver,
+                mUserId);
+
+        mBaseStateDir = Objects.requireNonNull(baseStateDir, "baseStateDir cannot be null");
+        // TODO (b/120424138): Remove once the system user is migrated to use the per-user CE
+        // directory. Per-user CE directories are managed by vold.
+        if (userId == UserHandle.USER_SYSTEM) {
+            mBaseStateDir.mkdirs();
+            if (!SELinux.restorecon(mBaseStateDir)) {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                userId, "SELinux restorecon failed on " + mBaseStateDir));
+            }
+        }
+
+        // TODO (b/120424138): The system user currently uses the cache which is managed by init.rc
+        // Initialization and restorecon is managed by vold for per-user CE directories.
+        mDataDir = Objects.requireNonNull(dataDir, "dataDir cannot be null");
+        mBackupPasswordManager = new BackupPasswordManager(mContext, mBaseStateDir, mRng);
+
+        // Receiver for transport initialization.
+        mRunInitReceiver = new RunInitializeReceiver(this);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(RUN_INITIALIZE_ACTION);
+        context.registerReceiverAsUser(
+                mRunInitReceiver,
+                UserHandle.of(userId),
+                filter,
+                android.Manifest.permission.BACKUP,
+                /* scheduler */ null);
+
+        Intent initIntent = new Intent(RUN_INITIALIZE_ACTION);
+        initIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+        mRunInitIntent =
+                PendingIntent.getBroadcastAsUser(
+                        context,
+                        /* requestCode */ 0,
+                        initIntent,
+                        /* flags */ 0,
+                        UserHandle.of(userId));
+
+        // Set up the backup-request journaling
+        mJournalDir = new File(mBaseStateDir, "pending");
+        mJournalDir.mkdirs();   // creates mBaseStateDir along the way
+        mJournal = null;        // will be created on first use
+
+        mConstants = new BackupManagerConstants(mBackupHandler, mContext.getContentResolver());
+        // We are observing changes to the constants throughout the lifecycle of BMS. This is
+        // because we reference the constants in multiple areas of BMS, which otherwise would
+        // require frequent starting and stopping.
+        mConstants.start();
+
+        // Build our mapping of uid to backup client services.  This implicitly
+        // schedules a backup pass on the Package Manager metadata the first
+        // time anything needs to be backed up.
+        synchronized (mBackupParticipants) {
+            addPackageParticipantsLocked(null);
+        }
+
+        mTransportManager =
+                Objects.requireNonNull(transportManager, "transportManager cannot be null");
+        mTransportManager.setOnTransportRegisteredListener(this::onTransportRegistered);
+        mRegisterTransportsRequestedTime = SystemClock.elapsedRealtime();
+        mBackupHandler.postDelayed(
+                mTransportManager::registerTransports, INITIALIZATION_DELAY_MILLIS);
+
+        // Now that we know about valid backup participants, parse any leftover journal files into
+        // the pending backup set
+        mBackupHandler.postDelayed(this::parseLeftoverJournals, INITIALIZATION_DELAY_MILLIS);
+
+        mBackupPreferences = new UserBackupPreferences(mContext, mBaseStateDir);
+
+        // Power management
+        mWakelock = new BackupWakeLock(
+                mPowerManager.newWakeLock(
+                        PowerManager.PARTIAL_WAKE_LOCK,
+                        "*backup*-" + userId + "-" + userBackupThread.getThreadId()), userId);
+
+        // Set up the various sorts of package tracking we do
+        mFullBackupScheduleFile = new File(mBaseStateDir, "fb-schedule");
+        initPackageTracking();
+    }
+
+    void initializeBackupEnableState() {
+        boolean isEnabled = UserBackupManagerFilePersistedSettings.readBackupEnableState(mUserId);
+        setBackupEnabled(isEnabled);
+    }
+
+    /** Cleans up state when the user of this service is stopped. */
+    @VisibleForTesting
+    protected void tearDownService() {
+        mAgentTimeoutParameters.stop();
+        mConstants.stop();
+        mContext.getContentResolver().unregisterContentObserver(mSetupObserver);
+        mContext.unregisterReceiver(mRunInitReceiver);
+        mContext.unregisterReceiver(mPackageTrackingReceiver);
+        mBackupHandler.stop();
+    }
+
+    public @UserIdInt int getUserId() {
+        return mUserId;
+    }
+
+    public BackupManagerConstants getConstants() {
+        return mConstants;
+    }
+
+    public BackupAgentTimeoutParameters getAgentTimeoutParameters() {
+        return mAgentTimeoutParameters;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public PackageManager getPackageManager() {
+        return mPackageManager;
+    }
+
+    public IPackageManager getPackageManagerBinder() {
+        return mPackageManagerBinder;
+    }
+
+    public IActivityManager getActivityManager() {
+        return mActivityManager;
+    }
+
+    public AlarmManager getAlarmManager() {
+        return mAlarmManager;
+    }
+
+    @VisibleForTesting
+    void setPowerManager(PowerManager powerManager) {
+        mPowerManager = powerManager;
+    }
+
+    public TransportManager getTransportManager() {
+        return mTransportManager;
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    public boolean isSetupComplete() {
+        return mSetupComplete;
+    }
+
+    public void setSetupComplete(boolean setupComplete) {
+        mSetupComplete = setupComplete;
+    }
+
+    public BackupWakeLock getWakelock() {
+        return mWakelock;
+    }
+
+    /**
+     * Sets the {@link WorkSource} of the {@link PowerManager.WakeLock} returned by {@link
+     * #getWakelock()}.
+     */
+    @VisibleForTesting
+    public void setWorkSource(@Nullable WorkSource workSource) {
+        // TODO: This is for testing, unfortunately WakeLock is final and WorkSource is not exposed
+        mWakelock.mPowerManagerWakeLock.setWorkSource(workSource);
+    }
+
+    public Handler getBackupHandler() {
+        return mBackupHandler;
+    }
+
+    public PendingIntent getRunInitIntent() {
+        return mRunInitIntent;
+    }
+
+    public HashMap<String, BackupRequest> getPendingBackups() {
+        return mPendingBackups;
+    }
+
+    public Object getQueueLock() {
+        return mQueueLock;
+    }
+
+    public boolean isBackupRunning() {
+        return mBackupRunning;
+    }
+
+    public void setBackupRunning(boolean backupRunning) {
+        mBackupRunning = backupRunning;
+    }
+
+    public void setLastBackupPass(long lastBackupPass) {
+        mLastBackupPass = lastBackupPass;
+    }
+
+    public Object getClearDataLock() {
+        return mClearDataLock;
+    }
+
+    public void setClearingData(boolean clearingData) {
+        mClearingData = clearingData;
+    }
+
+    public boolean isRestoreInProgress() {
+        return mIsRestoreInProgress;
+    }
+
+    public void setRestoreInProgress(boolean restoreInProgress) {
+        mIsRestoreInProgress = restoreInProgress;
+    }
+
+    public Queue<PerformUnifiedRestoreTask> getPendingRestores() {
+        return mPendingRestores;
+    }
+
+    public ActiveRestoreSession getActiveRestoreSession() {
+        return mActiveRestoreSession;
+    }
+
+    public SparseArray<Operation> getCurrentOperations() {
+        return mCurrentOperations;
+    }
+
+    public Object getCurrentOpLock() {
+        return mCurrentOpLock;
+    }
+
+    public SparseArray<AdbParams> getAdbBackupRestoreConfirmations() {
+        return mAdbBackupRestoreConfirmations;
+    }
+
+    public File getBaseStateDir() {
+        return mBaseStateDir;
+    }
+
+    public File getDataDir() {
+        return mDataDir;
+    }
+
+    @VisibleForTesting
+    BroadcastReceiver getPackageTrackingReceiver() {
+        return mPackageTrackingReceiver;
+    }
+
+    @Nullable
+    public DataChangedJournal getJournal() {
+        return mJournal;
+    }
+
+    public void setJournal(@Nullable DataChangedJournal journal) {
+        mJournal = journal;
+    }
+
+    public SecureRandom getRng() {
+        return mRng;
+    }
+
+    public void setAncestralPackages(Set<String> ancestralPackages) {
+        mAncestralPackages = ancestralPackages;
+    }
+
+    public void setAncestralToken(long ancestralToken) {
+        mAncestralToken = ancestralToken;
+    }
+
+    public long getCurrentToken() {
+        return mCurrentToken;
+    }
+
+    public void setCurrentToken(long currentToken) {
+        mCurrentToken = currentToken;
+    }
+
+    public ArraySet<String> getPendingInits() {
+        return mPendingInits;
+    }
+
+    /** Clear all pending transport initializations. */
+    public void clearPendingInits() {
+        mPendingInits.clear();
+    }
+
+    public void setRunningFullBackupTask(
+            PerformFullTransportBackupTask runningFullBackupTask) {
+        mRunningFullBackupTask = runningFullBackupTask;
+    }
+
+    /**
+     *  Utility: build a new random integer token. The low bits are the ordinal of the operation for
+     *  near-time uniqueness, and the upper bits are random for app-side unpredictability.
+     */
+    public int generateRandomIntegerToken() {
+        int token = mTokenGenerator.nextInt();
+        if (token < 0) token = -token;
+        token &= ~0xFF;
+        token |= (mNextToken.incrementAndGet() & 0xFF);
+        return token;
+    }
+
+    /**
+     * Construct a backup agent instance for the metadata pseudopackage. This is a process-local
+     * non-lifecycle agent instance, so we manually set up the context topology for it.
+     */
+    public BackupAgent makeMetadataAgent() {
+        PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent(mPackageManager, mUserId);
+        pmAgent.attach(mContext);
+        pmAgent.onCreate(UserHandle.of(mUserId));
+        return pmAgent;
+    }
+
+    /**
+     * Same as {@link #makeMetadataAgent()} but with explicit package-set configuration.
+     */
+    public PackageManagerBackupAgent makeMetadataAgent(List<PackageInfo> packages) {
+        PackageManagerBackupAgent pmAgent =
+                new PackageManagerBackupAgent(mPackageManager, packages, mUserId);
+        pmAgent.attach(mContext);
+        pmAgent.onCreate(UserHandle.of(mUserId));
+        return pmAgent;
+    }
+
+    private void initPackageTracking() {
+        if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "` tracking"));
+
+        // Remember our ancestral dataset
+        mTokenFile = new File(mBaseStateDir, "ancestral");
+        try (DataInputStream tokenStream = new DataInputStream(new BufferedInputStream(
+                new FileInputStream(mTokenFile)))) {
+            int version = tokenStream.readInt();
+            if (version == CURRENT_ANCESTRAL_RECORD_VERSION) {
+                mAncestralToken = tokenStream.readLong();
+                mCurrentToken = tokenStream.readLong();
+
+                int numPackages = tokenStream.readInt();
+                if (numPackages >= 0) {
+                    mAncestralPackages = new HashSet<>();
+                    for (int i = 0; i < numPackages; i++) {
+                        String pkgName = tokenStream.readUTF();
+                        mAncestralPackages.add(pkgName);
+                    }
+                }
+            }
+        } catch (FileNotFoundException fnf) {
+            // Probably innocuous
+            Slog.v(TAG, addUserIdToLogMessage(mUserId, "No ancestral data"));
+        } catch (IOException e) {
+            Slog.w(TAG, addUserIdToLogMessage(mUserId, "Unable to read token file"), e);
+        }
+
+        mProcessedPackagesJournal = new ProcessedPackagesJournal(mBaseStateDir);
+        mProcessedPackagesJournal.init();
+
+        synchronized (mQueueLock) {
+            // Resume the full-data backup queue
+            mFullBackupQueue = readFullBackupSchedule();
+        }
+
+        // Register for broadcasts about package changes.
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        mContext.registerReceiverAsUser(
+                mPackageTrackingReceiver,
+                UserHandle.of(mUserId),
+                filter,
+                /* broadcastPermission */ null,
+                /* scheduler */ null);
+
+        // Register for events related to sdcard installation.
+        IntentFilter sdFilter = new IntentFilter();
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+        mContext.registerReceiverAsUser(
+                mPackageTrackingReceiver,
+                UserHandle.of(mUserId),
+                sdFilter,
+                /* broadcastPermission */ null,
+                /* scheduler */ null);
+    }
+
+    private ArrayList<FullBackupEntry> readFullBackupSchedule() {
+        boolean changed = false;
+        ArrayList<FullBackupEntry> schedule = null;
+        List<PackageInfo> apps =
+                PackageManagerBackupAgent.getStorableApplications(mPackageManager, mUserId);
+
+        if (mFullBackupScheduleFile.exists()) {
+            try (FileInputStream fstream = new FileInputStream(mFullBackupScheduleFile);
+                 BufferedInputStream bufStream = new BufferedInputStream(fstream);
+                 DataInputStream in = new DataInputStream(bufStream)) {
+                int version = in.readInt();
+                if (version != SCHEDULE_FILE_VERSION) {
+                    Slog.e(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Unknown backup schedule version " + version));
+                    return null;
+                }
+
+                final int numPackages = in.readInt();
+                schedule = new ArrayList<>(numPackages);
+
+                // HashSet instead of ArraySet specifically because we want the eventual
+                // lookups against O(hundreds) of entries to be as fast as possible, and
+                // we discard the set immediately after the scan so the extra memory
+                // overhead is transient.
+                HashSet<String> foundApps = new HashSet<>(numPackages);
+
+                for (int i = 0; i < numPackages; i++) {
+                    String pkgName = in.readUTF();
+                    long lastBackup = in.readLong();
+                    foundApps.add(pkgName); // all apps that we've addressed already
+                    try {
+                        PackageInfo pkg = mPackageManager.getPackageInfoAsUser(pkgName, 0, mUserId);
+                        if (AppBackupUtils.appGetsFullBackup(pkg)
+                                && AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo,
+                                mUserId)) {
+                            schedule.add(new FullBackupEntry(pkgName, lastBackup));
+                        } else {
+                            if (DEBUG) {
+                                Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
+                                        + " no longer eligible for full backup"));
+                            }
+                        }
+                    } catch (NameNotFoundException e) {
+                        if (DEBUG) {
+                            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
+                                    + " not installed; dropping from full backup"));
+                        }
+                    }
+                }
+
+                // New apps can arrive "out of band" via OTA and similar, so we also need to
+                // scan to make sure that we're tracking all full-backup candidates properly
+                for (PackageInfo app : apps) {
+                    if (AppBackupUtils.appGetsFullBackup(app)
+                            && AppBackupUtils.appIsEligibleForBackup(app.applicationInfo,
+                            mUserId)) {
+                        if (!foundApps.contains(app.packageName)) {
+                            if (MORE_DEBUG) {
+                                Slog.i(
+                                        TAG,
+                                        addUserIdToLogMessage(
+                                                mUserId,
+                                                "New full backup app "
+                                                        + app.packageName
+                                                        + " found"));
+                            }
+                            schedule.add(new FullBackupEntry(app.packageName, 0));
+                            changed = true;
+                        }
+                    }
+                }
+
+                Collections.sort(schedule);
+            } catch (Exception e) {
+                Slog.e(TAG, addUserIdToLogMessage(mUserId, "Unable to read backup schedule"), e);
+                mFullBackupScheduleFile.delete();
+                schedule = null;
+            }
+        }
+
+        if (schedule == null) {
+            // no prior queue record, or unable to read it.  Set up the queue
+            // from scratch.
+            changed = true;
+            schedule = new ArrayList<>(apps.size());
+            for (PackageInfo info : apps) {
+                if (AppBackupUtils.appGetsFullBackup(info) && AppBackupUtils.appIsEligibleForBackup(
+                        info.applicationInfo, mUserId)) {
+                    schedule.add(new FullBackupEntry(info.packageName, 0));
+                }
+            }
+        }
+
+        if (changed) {
+            writeFullBackupScheduleAsync();
+        }
+        return schedule;
+    }
+
+    private Runnable mFullBackupScheduleWriter = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mQueueLock) {
+                try {
+                    ByteArrayOutputStream bufStream = new ByteArrayOutputStream(4096);
+                    DataOutputStream bufOut = new DataOutputStream(bufStream);
+                    bufOut.writeInt(SCHEDULE_FILE_VERSION);
+
+                    // version 1:
+                    //
+                    // [int] # of packages in the queue = N
+                    // N * {
+                    //     [utf8] package name
+                    //     [long] last backup time for this package
+                    //     }
+                    int numPackages = mFullBackupQueue.size();
+                    bufOut.writeInt(numPackages);
+
+                    for (int i = 0; i < numPackages; i++) {
+                        FullBackupEntry entry = mFullBackupQueue.get(i);
+                        bufOut.writeUTF(entry.packageName);
+                        bufOut.writeLong(entry.lastBackup);
+                    }
+                    bufOut.flush();
+
+                    AtomicFile af = new AtomicFile(mFullBackupScheduleFile);
+                    FileOutputStream out = af.startWrite();
+                    out.write(bufStream.toByteArray());
+                    af.finishWrite(out);
+                } catch (Exception e) {
+                    Slog.e(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Unable to write backup schedule!"),
+                            e);
+                }
+            }
+        }
+    };
+
+    private void writeFullBackupScheduleAsync() {
+        mBackupHandler.removeCallbacks(mFullBackupScheduleWriter);
+        mBackupHandler.post(mFullBackupScheduleWriter);
+    }
+
+    private void parseLeftoverJournals() {
+        ArrayList<DataChangedJournal> journals = DataChangedJournal.listJournals(mJournalDir);
+        for (DataChangedJournal journal : journals) {
+            if (!journal.equals(mJournal)) {
+                try {
+                    journal.forEach(packageName -> {
+                        Slog.i(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Found stale backup journal, scheduling"));
+                        if (MORE_DEBUG) {
+                            Slog.i(TAG, addUserIdToLogMessage(mUserId, "  " + packageName));
+                        }
+                        dataChangedImpl(packageName);
+                    });
+                } catch (IOException e) {
+                    Slog.e(TAG, addUserIdToLogMessage(mUserId, "Can't read " + journal), e);
+                }
+            }
+        }
+    }
+
+    public Set<String> getExcludedRestoreKeys(String packageName) {
+        return mBackupPreferences.getExcludedRestoreKeysForPackage(packageName);
+    }
+
+    /** Used for generating random salts or passwords. */
+    public byte[] randomBytes(int bits) {
+        byte[] array = new byte[bits / 8];
+        mRng.nextBytes(array);
+        return array;
+    }
+
+    /** For adb backup/restore. */
+    public boolean setBackupPassword(String currentPw, String newPw) {
+        return mBackupPasswordManager.setBackupPassword(currentPw, newPw);
+    }
+
+    /** For adb backup/restore. */
+    public boolean hasBackupPassword() {
+        return mBackupPasswordManager.hasBackupPassword();
+    }
+
+    /** For adb backup/restore. */
+    public boolean backupPasswordMatches(String currentPw) {
+        return mBackupPasswordManager.backupPasswordMatches(currentPw);
+    }
+
+    /**
+     * Maintain persistent state around whether need to do an initialize operation. This will lock
+     * on {@link #getQueueLock()}.
+     */
+    public void recordInitPending(
+            boolean isPending, String transportName, String transportDirName) {
+        synchronized (mQueueLock) {
+            if (MORE_DEBUG) {
+                Slog.i(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "recordInitPending("
+                                        + isPending
+                                        + ") on transport "
+                                        + transportName));
+            }
+
+            File stateDir = new File(mBaseStateDir, transportDirName);
+            File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME);
+
+            if (isPending) {
+                // We need an init before we can proceed with sending backup data.
+                // Record that with an entry in our set of pending inits, as well as
+                // journaling it via creation of a sentinel file.
+                mPendingInits.add(transportName);
+                try {
+                    (new FileOutputStream(initPendingFile)).close();
+                } catch (IOException ioe) {
+                    // Something is badly wrong with our permissions; just try to move on
+                }
+            } else {
+                // No more initialization needed; wipe the journal and reset our state.
+                initPendingFile.delete();
+                mPendingInits.remove(transportName);
+            }
+        }
+    }
+
+    /**
+     * Reset all of our bookkeeping because the backend data has been wiped (for example due to idle
+     * expiry), so we must re-upload all saved settings.
+     */
+    public void resetBackupState(File stateFileDir) {
+        synchronized (mQueueLock) {
+            mProcessedPackagesJournal.reset();
+
+            mCurrentToken = 0;
+            writeRestoreTokens();
+
+            // Remove all the state files
+            for (File sf : stateFileDir.listFiles()) {
+                // ... but don't touch the needs-init sentinel
+                if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) {
+                    sf.delete();
+                }
+            }
+        }
+
+        // Enqueue a new backup of every participant
+        synchronized (mBackupParticipants) {
+            final int numParticipants = mBackupParticipants.size();
+            for (int i = 0; i < numParticipants; i++) {
+                HashSet<String> participants = mBackupParticipants.valueAt(i);
+                if (participants != null) {
+                    for (String packageName : participants) {
+                        dataChangedImpl(packageName);
+                    }
+                }
+            }
+        }
+    }
+
+    private void onTransportRegistered(String transportName, String transportDirName) {
+        if (DEBUG) {
+            long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime;
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Transport "
+                                    + transportName
+                                    + " registered "
+                                    + timeMs
+                                    + "ms after first request (delay = "
+                                    + INITIALIZATION_DELAY_MILLIS
+                                    + "ms)"));
+        }
+
+        File stateDir = new File(mBaseStateDir, transportDirName);
+        stateDir.mkdirs();
+
+        File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
+        if (initSentinel.exists()) {
+            synchronized (mQueueLock) {
+                mPendingInits.add(transportName);
+
+                // TODO: pick a better starting time than now + 1 minute
+                long delay = 1000 * 60; // one minute, in milliseconds
+                mAlarmManager.set(AlarmManager.RTC_WAKEUP,
+                        System.currentTimeMillis() + delay, mRunInitIntent);
+            }
+        }
+    }
+
+    /**
+     * A {@link BroadcastReceiver} tracking changes to packages and sd cards in order to update our
+     * internal bookkeeping.
+     */
+    private BroadcastReceiver mPackageTrackingReceiver = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            if (MORE_DEBUG) {
+                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Received broadcast " + intent));
+            }
+
+            String action = intent.getAction();
+            boolean replacing = false;
+            boolean added = false;
+            boolean changed = false;
+            Bundle extras = intent.getExtras();
+            String[] packageList = null;
+
+            if (Intent.ACTION_PACKAGE_ADDED.equals(action)
+                    || Intent.ACTION_PACKAGE_REMOVED.equals(action)
+                    || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+                Uri uri = intent.getData();
+                if (uri == null) {
+                    return;
+                }
+
+                String packageName = uri.getSchemeSpecificPart();
+                if (packageName != null) {
+                    packageList = new String[] {packageName};
+                }
+
+                changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
+                if (changed) {
+                    // Look at new transport states for package changed events.
+                    String[] components =
+                            intent.getStringArrayExtra(
+                                    Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+
+                    if (MORE_DEBUG) {
+                        Slog.i(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Package " + packageName + " changed"));
+                        for (int i = 0; i < components.length; i++) {
+                            Slog.i(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId, "   * " + components[i]));
+                        }
+                    }
+
+                    mBackupHandler.post(
+                            () ->
+                                    mTransportManager.onPackageChanged(
+                                            packageName, components));
+                    return;
+                }
+
+                added = Intent.ACTION_PACKAGE_ADDED.equals(action);
+                replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false);
+            } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
+                added = true;
+                packageList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
+                added = false;
+                packageList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+            }
+
+            if (packageList == null || packageList.length == 0) {
+                return;
+            }
+
+            int uid = extras.getInt(Intent.EXTRA_UID);
+            if (added) {
+                synchronized (mBackupParticipants) {
+                    if (replacing) {
+                        // Remove the entry under the old uid and fall through to re-add. If
+                        // an app
+                        // just opted into key/value backup, add it as a known participant.
+                        removePackageParticipantsLocked(packageList, uid);
+                    }
+                    addPackageParticipantsLocked(packageList);
+                }
+
+                long now = System.currentTimeMillis();
+                for (String packageName : packageList) {
+                    try {
+                        PackageInfo app =
+                                mPackageManager.getPackageInfoAsUser(
+                                        packageName, /* flags */ 0, mUserId);
+                        if (AppBackupUtils.appGetsFullBackup(app)
+                                && AppBackupUtils.appIsEligibleForBackup(
+                                        app.applicationInfo, mUserId)) {
+                            enqueueFullBackup(packageName, now);
+                            scheduleNextFullBackupJob(0);
+                        } else {
+                            // The app might have just transitioned out of full-data into
+                            // doing
+                            // key/value backups, or might have just disabled backups
+                            // entirely. Make
+                            // sure it is no longer in the full-data queue.
+                            synchronized (mQueueLock) {
+                                dequeueFullBackupLocked(packageName);
+                            }
+                            writeFullBackupScheduleAsync();
+                        }
+
+                        mBackupHandler.post(
+                                () -> mTransportManager.onPackageAdded(packageName));
+                    } catch (NameNotFoundException e) {
+                        if (DEBUG) {
+                            Slog.w(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId,
+                                            "Can't resolve new app " + packageName));
+                        }
+                    }
+                }
+
+                // Whenever a package is added or updated we need to update the package
+                // metadata
+                // bookkeeping.
+                dataChangedImpl(PACKAGE_MANAGER_SENTINEL);
+            } else {
+                if (!replacing) {
+                    // Outright removal. In the full-data case, the app will be dropped from
+                    // the
+                    // queue when its (now obsolete) name comes up again for backup.
+                    synchronized (mBackupParticipants) {
+                        removePackageParticipantsLocked(packageList, uid);
+                    }
+                }
+
+                for (String packageName : packageList) {
+                    mBackupHandler.post(
+                            () -> mTransportManager.onPackageRemoved(packageName));
+                }
+            }
+        }
+    };
+
+    // Add the backup agents in the given packages to our set of known backup participants.
+    // If 'packageNames' is null, adds all backup agents in the whole system.
+    private void addPackageParticipantsLocked(String[] packageNames) {
+        // Look for apps that define the android:backupAgent attribute
+        List<PackageInfo> targetApps = allAgentPackages();
+        if (packageNames != null) {
+            if (MORE_DEBUG) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "addPackageParticipantsLocked: #" + packageNames.length));
+            }
+            for (String packageName : packageNames) {
+                addPackageParticipantsLockedInner(packageName, targetApps);
+            }
+        } else {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, addUserIdToLogMessage(mUserId, "addPackageParticipantsLocked: all"));
+            }
+            addPackageParticipantsLockedInner(null, targetApps);
+        }
+    }
+
+    private void addPackageParticipantsLockedInner(String packageName,
+            List<PackageInfo> targetPkgs) {
+        if (MORE_DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Examining " + packageName + " for backup agent"));
+        }
+
+        for (PackageInfo pkg : targetPkgs) {
+            if (packageName == null || pkg.packageName.equals(packageName)) {
+                int uid = pkg.applicationInfo.uid;
+                HashSet<String> set = mBackupParticipants.get(uid);
+                if (set == null) {
+                    set = new HashSet<>();
+                    mBackupParticipants.put(uid, set);
+                }
+                set.add(pkg.packageName);
+                if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Agent found; added"));
+
+                // Schedule a backup for it on general principles
+                if (MORE_DEBUG) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Scheduling backup for new app " + pkg.packageName));
+                }
+                Message msg = mBackupHandler
+                        .obtainMessage(MSG_SCHEDULE_BACKUP_PACKAGE, pkg.packageName);
+                mBackupHandler.sendMessage(msg);
+            }
+        }
+    }
+
+    // Remove the given packages' entries from our known active set.
+    private void removePackageParticipantsLocked(String[] packageNames, int oldUid) {
+        if (packageNames == null) {
+            Slog.w(TAG, addUserIdToLogMessage(mUserId, "removePackageParticipants with null list"));
+            return;
+        }
+
+        if (MORE_DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "removePackageParticipantsLocked: uid="
+                                    + oldUid
+                                    + " #"
+                                    + packageNames.length));
+        }
+        for (String pkg : packageNames) {
+            // Known previous UID, so we know which package set to check
+            HashSet<String> set = mBackupParticipants.get(oldUid);
+            if (set != null && set.contains(pkg)) {
+                removePackageFromSetLocked(set, pkg);
+                if (set.isEmpty()) {
+                    if (MORE_DEBUG) {
+                        Slog.v(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "  last one of this uid; purging set"));
+                    }
+                    mBackupParticipants.remove(oldUid);
+                }
+            }
+        }
+    }
+
+    private void removePackageFromSetLocked(final HashSet<String> set,
+            final String packageName) {
+        if (set.contains(packageName)) {
+            // Found it.  Remove this one package from the bookkeeping, and
+            // if it's the last participating app under this uid we drop the
+            // (now-empty) set as well.
+            // Note that we deliberately leave it 'known' in the "ever backed up"
+            // bookkeeping so that its current-dataset data will be retrieved
+            // if the app is subsequently reinstalled
+            if (MORE_DEBUG) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(mUserId, "  removing participant " + packageName));
+            }
+            set.remove(packageName);
+            mPendingBackups.remove(packageName);
+        }
+    }
+
+    // Returns the set of all applications that define an android:backupAgent attribute
+    private List<PackageInfo> allAgentPackages() {
+        // !!! TODO: cache this and regenerate only when necessary
+        int flags = PackageManager.GET_SIGNING_CERTIFICATES;
+        List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(flags, mUserId);
+        int numPackages = packages.size();
+        for (int a = numPackages - 1; a >= 0; a--) {
+            PackageInfo pkg = packages.get(a);
+            try {
+                ApplicationInfo app = pkg.applicationInfo;
+                if (((app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0)
+                        || app.backupAgentName == null
+                        || (app.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0) {
+                    packages.remove(a);
+                } else {
+                    // we will need the shared library path, so look that up and store it here.
+                    // This is used implicitly when we pass the PackageInfo object off to
+                    // the Activity Manager to launch the app for backup/restore purposes.
+                    app = mPackageManager.getApplicationInfoAsUser(pkg.packageName,
+                            PackageManager.GET_SHARED_LIBRARY_FILES, mUserId);
+                    pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles;
+                    pkg.applicationInfo.sharedLibraryInfos = app.sharedLibraryInfos;
+                }
+            } catch (NameNotFoundException e) {
+                packages.remove(a);
+            }
+        }
+        return packages;
+    }
+
+    /**
+     * Called from the backup tasks: record that the given app has been successfully backed up at
+     * least once. This includes both key/value and full-data backups through the transport.
+     */
+    public void logBackupComplete(String packageName) {
+        if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return;
+
+        for (String receiver : mConstants.getBackupFinishedNotificationReceivers()) {
+            final Intent notification = new Intent();
+            notification.setAction(BACKUP_FINISHED_ACTION);
+            notification.setPackage(receiver);
+            notification.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES
+                    | Intent.FLAG_RECEIVER_FOREGROUND);
+            notification.putExtra(BACKUP_FINISHED_PACKAGE_EXTRA, packageName);
+            mContext.sendBroadcastAsUser(notification, UserHandle.of(mUserId));
+        }
+
+        mProcessedPackagesJournal.addPackage(packageName);
+    }
+
+    /**
+     * Persistently record the current and ancestral backup tokens, as well as the set of packages
+     * with data available in the ancestral dataset.
+     */
+    public void writeRestoreTokens() {
+        try (RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd")) {
+            // First, the version number of this record, for futureproofing
+            af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION);
+
+            // Write the ancestral and current tokens
+            af.writeLong(mAncestralToken);
+            af.writeLong(mCurrentToken);
+
+            // Now write the set of ancestral packages
+            if (mAncestralPackages == null) {
+                af.writeInt(-1);
+            } else {
+                af.writeInt(mAncestralPackages.size());
+                if (DEBUG) {
+                    Slog.v(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Ancestral packages:  " + mAncestralPackages.size()));
+                }
+                for (String pkgName : mAncestralPackages) {
+                    af.writeUTF(pkgName);
+                    if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "   " + pkgName));
+                }
+            }
+        } catch (IOException e) {
+            Slog.w(TAG, addUserIdToLogMessage(mUserId, "Unable to write token file:"), e);
+        }
+    }
+
+    /** Fires off a backup agent, blocking until it attaches or times out. */
+    @Nullable
+    public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) {
+        IBackupAgent agent = null;
+        synchronized (mAgentConnectLock) {
+            mConnecting = true;
+            mConnectedAgent = null;
+            try {
+                if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId)) {
+                    Slog.d(TAG, addUserIdToLogMessage(mUserId, "awaiting agent for " + app));
+
+                    // success; wait for the agent to arrive
+                    // only wait 10 seconds for the bind to happen
+                    long timeoutMark = System.currentTimeMillis() + BIND_TIMEOUT_INTERVAL;
+                    while (mConnecting && mConnectedAgent == null
+                            && (System.currentTimeMillis() < timeoutMark)) {
+                        try {
+                            mAgentConnectLock.wait(5000);
+                        } catch (InterruptedException e) {
+                            // just bail
+                            Slog.w(TAG, addUserIdToLogMessage(mUserId, "Interrupted: " + e));
+                            mConnecting = false;
+                            mConnectedAgent = null;
+                        }
+                    }
+
+                    // if we timed out with no connect, abort and move on
+                    if (mConnecting) {
+                        Slog.w(
+                                TAG,
+                                addUserIdToLogMessage(mUserId, "Timeout waiting for agent " + app));
+                        mConnectedAgent = null;
+                    }
+                    if (DEBUG) {
+                        Slog.i(TAG, addUserIdToLogMessage(mUserId, "got agent " + mConnectedAgent));
+                    }
+                    agent = mConnectedAgent;
+                }
+            } catch (RemoteException e) {
+                // can't happen - ActivityManager is local
+            }
+        }
+        if (agent == null) {
+            mActivityManagerInternal.clearPendingBackup(mUserId);
+        }
+        return agent;
+    }
+
+    /** Unbind from a backup agent. */
+    public void unbindAgent(ApplicationInfo app) {
+        try {
+            mActivityManager.unbindBackupAgent(app);
+        } catch (RemoteException e) {
+            // Can't happen - activity manager is local
+        }
+    }
+
+    /**
+     * Clear an application's data after a failed restore, blocking until the operation completes or
+     * times out.
+     */
+    public void clearApplicationDataAfterRestoreFailure(String packageName) {
+        clearApplicationDataSynchronous(packageName, true, false);
+    }
+
+    /**
+     * Clear an application's data before restore, blocking until the operation completes or times
+     * out.
+     */
+    public void clearApplicationDataBeforeRestore(String packageName) {
+        clearApplicationDataSynchronous(packageName, false, true);
+    }
+
+    /**
+     * Clear an application's data, blocking until the operation completes or times out.
+     *
+     * @param checkFlagAllowClearUserDataOnFailedRestore if {@code true} uses
+     *    {@link ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE} to decide if
+     *    clearing data is allowed after a failed restore.
+     *
+     * @param keepSystemState if {@code true}, we don't clear system state such as already restored
+     *    notification settings, permission grants, etc.
+     */
+    private void clearApplicationDataSynchronous(String packageName,
+            boolean checkFlagAllowClearUserDataOnFailedRestore, boolean keepSystemState) {
+        try {
+            ApplicationInfo applicationInfo = mPackageManager.getPackageInfoAsUser(
+                    packageName, 0, mUserId).applicationInfo;
+
+            boolean shouldClearData;
+            if (checkFlagAllowClearUserDataOnFailedRestore
+                    && applicationInfo.targetSdkVersion >= Build.VERSION_CODES.Q) {
+                shouldClearData = (applicationInfo.privateFlags
+                    & ApplicationInfo.PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE) != 0;
+            } else {
+                shouldClearData =
+                    (applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) != 0;
+            }
+
+            if (!shouldClearData) {
+                if (MORE_DEBUG) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Clearing app data is not allowed so not wiping "
+                                            + packageName));
+                }
+                return;
+            }
+        } catch (NameNotFoundException e) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Tried to clear data for " + packageName + " but not found"));
+            return;
+        }
+
+        ClearDataObserver observer = new ClearDataObserver(this);
+
+        synchronized (mClearDataLock) {
+            mClearingData = true;
+            try {
+                mActivityManager.clearApplicationUserData(packageName, keepSystemState, observer,
+                        mUserId);
+            } catch (RemoteException e) {
+                // can't happen because the activity manager is in this process
+            }
+
+            // Only wait 30 seconds for the clear data to happen.
+            long timeoutMark = System.currentTimeMillis() + CLEAR_DATA_TIMEOUT_INTERVAL;
+            while (mClearingData && (System.currentTimeMillis() < timeoutMark)) {
+                try {
+                    mClearDataLock.wait(5000);
+                } catch (InterruptedException e) {
+                    // won't happen, but still.
+                    mClearingData = false;
+                    Slog.w(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Interrupted while waiting for "
+                                            + packageName
+                                            + " data to be cleared"),
+                            e);
+                }
+            }
+
+            if (mClearingData) {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Clearing app data for " + packageName + " timed out"));
+            }
+        }
+    }
+
+    /**
+     * Get the restore-set token for the best-available restore set for this {@code packageName}:
+     * the active set if possible, else the ancestral one. Returns zero if none available.
+     */
+    public long getAvailableRestoreToken(String packageName) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "getAvailableRestoreToken");
+
+        long token = mAncestralToken;
+        synchronized (mQueueLock) {
+            if (mCurrentToken != 0 && mProcessedPackagesJournal.hasBeenProcessed(packageName)) {
+                if (MORE_DEBUG) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "App in ever-stored, so using current token"));
+                }
+                token = mCurrentToken;
+            }
+        }
+        if (MORE_DEBUG) {
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "getAvailableRestoreToken() == " + token));
+        }
+        return token;
+    }
+
+    /**
+     * Requests a backup for the inputted {@code packages}.
+     *
+     * @see #requestBackup(String[], IBackupObserver, IBackupManagerMonitor, int).
+     */
+    public int requestBackup(String[] packages, IBackupObserver observer, int flags) {
+        return requestBackup(packages, observer, null, flags);
+    }
+
+    /**
+     * Requests a backup for the inputted {@code packages} with a specified {@link
+     * IBackupManagerMonitor}.
+     */
+    public int requestBackup(String[] packages, IBackupObserver observer,
+            IBackupManagerMonitor monitor, int flags) {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "requestBackup");
+
+        if (packages == null || packages.length < 1) {
+            Slog.e(TAG, addUserIdToLogMessage(mUserId, "No packages named for backup request"));
+            BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
+            monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES,
+                    null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
+            throw new IllegalArgumentException("No packages are provided for backup");
+        }
+
+        if (!mEnabled || !mSetupComplete) {
+            Slog.i(
+                    TAG,
+                    addUserIdToLogMessage(mUserId, "Backup requested but enabled="
+                            + mEnabled
+                            + " setupComplete="
+                            + mSetupComplete));
+            BackupObserverUtils.sendBackupFinished(observer,
+                    BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+            final int logTag = mSetupComplete
+                    ? BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED
+                    : BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
+            monitor = BackupManagerMonitorUtils.monitorEvent(monitor, logTag, null,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+            return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
+        }
+
+        final TransportClient transportClient;
+        final String transportDirName;
+        try {
+            transportDirName =
+                    mTransportManager.getTransportDirName(
+                            mTransportManager.getCurrentTransportName());
+            transportClient =
+                    mTransportManager.getCurrentTransportClientOrThrow("BMS.requestBackup()");
+        } catch (TransportNotRegisteredException e) {
+            BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
+            monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL,
+                    null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
+            return BackupManager.ERROR_TRANSPORT_ABORTED;
+        }
+
+        OnTaskFinishedListener listener =
+                caller -> mTransportManager.disposeOfTransportClient(transportClient, caller);
+
+        ArrayList<String> fullBackupList = new ArrayList<>();
+        ArrayList<String> kvBackupList = new ArrayList<>();
+        for (String packageName : packages) {
+            if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) {
+                kvBackupList.add(packageName);
+                continue;
+            }
+            try {
+                PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser(packageName,
+                        PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+                if (!AppBackupUtils.appIsEligibleForBackup(packageInfo.applicationInfo, mUserId)) {
+                    BackupObserverUtils.sendBackupOnPackageResult(observer, packageName,
+                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                    continue;
+                }
+                if (AppBackupUtils.appGetsFullBackup(packageInfo)) {
+                    fullBackupList.add(packageInfo.packageName);
+                } else {
+                    kvBackupList.add(packageInfo.packageName);
+                }
+            } catch (NameNotFoundException e) {
+                BackupObserverUtils.sendBackupOnPackageResult(observer, packageName,
+                        BackupManager.ERROR_PACKAGE_NOT_FOUND);
+            }
+        }
+        EventLog.writeEvent(EventLogTags.BACKUP_REQUESTED, packages.length, kvBackupList.size(),
+                fullBackupList.size());
+        if (MORE_DEBUG) {
+            Slog.i(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Backup requested for "
+                                    + packages.length
+                                    + " packages, of them: "
+                                    + fullBackupList.size()
+                                    + " full backups, "
+                                    + kvBackupList.size()
+                                    + " k/v backups"));
+        }
+
+        boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0;
+
+        Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP);
+        msg.obj = new BackupParams(transportClient, transportDirName, kvBackupList, fullBackupList,
+                observer, monitor, listener, true, nonIncrementalBackup);
+        mBackupHandler.sendMessage(msg);
+        return BackupManager.SUCCESS;
+    }
+
+    /** Cancel all running backups. */
+    public void cancelBackups() {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups");
+        if (MORE_DEBUG) {
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "cancelBackups() called."));
+        }
+        final long oldToken = Binder.clearCallingIdentity();
+        try {
+            List<Integer> operationsToCancel = new ArrayList<>();
+            synchronized (mCurrentOpLock) {
+                for (int i = 0; i < mCurrentOperations.size(); i++) {
+                    Operation op = mCurrentOperations.valueAt(i);
+                    int token = mCurrentOperations.keyAt(i);
+                    if (op.type == OP_TYPE_BACKUP) {
+                        operationsToCancel.add(token);
+                    }
+                }
+            }
+            for (Integer token : operationsToCancel) {
+                handleCancel(token, true /* cancelAll */);
+            }
+            // We don't want the backup jobs to kick in any time soon.
+            // Reschedules them to run in the distant future.
+            KeyValueBackupJob.schedule(mUserId, mContext, BUSY_BACKOFF_MIN_MILLIS, mConstants);
+            FullBackupJob.schedule(mUserId, mContext, 2 * BUSY_BACKOFF_MIN_MILLIS, mConstants);
+        } finally {
+            Binder.restoreCallingIdentity(oldToken);
+        }
+    }
+
+    /** Schedule a timeout message for the operation identified by {@code token}. */
+    public void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback,
+            int operationType) {
+        if (operationType != OP_TYPE_BACKUP_WAIT && operationType != OP_TYPE_RESTORE_WAIT) {
+            Slog.wtf(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "prepareOperationTimeout() doesn't support operation "
+                                    + Integer.toHexString(token)
+                                    + " of type "
+                                    + operationType));
+            return;
+        }
+        if (MORE_DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "starting timeout: token="
+                                    + Integer.toHexString(token)
+                                    + " interval="
+                                    + interval
+                                    + " callback="
+                                    + callback));
+        }
+
+        synchronized (mCurrentOpLock) {
+            mCurrentOperations.put(token, new Operation(OP_PENDING, callback, operationType));
+            Message msg = mBackupHandler.obtainMessage(getMessageIdForOperationType(operationType),
+                    token, 0, callback);
+            mBackupHandler.sendMessageDelayed(msg, interval);
+        }
+    }
+
+    private int getMessageIdForOperationType(int operationType) {
+        switch (operationType) {
+            case OP_TYPE_BACKUP_WAIT:
+                return MSG_BACKUP_OPERATION_TIMEOUT;
+            case OP_TYPE_RESTORE_WAIT:
+                return MSG_RESTORE_OPERATION_TIMEOUT;
+            default:
+                Slog.wtf(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "getMessageIdForOperationType called on invalid operation type: "
+                                        + operationType));
+                return -1;
+        }
+    }
+
+    /**
+     * Add an operation to the list of currently running operations. Used for cancellation,
+     * completion and timeout callbacks that act on the operation via the {@code token}.
+     */
+    public void putOperation(int token, Operation operation) {
+        if (MORE_DEBUG) {
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Adding operation token="
+                                    + Integer.toHexString(token)
+                                    + ", operation type="
+                                    + operation.type));
+        }
+        synchronized (mCurrentOpLock) {
+            mCurrentOperations.put(token, operation);
+        }
+    }
+
+    /**
+     * Remove an operation from the list of currently running operations. An operation is removed
+     * when it is completed, cancelled, or timed out, and thus no longer running.
+     */
+    public void removeOperation(int token) {
+        if (MORE_DEBUG) {
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Removing operation token=" + Integer.toHexString(token)));
+        }
+        synchronized (mCurrentOpLock) {
+            if (mCurrentOperations.get(token) == null) {
+                Slog.w(TAG, addUserIdToLogMessage(mUserId, "Duplicate remove for operation. token="
+                        + Integer.toHexString(token)));
+            }
+            mCurrentOperations.remove(token);
+        }
+    }
+
+    /** Block until we received an operation complete message (from the agent or cancellation). */
+    public boolean waitUntilOperationComplete(int token) {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Blocking until operation complete for "
+                    + Integer.toHexString(token)));
+        }
+        int finalState = OP_PENDING;
+        Operation op = null;
+        synchronized (mCurrentOpLock) {
+            while (true) {
+                op = mCurrentOperations.get(token);
+                if (op == null) {
+                    // mysterious disappearance: treat as success with no callback
+                    break;
+                } else {
+                    if (op.state == OP_PENDING) {
+                        try {
+                            mCurrentOpLock.wait();
+                        } catch (InterruptedException e) {
+                        }
+                        // When the wait is notified we loop around and recheck the current state
+                    } else {
+                        if (MORE_DEBUG) {
+                            Slog.d(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId,
+                                            "Unblocked waiting for operation token="
+                                                    + Integer.toHexString(token)));
+                        }
+                        // No longer pending; we're done
+                        finalState = op.state;
+                        break;
+                    }
+                }
+            }
+        }
+
+        removeOperation(token);
+        if (op != null) {
+            mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
+        }
+        if (MORE_DEBUG) {
+            Slog.v(TAG, addUserIdToLogMessage(mUserId, "operation " + Integer.toHexString(token)
+                    + " complete: finalState=" + finalState));
+        }
+        return finalState == OP_ACKNOWLEDGED;
+    }
+
+    /** Cancel the operation associated with {@code token}. */
+    public void handleCancel(int token, boolean cancelAll) {
+        // Notify any synchronous waiters
+        Operation op = null;
+        synchronized (mCurrentOpLock) {
+            op = mCurrentOperations.get(token);
+            if (MORE_DEBUG) {
+                if (op == null) {
+                    Slog.w(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Cancel of token "
+                                            + Integer.toHexString(token)
+                                            + " but no op found"));
+                }
+            }
+            int state = (op != null) ? op.state : OP_TIMEOUT;
+            if (state == OP_ACKNOWLEDGED) {
+                // The operation finished cleanly, so we have nothing more to do.
+                if (DEBUG) {
+                    Slog.w(TAG, addUserIdToLogMessage(mUserId, "Operation already got an ack."
+                            + "Should have been removed from mCurrentOperations."));
+                }
+                op = null;
+                mCurrentOperations.delete(token);
+            } else if (state == OP_PENDING) {
+                if (DEBUG) {
+                    Slog.v(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Cancel: token=" + Integer.toHexString(token)));
+                }
+                op.state = OP_TIMEOUT;
+                // Can't delete op from mCurrentOperations here. waitUntilOperationComplete may be
+                // called after we receive cancel here. We need this op's state there.
+
+                // Remove all pending timeout messages of types OP_TYPE_BACKUP_WAIT and
+                // OP_TYPE_RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and
+                // doesn't require cancellation.
+                if (op.type == OP_TYPE_BACKUP_WAIT || op.type == OP_TYPE_RESTORE_WAIT) {
+                    mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
+                }
+            }
+            mCurrentOpLock.notifyAll();
+        }
+
+        // If there's a TimeoutHandler for this event, call it
+        if (op != null && op.callback != null) {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, addUserIdToLogMessage(mUserId, "   Invoking cancel on " + op.callback));
+            }
+            op.callback.handleCancel(cancelAll);
+        }
+    }
+
+    /** Returns {@code true} if a backup is currently running, else returns {@code false}. */
+    public boolean isBackupOperationInProgress() {
+        synchronized (mCurrentOpLock) {
+            for (int i = 0; i < mCurrentOperations.size(); i++) {
+                Operation op = mCurrentOperations.valueAt(i);
+                if (op.type == OP_TYPE_BACKUP && op.state == OP_PENDING) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** Unbind the backup agent and kill the app if it's a non-system app. */
+    public void tearDownAgentAndKill(ApplicationInfo app) {
+        if (app == null) {
+            // Null means the system package, so just quietly move on.  :)
+            return;
+        }
+
+        try {
+            // unbind and tidy up even on timeout or failure, just in case
+            mActivityManager.unbindBackupAgent(app);
+
+            // The agent was running with a stub Application object, so shut it down.
+            // !!! We hardcode the confirmation UI's package name here rather than use a
+            //     manifest flag!  TODO something less direct.
+            if (!UserHandle.isCore(app.uid)
+                    && !app.packageName.equals("com.android.backupconfirm")) {
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, addUserIdToLogMessage(mUserId, "Killing agent host process"));
+                }
+                mActivityManager.killApplicationProcess(app.processName, app.uid);
+            } else {
+                if (MORE_DEBUG) {
+                    Slog.d(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Not killing after operation: " + app.processName));
+                }
+            }
+        } catch (RemoteException e) {
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Lost app trying to shut down"));
+        }
+    }
+
+    /** For adb backup/restore. */
+    public boolean deviceIsEncrypted() {
+        try {
+            return mStorageManager.getEncryptionState()
+                    != StorageManager.ENCRYPTION_STATE_NONE
+                    && mStorageManager.getPasswordType()
+                    != StorageManager.CRYPT_TYPE_DEFAULT;
+        } catch (Exception e) {
+            // If we can't talk to the storagemanager service we have a serious problem; fail
+            // "secure" i.e. assuming that the device is encrypted.
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Unable to communicate with storagemanager service: "
+                                    + e.getMessage()));
+            return true;
+        }
+    }
+
+    // ----- Full-data backup scheduling -----
+
+    /**
+     * Schedule a job to tell us when it's a good time to run a full backup
+     */
+    public void scheduleNextFullBackupJob(long transportMinLatency) {
+        synchronized (mQueueLock) {
+            if (mFullBackupQueue.size() > 0) {
+                // schedule the next job at the point in the future when the least-recently
+                // backed up app comes due for backup again; or immediately if it's already
+                // due.
+                final long upcomingLastBackup = mFullBackupQueue.get(0).lastBackup;
+                final long timeSinceLast = System.currentTimeMillis() - upcomingLastBackup;
+                final long interval = mConstants.getFullBackupIntervalMilliseconds();
+                final long appLatency = (timeSinceLast < interval) ? (interval - timeSinceLast) : 0;
+                final long latency = Math.max(transportMinLatency, appLatency);
+                FullBackupJob.schedule(mUserId, mContext, latency, mConstants);
+            } else {
+                if (DEBUG_SCHEDULING) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Full backup queue empty; not scheduling"));
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove a package from the full-data queue.
+     */
+    @GuardedBy("mQueueLock")
+    private void dequeueFullBackupLocked(String packageName) {
+        final int numPackages = mFullBackupQueue.size();
+        for (int i = numPackages - 1; i >= 0; i--) {
+            final FullBackupEntry e = mFullBackupQueue.get(i);
+            if (packageName.equals(e.packageName)) {
+                mFullBackupQueue.remove(i);
+            }
+        }
+    }
+
+    /**
+     * Enqueue full backup for the given app, with a note about when it last ran.
+     */
+    public void enqueueFullBackup(String packageName, long lastBackedUp) {
+        FullBackupEntry newEntry = new FullBackupEntry(packageName, lastBackedUp);
+        synchronized (mQueueLock) {
+            // First, sanity check that we aren't adding a duplicate.  Slow but
+            // straightforward; we'll have at most on the order of a few hundred
+            // items in this list.
+            dequeueFullBackupLocked(packageName);
+
+            // This is also slow but easy for modest numbers of apps: work backwards
+            // from the end of the queue until we find an item whose last backup
+            // time was before this one, then insert this new entry after it.  If we're
+            // adding something new we don't bother scanning, and just prepend.
+            int which = -1;
+            if (lastBackedUp > 0) {
+                for (which = mFullBackupQueue.size() - 1; which >= 0; which--) {
+                    final FullBackupEntry entry = mFullBackupQueue.get(which);
+                    if (entry.lastBackup <= lastBackedUp) {
+                        mFullBackupQueue.add(which + 1, newEntry);
+                        break;
+                    }
+                }
+            }
+            if (which < 0) {
+                // this one is earlier than any existing one, so prepend
+                mFullBackupQueue.add(0, newEntry);
+            }
+        }
+        writeFullBackupScheduleAsync();
+    }
+
+    private boolean fullBackupAllowable(String transportName) {
+        if (!mTransportManager.isTransportRegistered(transportName)) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Transport not registered; full data backup not performed"));
+            return false;
+        }
+
+        // Don't proceed unless we have already established package metadata
+        // for the current dataset via a key/value backup pass.
+        try {
+            String transportDirName = mTransportManager.getTransportDirName(transportName);
+            File stateDir = new File(mBaseStateDir, transportDirName);
+            File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL);
+            if (pmState.length() <= 0) {
+                if (DEBUG) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Full backup requested but dataset not yet initialized"));
+                }
+                return false;
+            }
+        } catch (Exception e) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Unable to get transport name: " + e.getMessage()));
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Conditions are right for a full backup operation, so run one.  The model we use is
+     * to perform one app backup per scheduled job execution, and to reschedule the job
+     * with zero latency as long as conditions remain right and we still have work to do.
+     *
+     * <p>This is the "start a full backup operation" entry point called by the scheduled job.
+     *
+     * @return Whether ongoing work will continue.  The return value here will be passed
+     * along as the return value to the scheduled job's onStartJob() callback.
+     */
+    public boolean beginFullBackup(FullBackupJob scheduledJob) {
+        final long now = System.currentTimeMillis();
+        final long fullBackupInterval;
+        final long keyValueBackupInterval;
+        synchronized (mConstants) {
+            fullBackupInterval = mConstants.getFullBackupIntervalMilliseconds();
+            keyValueBackupInterval = mConstants.getKeyValueBackupIntervalMilliseconds();
+        }
+        FullBackupEntry entry = null;
+        long latency = fullBackupInterval;
+
+        if (!mEnabled || !mSetupComplete) {
+            // Backups are globally disabled, so don't proceed.  We also don't reschedule
+            // the job driving automatic backups; that job will be scheduled again when
+            // the user enables backup.
+            if (MORE_DEBUG) {
+                Slog.i(TAG, addUserIdToLogMessage(mUserId, "beginFullBackup but enabled=" + mEnabled
+                        + " setupComplete=" + mSetupComplete + "; ignoring"));
+            }
+            return false;
+        }
+
+        // Don't run the backup if we're in battery saver mode, but reschedule
+        // to try again in the not-so-distant future.
+        final PowerSaveState result =
+                mPowerManager.getPowerSaveState(ServiceType.FULL_BACKUP);
+        if (result.batterySaverEnabled) {
+            if (DEBUG) {
+                Slog.i(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Deferring scheduled full backups in battery saver mode"));
+            }
+            FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval, mConstants);
+            return false;
+        }
+
+        if (DEBUG_SCHEDULING) {
+            Slog.i(
+                    TAG,
+                    addUserIdToLogMessage(mUserId, "Beginning scheduled full backup operation"));
+        }
+
+        // Great; we're able to run full backup jobs now.  See if we have any work to do.
+        synchronized (mQueueLock) {
+            if (mRunningFullBackupTask != null) {
+                Slog.e(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Backup triggered but one already/still running!"));
+                return false;
+            }
+
+            // At this point we think that we have work to do, but possibly not right now.
+            // Any exit without actually running backups will also require that we
+            // reschedule the job.
+            boolean runBackup = true;
+            boolean headBusy;
+
+            do {
+                // Recheck each time, because culling due to ineligibility may
+                // have emptied the queue.
+                if (mFullBackupQueue.size() == 0) {
+                    // no work to do so just bow out
+                    if (DEBUG) {
+                        Slog.i(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Backup queue empty; doing nothing"));
+                    }
+                    runBackup = false;
+                    break;
+                }
+
+                headBusy = false;
+
+                String transportName = mTransportManager.getCurrentTransportName();
+                if (!fullBackupAllowable(transportName)) {
+                    if (MORE_DEBUG) {
+                        Slog.i(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Preconditions not met; not running full backup"));
+                    }
+                    runBackup = false;
+                    // Typically this means we haven't run a key/value backup yet.  Back off
+                    // full-backup operations by the key/value job's run interval so that
+                    // next time we run, we are likely to be able to make progress.
+                    latency = keyValueBackupInterval;
+                }
+
+                if (runBackup) {
+                    entry = mFullBackupQueue.get(0);
+                    long timeSinceRun = now - entry.lastBackup;
+                    runBackup = (timeSinceRun >= fullBackupInterval);
+                    if (!runBackup) {
+                        // It's too early to back up the next thing in the queue, so bow out
+                        if (MORE_DEBUG) {
+                            Slog.i(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId,
+                                            "Device ready but too early to back up next app"));
+                        }
+                        // Wait until the next app in the queue falls due for a full data backup
+                        latency = fullBackupInterval - timeSinceRun;
+                        break;  // we know we aren't doing work yet, so bail.
+                    }
+
+                    try {
+                        PackageInfo appInfo = mPackageManager.getPackageInfoAsUser(
+                                entry.packageName, 0, mUserId);
+                        if (!AppBackupUtils.appGetsFullBackup(appInfo)) {
+                            // The head app isn't supposed to get full-data backups [any more];
+                            // so we cull it and force a loop around to consider the new head
+                            // app.
+                            if (MORE_DEBUG) {
+                                Slog.i(
+                                        TAG,
+                                        addUserIdToLogMessage(
+                                                mUserId,
+                                                "Culling package "
+                                                        + entry.packageName
+                                                        + " in full-backup queue but not"
+                                                        + " eligible"));
+                            }
+                            mFullBackupQueue.remove(0);
+                            headBusy = true; // force the while() condition
+                            continue;
+                        }
+
+                        final int privFlags = appInfo.applicationInfo.privateFlags;
+                        headBusy = (privFlags & PRIVATE_FLAG_BACKUP_IN_FOREGROUND) == 0
+                                && mActivityManagerInternal.isAppForeground(
+                                        appInfo.applicationInfo.uid);
+
+                        if (headBusy) {
+                            final long nextEligible = System.currentTimeMillis()
+                                    + BUSY_BACKOFF_MIN_MILLIS
+                                    + mTokenGenerator.nextInt(BUSY_BACKOFF_FUZZ);
+                            if (DEBUG_SCHEDULING) {
+                                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                                Slog.i(
+                                        TAG,
+                                        addUserIdToLogMessage(
+                                                mUserId,
+                                                "Full backup time but "
+                                                        + entry.packageName
+                                                        + " is busy; deferring to "
+                                                        + sdf.format(new Date(nextEligible))));
+                            }
+                            // This relocates the app's entry from the head of the queue to
+                            // its order-appropriate position further down, so upon looping
+                            // a new candidate will be considered at the head.
+                            enqueueFullBackup(entry.packageName, nextEligible - fullBackupInterval);
+                        }
+                    } catch (NameNotFoundException nnf) {
+                        // So, we think we want to back this up, but it turns out the package
+                        // in question is no longer installed.  We want to drop it from the
+                        // queue entirely and move on, but if there's nothing else in the queue
+                        // we should bail entirely.  headBusy cannot have been set to true yet.
+                        runBackup = (mFullBackupQueue.size() > 1);
+                    }
+                }
+            } while (headBusy);
+
+            if (!runBackup) {
+                if (DEBUG_SCHEDULING) {
+                    Slog.i(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Nothing pending full backup; rescheduling +" + latency));
+                }
+                final long deferTime = latency;     // pin for the closure
+                FullBackupJob.schedule(mUserId, mContext, deferTime, mConstants);
+                return false;
+            }
+
+            // Okay, the top thing is ready for backup now.  Do it.
+            mFullBackupQueue.remove(0);
+            CountDownLatch latch = new CountDownLatch(1);
+            String[] pkg = new String[]{entry.packageName};
+            mRunningFullBackupTask = PerformFullTransportBackupTask.newWithCurrentTransport(
+                    this,
+                    /* observer */ null,
+                    pkg,
+                    /* updateSchedule */ true,
+                    scheduledJob,
+                    latch,
+                    /* backupObserver */ null,
+                    /* monitor */ null,
+                    /* userInitiated */ false,
+                    "BMS.beginFullBackup()");
+            // Acquiring wakelock for PerformFullTransportBackupTask before its start.
+            mWakelock.acquire();
+            (new Thread(mRunningFullBackupTask)).start();
+        }
+
+        return true;
+    }
+
+    /**
+     * The job scheduler says our constraints don't hold anymore, so tear down any ongoing backup
+     * task right away.
+     */
+    public void endFullBackup() {
+        // offload the mRunningFullBackupTask.handleCancel() call to another thread,
+        // as we might have to wait for mCancelLock
+        Runnable endFullBackupRunnable = new Runnable() {
+            @Override
+            public void run() {
+                PerformFullTransportBackupTask pftbt = null;
+                synchronized (mQueueLock) {
+                    if (mRunningFullBackupTask != null) {
+                        pftbt = mRunningFullBackupTask;
+                    }
+                }
+                if (pftbt != null) {
+                    if (DEBUG_SCHEDULING) {
+                        Slog.i(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Telling running backup to stop"));
+                    }
+                    pftbt.handleCancel(true);
+                }
+            }
+        };
+        new Thread(endFullBackupRunnable, "end-full-backup").start();
+    }
+
+    /** Used by both incremental and full restore to restore widget data. */
+    public void restoreWidgetData(String packageName, byte[] widgetData) {
+        // Apply the restored widget state and generate the ID update for the app
+        if (MORE_DEBUG) {
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Incorporating restored widget data"));
+        }
+        AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId);
+    }
+
+    // *****************************
+    // NEW UNIFIED RESTORE IMPLEMENTATION
+    // *****************************
+
+    /** Schedule a backup pass for {@code packageName}. */
+    public void dataChangedImpl(String packageName) {
+        HashSet<String> targets = dataChangedTargets(packageName);
+        dataChangedImpl(packageName, targets);
+    }
+
+    private void dataChangedImpl(String packageName, HashSet<String> targets) {
+        // Record that we need a backup pass for the caller.  Since multiple callers
+        // may share a uid, we need to note all candidates within that uid and schedule
+        // a backup pass for each of them.
+        if (targets == null) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "dataChanged but no participant pkg='"
+                                    + packageName
+                                    + "'"
+                                    + " uid="
+                                    + Binder.getCallingUid()));
+            return;
+        }
+
+        synchronized (mQueueLock) {
+            // Note that this client has made data changes that need to be backed up
+            if (targets.contains(packageName)) {
+                // Add the caller to the set of pending backups.  If there is
+                // one already there, then overwrite it, but no harm done.
+                BackupRequest req = new BackupRequest(packageName);
+                if (mPendingBackups.put(packageName, req) == null) {
+                    if (MORE_DEBUG) {
+                        Slog.d(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "Now staging backup of " + packageName));
+                    }
+
+                    // Journal this request in case of crash.  The put()
+                    // operation returned null when this package was not already
+                    // in the set; we want to avoid touching the disk redundantly.
+                    writeToJournalLocked(packageName);
+                }
+            }
+        }
+
+        // ...and schedule a backup pass if necessary
+        KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+    }
+
+    // Note: packageName is currently unused, but may be in the future
+    private HashSet<String> dataChangedTargets(String packageName) {
+        // If the caller does not hold the BACKUP permission, it can only request a
+        // backup of its own data.
+        if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(),
+                Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) {
+            synchronized (mBackupParticipants) {
+                return mBackupParticipants.get(Binder.getCallingUid());
+            }
+        }
+
+        // a caller with full permission can ask to back up any participating app
+        if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) {
+            return Sets.newHashSet(PACKAGE_MANAGER_SENTINEL);
+        } else {
+            synchronized (mBackupParticipants) {
+                return SparseArrayUtils.union(mBackupParticipants);
+            }
+        }
+    }
+
+    private void writeToJournalLocked(String str) {
+        try {
+            if (mJournal == null) mJournal = DataChangedJournal.newJournal(mJournalDir);
+            mJournal.addPackage(str);
+        } catch (IOException e) {
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(mUserId, "Can't write " + str + " to backup journal"),
+                    e);
+            mJournal = null;
+        }
+    }
+
+    // ----- IBackupManager binder interface -----
+
+    /** Sent from an app's backup agent to let the service know that there's new data to backup. */
+    public void dataChanged(final String packageName) {
+        final HashSet<String> targets = dataChangedTargets(packageName);
+        if (targets == null) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "dataChanged but no participant pkg='"
+                                    + packageName
+                                    + "'"
+                                    + " uid="
+                                    + Binder.getCallingUid()));
+            return;
+        }
+
+        mBackupHandler.post(new Runnable() {
+            public void run() {
+                dataChangedImpl(packageName, targets);
+            }
+        });
+    }
+
+    /** Run an initialize operation for the given transport. */
+    public void initializeTransports(String[] transportNames, IBackupObserver observer) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "initializeTransport");
+        Slog.v(
+                TAG,
+                addUserIdToLogMessage(
+                        mUserId, "initializeTransport(): " + Arrays.asList(transportNames)));
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            mWakelock.acquire();
+            OnTaskFinishedListener listener = caller -> mWakelock.release();
+            mBackupHandler.post(
+                    new PerformInitializeTask(this, transportNames, observer, listener));
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * Sets the work profile serial number of the ancestral work profile.
+     */
+    public void setAncestralSerialNumber(long ancestralSerialNumber) {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
+                "setAncestralSerialNumber");
+        Slog.v(
+                TAG,
+                addUserIdToLogMessage(
+                        mUserId, "Setting ancestral work profile id to " + ancestralSerialNumber));
+        try (RandomAccessFile af = getAncestralSerialNumberFile()) {
+            af.writeLong(ancestralSerialNumber);
+        } catch (IOException e) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Unable to write to work profile serial mapping file:"),
+                    e);
+        }
+    }
+
+    /**
+     * Returns the work profile serial number of the ancestral device. This will be set by
+     * {@link #setAncestralSerialNumber(long)}. Will return {@code -1} if not set.
+     */
+    public long getAncestralSerialNumber() {
+        try (RandomAccessFile af = getAncestralSerialNumberFile()) {
+            return af.readLong();
+        } catch (IOException e) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "Unable to write to work profile serial number file:"),
+                    e);
+            return -1;
+        }
+    }
+
+    private RandomAccessFile getAncestralSerialNumberFile() throws FileNotFoundException {
+        if (mAncestralSerialNumberFile == null) {
+            mAncestralSerialNumberFile = new File(
+                UserBackupManagerFiles.getBaseStateDir(getUserId()),
+                SERIAL_ID_FILE);
+            FileUtils.createNewFile(mAncestralSerialNumberFile);
+        }
+        return new RandomAccessFile(mAncestralSerialNumberFile, "rwd");
+    }
+
+    @VisibleForTesting
+    void setAncestralSerialNumberFile(File ancestralSerialNumberFile) {
+        mAncestralSerialNumberFile = ancestralSerialNumberFile;
+    }
+
+
+    /** Clear the given package's backup data from the current transport. */
+    public void clearBackupData(String transportName, String packageName) {
+        if (DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "clearBackupData() of " + packageName + " on " + transportName));
+        }
+
+        PackageInfo info;
+        try {
+            info = mPackageManager.getPackageInfoAsUser(packageName,
+                    PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+        } catch (NameNotFoundException e) {
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "No such package '" + packageName + "' - not clearing backup data"));
+            return;
+        }
+
+        // If the caller does not hold the BACKUP permission, it can only request a
+        // wipe of its own backed-up data.
+        Set<String> apps;
+        if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(),
+                Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) {
+            apps = mBackupParticipants.get(Binder.getCallingUid());
+        } else {
+            // a caller with full permission can ask to back up any participating app
+            // !!! TODO: allow data-clear of ANY app?
+            if (MORE_DEBUG) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Privileged caller, allowing clear of other apps"));
+            }
+            apps = mProcessedPackagesJournal.getPackagesCopy();
+        }
+
+        if (apps.contains(packageName)) {
+            // found it; fire off the clear request
+            if (MORE_DEBUG) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(mUserId, "Found the app - running clear process"));
+            }
+            mBackupHandler.removeMessages(MSG_RETRY_CLEAR);
+            synchronized (mQueueLock) {
+                TransportClient transportClient =
+                        mTransportManager
+                                .getTransportClient(transportName, "BMS.clearBackupData()");
+                if (transportClient == null) {
+                    // transport is currently unregistered -- make sure to retry
+                    Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR,
+                            new ClearRetryParams(transportName, packageName));
+                    mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL);
+                    return;
+                }
+                long oldId = Binder.clearCallingIdentity();
+                OnTaskFinishedListener listener =
+                        caller ->
+                                mTransportManager.disposeOfTransportClient(transportClient, caller);
+                mWakelock.acquire();
+                Message msg = mBackupHandler.obtainMessage(
+                        MSG_RUN_CLEAR,
+                        new ClearParams(transportClient, info, listener));
+                mBackupHandler.sendMessage(msg);
+                Binder.restoreCallingIdentity(oldId);
+            }
+        }
+    }
+
+    /**
+     * Run a backup pass immediately for any applications that have declared that they have pending
+     * updates.
+     */
+    public void backupNow() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow");
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            final PowerSaveState result =
+                    mPowerManager.getPowerSaveState(ServiceType.KEYVALUE_BACKUP);
+            if (result.batterySaverEnabled) {
+                if (DEBUG) {
+                    Slog.v(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Not running backup while in battery save mode"));
+                }
+                // Try again in several hours.
+                KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+            } else {
+                if (DEBUG) {
+                    Slog.v(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass"));
+                }
+
+                synchronized (getQueueLock()) {
+                    if (getPendingInits().size() > 0) {
+                        // If there are pending init operations, we process those and then settle
+                        // into the usual periodic backup schedule.
+                        if (MORE_DEBUG) {
+                            Slog.v(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId, "Init pending at scheduled backup"));
+                        }
+                        try {
+                            getAlarmManager().cancel(mRunInitIntent);
+                            mRunInitIntent.send();
+                        } catch (PendingIntent.CanceledException ce) {
+                            Slog.w(
+                                    TAG,
+                                    addUserIdToLogMessage(mUserId, "Run init intent cancelled"));
+                        }
+                        return;
+                    }
+                }
+
+                // Don't run backups if we're disabled or not yet set up.
+                if (!isEnabled() || !isSetupComplete()) {
+                    Slog.w(
+                            TAG,
+                            addUserIdToLogMessage(mUserId, "Backup pass but enabled="  + isEnabled()
+                                    + " setupComplete=" + isSetupComplete()));
+                    return;
+                }
+
+                // Fire the msg that kicks off the whole shebang...
+                Message message = mBackupHandler.obtainMessage(MSG_RUN_BACKUP);
+                mBackupHandler.sendMessage(message);
+                // ...and cancel any pending scheduled job, because we've just superseded it
+                KeyValueBackupJob.cancel(mUserId, mContext);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * Used by 'adb backup' to run a backup pass for packages supplied via the command line, writing
+     * the resulting data stream to the supplied {@code fd}. This method is synchronous and does not
+     * return to the caller until the backup has been completed. It requires on-screen confirmation
+     * by the user.
+     */
+    public void adbBackup(ParcelFileDescriptor fd, boolean includeApks,
+            boolean includeObbs, boolean includeShared, boolean doWidgets, boolean doAllApps,
+            boolean includeSystem, boolean compress, boolean doKeyValue, String[] pkgList) {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbBackup");
+
+        final int callingUserHandle = UserHandle.getCallingUserId();
+        if (callingUserHandle != UserHandle.USER_SYSTEM) {
+            throw new IllegalStateException("Backup supported only for the device owner");
+        }
+
+        // Validate
+        if (!doAllApps) {
+            if (!includeShared) {
+                // If we're backing up shared data (sdcard or equivalent), then we can run
+                // without any supplied app names.  Otherwise, we'd be doing no work, so
+                // report the error.
+                if (pkgList == null || pkgList.length == 0) {
+                    throw new IllegalArgumentException(
+                            "Backup requested but neither shared nor any apps named");
+                }
+            }
+        }
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            if (!mSetupComplete) {
+                Slog.i(TAG, addUserIdToLogMessage(mUserId, "Backup not supported before setup"));
+                return;
+            }
+
+            if (DEBUG) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Requesting backup: apks="
+                                        + includeApks
+                                        + " obb="
+                                        + includeObbs
+                                        + " shared="
+                                        + includeShared
+                                        + " all="
+                                        + doAllApps
+                                        + " system="
+                                        + includeSystem
+                                        + " includekeyvalue="
+                                        + doKeyValue
+                                        + " pkgs="
+                                        + pkgList));
+            }
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning adb backup..."));
+
+            AdbBackupParams params = new AdbBackupParams(fd, includeApks, includeObbs,
+                    includeShared, doWidgets, doAllApps, includeSystem, compress, doKeyValue,
+                    pkgList);
+            final int token = generateRandomIntegerToken();
+            synchronized (mAdbBackupRestoreConfirmations) {
+                mAdbBackupRestoreConfirmations.put(token, params);
+            }
+
+            // start up the confirmation UI
+            if (DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Starting backup confirmation UI, token=" + token));
+            }
+            if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {
+                Slog.e(
+                        TAG,
+                        addUserIdToLogMessage(mUserId, "Unable to launch backup confirmation UI"));
+                mAdbBackupRestoreConfirmations.delete(token);
+                return;
+            }
+
+            // make sure the screen is lit for the user interaction
+            mPowerManager.userActivity(SystemClock.uptimeMillis(),
+                    PowerManager.USER_ACTIVITY_EVENT_OTHER,
+                    0);
+
+            // start the confirmation countdown
+            startConfirmationTimeout(token, params);
+
+            // wait for the backup to be performed
+            if (DEBUG) {
+                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for backup completion..."));
+            }
+            waitForCompletion(params);
+        } finally {
+            try {
+                fd.close();
+            } catch (IOException e) {
+                Slog.e(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "IO error closing output for adb backup: " + e.getMessage()));
+            }
+            Binder.restoreCallingIdentity(oldId);
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Adb backup processing complete."));
+        }
+    }
+
+    /** Run a full backup pass for the given packages. Used by 'adb shell bmgr'. */
+    public void fullTransportBackup(String[] pkgNames) {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
+                "fullTransportBackup");
+
+        final int callingUserHandle = UserHandle.getCallingUserId();
+        // TODO: http://b/22388012
+        if (callingUserHandle != UserHandle.USER_SYSTEM) {
+            throw new IllegalStateException("Restore supported only for the device owner");
+        }
+
+        String transportName = mTransportManager.getCurrentTransportName();
+        if (!fullBackupAllowable(transportName)) {
+            Slog.i(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Full backup not currently possible -- key/value backup not yet run?"));
+        } else {
+            if (DEBUG) {
+                Slog.d(TAG, addUserIdToLogMessage(mUserId, "fullTransportBackup()"));
+            }
+
+            final long oldId = Binder.clearCallingIdentity();
+            try {
+                CountDownLatch latch = new CountDownLatch(1);
+                Runnable task = PerformFullTransportBackupTask.newWithCurrentTransport(
+                        this,
+                        /* observer */ null,
+                        pkgNames,
+                        /* updateSchedule */ false,
+                        /* runningJob */ null,
+                        latch,
+                        /* backupObserver */ null,
+                        /* monitor */ null,
+                        /* userInitiated */ false,
+                        "BMS.fullTransportBackup()");
+                // Acquiring wakelock for PerformFullTransportBackupTask before its start.
+                mWakelock.acquire();
+                (new Thread(task, "full-transport-master")).start();
+                do {
+                    try {
+                        latch.await();
+                        break;
+                    } catch (InterruptedException e) {
+                        // Just go back to waiting for the latch to indicate completion
+                    }
+                } while (true);
+
+                // We just ran a backup on these packages, so kick them to the end of the queue
+                final long now = System.currentTimeMillis();
+                for (String pkg : pkgNames) {
+                    enqueueFullBackup(pkg, now);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(oldId);
+            }
+        }
+
+        if (DEBUG) {
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Done with full transport backup."));
+        }
+    }
+
+    /**
+     * Used by 'adb restore' to run a restore pass, blocking until completion. Requires user
+     * confirmation.
+     */
+    public void adbRestore(ParcelFileDescriptor fd) {
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbRestore");
+
+        final int callingUserHandle = UserHandle.getCallingUserId();
+        if (callingUserHandle != UserHandle.USER_SYSTEM) {
+            throw new IllegalStateException("Restore supported only for the device owner");
+        }
+
+        long oldId = Binder.clearCallingIdentity();
+
+        try {
+            if (!mSetupComplete) {
+                Slog.i(
+                        TAG,
+                        addUserIdToLogMessage(mUserId, "Full restore not permitted before setup"));
+                return;
+            }
+
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning restore..."));
+
+            AdbRestoreParams params = new AdbRestoreParams(fd);
+            final int token = generateRandomIntegerToken();
+            synchronized (mAdbBackupRestoreConfirmations) {
+                mAdbBackupRestoreConfirmations.put(token, params);
+            }
+
+            // start up the confirmation UI
+            if (DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Starting restore confirmation UI, token=" + token));
+            }
+            if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) {
+                Slog.e(
+                        TAG,
+                        addUserIdToLogMessage(mUserId, "Unable to launch restore confirmation"));
+                mAdbBackupRestoreConfirmations.delete(token);
+                return;
+            }
+
+            // make sure the screen is lit for the user interaction
+            mPowerManager.userActivity(SystemClock.uptimeMillis(),
+                    PowerManager.USER_ACTIVITY_EVENT_OTHER,
+                    0);
+
+            // start the confirmation countdown
+            startConfirmationTimeout(token, params);
+
+            // wait for the restore to be performed
+            if (DEBUG) {
+                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for restore completion..."));
+            }
+            waitForCompletion(params);
+        } finally {
+            try {
+                fd.close();
+            } catch (IOException e) {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Error trying to close fd after adb restore: " + e));
+            }
+            Binder.restoreCallingIdentity(oldId);
+            Slog.i(TAG, addUserIdToLogMessage(mUserId, "adb restore processing complete."));
+        }
+    }
+
+    /**
+     * Excludes keys from KV restore for a given package. The keys won't be part of the data passed
+     * to the backup agent during restore.
+     */
+    public void excludeKeysFromRestore(String packageName, List<String> keys) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "excludeKeysFromRestore");
+        mBackupPreferences.addExcludedKeys(packageName, keys);
+    }
+
+    private boolean startConfirmationUi(int token, String action) {
+        try {
+            Intent confIntent = new Intent(action);
+            confIntent.setClassName("com.android.backupconfirm",
+                    "com.android.backupconfirm.BackupRestoreConfirmation");
+            confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token);
+            confIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            mContext.startActivityAsUser(confIntent, UserHandle.SYSTEM);
+        } catch (ActivityNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private void startConfirmationTimeout(int token, AdbParams params) {
+        if (MORE_DEBUG) {
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Posting conf timeout msg after "
+                    + TIMEOUT_FULL_CONFIRMATION + " millis"));
+        }
+        Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT,
+                token, 0, params);
+        mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION);
+    }
+
+    private void waitForCompletion(AdbParams params) {
+        synchronized (params.latch) {
+            while (!params.latch.get()) {
+                try {
+                    params.latch.wait();
+                } catch (InterruptedException e) { /* never interrupted */ }
+            }
+        }
+    }
+
+    /** Called when adb backup/restore has completed. */
+    public void signalAdbBackupRestoreCompletion(AdbParams params) {
+        synchronized (params.latch) {
+            params.latch.set(true);
+            params.latch.notifyAll();
+        }
+    }
+
+    /**
+     * Confirm that the previously-requested full backup/restore operation can proceed. This is used
+     * to require a user-facing disclosure about the operation.
+     */
+    public void acknowledgeAdbBackupOrRestore(int token, boolean allow,
+            String curPassword, String encPpassword, IFullBackupRestoreObserver observer) {
+        if (DEBUG) {
+            Slog.d(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "acknowledgeAdbBackupOrRestore : token=" + token + " allow=" + allow));
+        }
+
+        // TODO: possibly require not just this signature-only permission, but even
+        // require that the specific designated confirmation-UI app uid is the caller?
+        mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
+                "acknowledgeAdbBackupOrRestore");
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+
+            AdbParams params;
+            synchronized (mAdbBackupRestoreConfirmations) {
+                params = mAdbBackupRestoreConfirmations.get(token);
+                if (params != null) {
+                    mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params);
+                    mAdbBackupRestoreConfirmations.delete(token);
+
+                    if (allow) {
+                        final int verb = params instanceof AdbBackupParams
+                                ? MSG_RUN_ADB_BACKUP
+                                : MSG_RUN_ADB_RESTORE;
+
+                        params.observer = observer;
+                        params.curPassword = curPassword;
+
+                        params.encryptPassword = encPpassword;
+
+                        if (MORE_DEBUG) {
+                            Slog.d(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId, "Sending conf message with verb " + verb));
+                        }
+                        mWakelock.acquire();
+                        Message msg = mBackupHandler.obtainMessage(verb, params);
+                        mBackupHandler.sendMessage(msg);
+                    } else {
+                        Slog.w(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId, "User rejected full backup/restore operation"));
+                        // indicate completion without having actually transferred any data
+                        signalAdbBackupRestoreCompletion(params);
+                    }
+                } else {
+                    Slog.w(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId,
+                                    "Attempted to ack full backup/restore with invalid token"));
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /** User-configurable enabling/disabling of backups. */
+    public void setBackupEnabled(boolean enable) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "setBackupEnabled");
+
+        Slog.i(TAG, addUserIdToLogMessage(mUserId, "Backup enabled => " + enable));
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            boolean wasEnabled = mEnabled;
+            synchronized (this) {
+                UserBackupManagerFilePersistedSettings.writeBackupEnableState(mUserId, enable);
+                mEnabled = enable;
+            }
+
+            synchronized (mQueueLock) {
+                if (enable && !wasEnabled && mSetupComplete) {
+                    // if we've just been enabled, start scheduling backup passes
+                    KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+                    scheduleNextFullBackupJob(0);
+                } else if (!enable) {
+                    // No longer enabled, so stop running backups
+                    if (MORE_DEBUG) {
+                        Slog.i(TAG, addUserIdToLogMessage(mUserId, "Opting out of backup"));
+                    }
+
+                    KeyValueBackupJob.cancel(mUserId, mContext);
+
+                    // This also constitutes an opt-out, so we wipe any data for
+                    // this device from the backend.  We start that process with
+                    // an alarm in order to guarantee wakelock states.
+                    if (wasEnabled && mSetupComplete) {
+                        // NOTE: we currently flush every registered transport, not just
+                        // the currently-active one.
+                        List<String> transportNames = new ArrayList<>();
+                        List<String> transportDirNames = new ArrayList<>();
+                        mTransportManager.forEachRegisteredTransport(
+                                name -> {
+                                    final String dirName;
+                                    try {
+                                        dirName = mTransportManager.getTransportDirName(name);
+                                    } catch (TransportNotRegisteredException e) {
+                                        // Should never happen
+                                        Slog.e(
+                                                TAG,
+                                                addUserIdToLogMessage(
+                                                        mUserId,
+                                                        "Unexpected unregistered transport"),
+                                                e);
+                                        return;
+                                    }
+                                    transportNames.add(name);
+                                    transportDirNames.add(dirName);
+                                });
+
+                        // build the set of transports for which we are posting an init
+                        for (int i = 0; i < transportNames.size(); i++) {
+                            recordInitPending(
+                                    true,
+                                    transportNames.get(i),
+                                    transportDirNames.get(i));
+                        }
+                        mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
+                                mRunInitIntent);
+                    }
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /** Enable/disable automatic restore of app data at install time. */
+    public void setAutoRestore(boolean doAutoRestore) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "setAutoRestore");
+
+        Slog.i(TAG, addUserIdToLogMessage(mUserId, "Auto restore => " + doAutoRestore));
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            synchronized (this) {
+                Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                        Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0, mUserId);
+                mAutoRestore = doAutoRestore;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /** Report whether the backup mechanism is currently enabled. */
+    public boolean isBackupEnabled() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "isBackupEnabled");
+        return mEnabled;    // no need to synchronize just to read it
+    }
+
+    /** Report the name of the currently active transport. */
+    public String getCurrentTransport() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "getCurrentTransport");
+        String currentTransport = mTransportManager.getCurrentTransportName();
+        if (MORE_DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId, "... getCurrentTransport() returning " + currentTransport));
+        }
+        return currentTransport;
+    }
+
+    /**
+     * Returns the {@link ComponentName} of the host service of the selected transport or {@code
+     * null} if no transport selected or if the transport selected is not registered.
+     */
+    @Nullable
+    public ComponentName getCurrentTransportComponent() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "getCurrentTransportComponent");
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            return mTransportManager.getCurrentTransportComponent();
+        } catch (TransportNotRegisteredException e) {
+            return null;
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /** Report all known, available backup transports by name. */
+    public String[] listAllTransports() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "listAllTransports");
+
+        return mTransportManager.getRegisteredTransportNames();
+    }
+
+    /** Report all known, available backup transports by component. */
+    public ComponentName[] listAllTransportComponents() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "listAllTransportComponents");
+        return mTransportManager.getRegisteredTransportComponents();
+    }
+
+    /**
+     * Update the attributes of the transport identified by {@code transportComponent}. If the
+     * specified transport has not been bound at least once (for registration), this call will be
+     * ignored. Only the host process of the transport can change its description, otherwise a
+     * {@link SecurityException} will be thrown.
+     *
+     * @param transportComponent The identity of the transport being described.
+     * @param name A {@link String} with the new name for the transport. This is NOT for
+     *     identification. MUST NOT be {@code null}.
+     * @param configurationIntent An {@link Intent} that can be passed to
+     *     {@link Context#startActivity} in order to launch the transport's configuration UI. It may
+     *     be {@code null} if the transport does not offer any user-facing configuration UI.
+     * @param currentDestinationString A {@link String} describing the destination to which the
+     *     transport is currently sending data. MUST NOT be {@code null}.
+     * @param dataManagementIntent An {@link Intent} that can be passed to
+     *     {@link Context#startActivity} in order to launch the transport's data-management UI. It
+     *     may be {@code null} if the transport does not offer any user-facing data
+     *     management UI.
+     * @param dataManagementLabel A {@link CharSequence} to be used as the label for the transport's
+     *     data management affordance. This MUST be {@code null} when dataManagementIntent is
+     *     {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}.
+     * @throws SecurityException If the UID of the calling process differs from the package UID of
+     *     {@code transportComponent} or if the caller does NOT have BACKUP permission.
+     */
+    public void updateTransportAttributes(
+            ComponentName transportComponent,
+            String name,
+            @Nullable Intent configurationIntent,
+            String currentDestinationString,
+            @Nullable Intent dataManagementIntent,
+            @Nullable CharSequence dataManagementLabel) {
+        updateTransportAttributes(
+                Binder.getCallingUid(),
+                transportComponent,
+                name,
+                configurationIntent,
+                currentDestinationString,
+                dataManagementIntent,
+                dataManagementLabel);
+    }
+
+    @VisibleForTesting
+    void updateTransportAttributes(
+            int callingUid,
+            ComponentName transportComponent,
+            String name,
+            @Nullable Intent configurationIntent,
+            String currentDestinationString,
+            @Nullable Intent dataManagementIntent,
+            @Nullable CharSequence dataManagementLabel) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "updateTransportAttributes");
+
+        Objects.requireNonNull(transportComponent, "transportComponent can't be null");
+        Objects.requireNonNull(name, "name can't be null");
+        Objects.requireNonNull(
+                currentDestinationString, "currentDestinationString can't be null");
+        Preconditions.checkArgument(
+                (dataManagementIntent == null) == (dataManagementLabel == null),
+                "dataManagementLabel should be null iff dataManagementIntent is null");
+
+        try {
+            int transportUid =
+                    mContext.getPackageManager()
+                            .getPackageUidAsUser(transportComponent.getPackageName(), 0, mUserId);
+            if (callingUid != transportUid) {
+                throw new SecurityException("Only the transport can change its description");
+            }
+        } catch (NameNotFoundException e) {
+            throw new SecurityException("Transport package not found", e);
+        }
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            mTransportManager.updateTransportAttributes(
+                    transportComponent,
+                    name,
+                    configurationIntent,
+                    currentDestinationString,
+                    dataManagementIntent,
+                    dataManagementLabel);
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * Selects transport {@code transportName} and returns previously selected transport.
+     *
+     * @deprecated Use {@link #selectBackupTransportAsync(ComponentName,
+     * ISelectBackupTransportCallback)} instead.
+     */
+    @Deprecated
+    @Nullable
+    public String selectBackupTransport(String transportName) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "selectBackupTransport");
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            String previousTransportName = mTransportManager.selectTransport(transportName);
+            updateStateForTransport(transportName);
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "selectBackupTransport(transport = "
+                                    + transportName
+                                    + "): previous transport = "
+                                    + previousTransportName));
+            return previousTransportName;
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * Selects transport {@code transportComponent} asynchronously and notifies {@code listener}
+     * with the result upon completion.
+     */
+    public void selectBackupTransportAsync(
+            ComponentName transportComponent, ISelectBackupTransportCallback listener) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "selectBackupTransportAsync");
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            String transportString = transportComponent.flattenToShortString();
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "selectBackupTransportAsync(transport = " + transportString + ")"));
+            mBackupHandler.post(
+                    () -> {
+                        String transportName = null;
+                        int result =
+                                mTransportManager.registerAndSelectTransport(transportComponent);
+                        if (result == BackupManager.SUCCESS) {
+                            try {
+                                transportName =
+                                        mTransportManager.getTransportName(transportComponent);
+                                updateStateForTransport(transportName);
+                            } catch (TransportNotRegisteredException e) {
+                                Slog.e(
+                                        TAG,
+                                        addUserIdToLogMessage(
+                                                mUserId, "Transport got unregistered"));
+                                result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+                            }
+                        }
+
+                        try {
+                            if (transportName != null) {
+                                listener.onSuccess(transportName);
+                            } else {
+                                listener.onFailure(result);
+                            }
+                        } catch (RemoteException e) {
+                            Slog.e(
+                                    TAG,
+                                    addUserIdToLogMessage(
+                                            mUserId,
+                                            "ISelectBackupTransportCallback listener not"
+                                                + " available"));
+                        }
+                    });
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    /**
+     * We want to skip backup/restore of certain packages if 'backup_skip_user_facing_data' is
+     * set to true in secure settings. See b/153940088 for details.
+     *
+     * TODO(b/154822946): Remove this logic in the next release.
+     */
+    public List<PackageInfo> filterUserFacingPackages(List<PackageInfo> packages) {
+        if (!shouldSkipUserFacingData()) {
+            return packages;
+        }
+
+        List<PackageInfo> filteredPackages = new ArrayList<>(packages.size());
+        for (PackageInfo packageInfo : packages)  {
+            if (!shouldSkipPackage(packageInfo.packageName)) {
+                filteredPackages.add(packageInfo);
+            } else {
+                Slog.i(TAG, "Will skip backup/restore for " + packageInfo.packageName);
+            }
+        }
+
+        return filteredPackages;
+    }
+
+    @VisibleForTesting
+    public boolean shouldSkipUserFacingData() {
+        return Settings.Secure.getInt(mContext.getContentResolver(), SKIP_USER_FACING_DATA,
+                /* def */ 0) != 0;
+    }
+
+    @VisibleForTesting
+    public boolean shouldSkipPackage(String packageName) {
+        return WALLPAPER_PACKAGE.equals(packageName);
+    }
+
+    private void updateStateForTransport(String newTransportName) {
+        // Publish the name change
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                Settings.Secure.BACKUP_TRANSPORT, newTransportName, mUserId);
+
+        // And update our current-dataset bookkeeping
+        String callerLogString = "BMS.updateStateForTransport()";
+        TransportClient transportClient =
+                mTransportManager.getTransportClient(newTransportName, callerLogString);
+        if (transportClient != null) {
+            try {
+                IBackupTransport transport = transportClient.connectOrThrow(callerLogString);
+                mCurrentToken = transport.getCurrentRestoreSet();
+            } catch (Exception e) {
+                // Oops.  We can't know the current dataset token, so reset and figure it out
+                // when we do the next k/v backup operation on this transport.
+                mCurrentToken = 0;
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Transport "
+                                        + newTransportName
+                                        + " not available: current token = 0"));
+            }
+            mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+        } else {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Transport "
+                                    + newTransportName
+                                    + " not registered: current token = 0"));
+            // The named transport isn't registered, so we can't know what its current dataset token
+            // is. Reset as above.
+            mCurrentToken = 0;
+        }
+    }
+
+    /**
+     * Supply the configuration intent for the given transport. If the name is not one of the
+     * available transports, or if the transport does not supply any configuration UI, the method
+     * returns {@code null}.
+     */
+    public Intent getConfigurationIntent(String transportName) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "getConfigurationIntent");
+        try {
+            Intent intent = mTransportManager.getTransportConfigurationIntent(transportName);
+            if (MORE_DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "getConfigurationIntent() returning intent " + intent));
+            }
+            return intent;
+        } catch (TransportNotRegisteredException e) {
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Unable to get configuration intent from transport: "
+                                    + e.getMessage()));
+            return null;
+        }
+    }
+
+    /**
+     * Supply the current destination string for the given transport. If the name is not one of the
+     * registered transports the method will return null.
+     *
+     * <p>This string is used VERBATIM as the summary text of the relevant Settings item.
+     *
+     * @param transportName The name of the registered transport.
+     * @return The current destination string or null if the transport is not registered.
+     */
+    public String getDestinationString(String transportName) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "getDestinationString");
+
+        try {
+            String string = mTransportManager.getTransportCurrentDestinationString(transportName);
+            if (MORE_DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "getDestinationString() returning " + string));
+            }
+            return string;
+        } catch (TransportNotRegisteredException e) {
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Unable to get destination string from transport: " + e.getMessage()));
+            return null;
+        }
+    }
+
+    /** Supply the manage-data intent for the given transport. */
+    public Intent getDataManagementIntent(String transportName) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "getDataManagementIntent");
+
+        try {
+            Intent intent = mTransportManager.getTransportDataManagementIntent(transportName);
+            if (MORE_DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "getDataManagementIntent() returning intent " + intent));
+            }
+            return intent;
+        } catch (TransportNotRegisteredException e) {
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Unable to get management intent from transport: " + e.getMessage()));
+            return null;
+        }
+    }
+
+    /**
+     * Supply the menu label for affordances that fire the manage-data intent for the given
+     * transport.
+     */
+    public CharSequence getDataManagementLabel(String transportName) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "getDataManagementLabel");
+
+        try {
+            CharSequence label = mTransportManager.getTransportDataManagementLabel(transportName);
+            if (MORE_DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "getDataManagementLabel() returning " + label));
+            }
+            return label;
+        } catch (TransportNotRegisteredException e) {
+            Slog.e(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Unable to get management label from transport: " + e.getMessage()));
+            return null;
+        }
+    }
+
+    /**
+     * Callback: a requested backup agent has been instantiated. This should only be called from the
+     * {@link ActivityManager}.
+     */
+    public void agentConnected(String packageName, IBinder agentBinder) {
+        synchronized (mAgentConnectLock) {
+            if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "agentConnected pkg=" + packageName + " agent=" + agentBinder));
+                mConnectedAgent = IBackupAgent.Stub.asInterface(agentBinder);
+                mConnecting = false;
+            } else {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Non-system process uid="
+                                        + Binder.getCallingUid()
+                                        + " claiming agent connected"));
+            }
+            mAgentConnectLock.notifyAll();
+        }
+    }
+
+    /**
+     * Callback: a backup agent has failed to come up, or has unexpectedly quit. If the agent failed
+     * to come up in the first place, the agentBinder argument will be {@code null}. This should
+     * only be called from the {@link ActivityManager}.
+     */
+    public void agentDisconnected(String packageName) {
+        // TODO: handle backup being interrupted
+        synchronized (mAgentConnectLock) {
+            if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+                mConnectedAgent = null;
+                mConnecting = false;
+            } else {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Non-system process uid="
+                                        + Binder.getCallingUid()
+                                        + " claiming agent disconnected"));
+            }
+            mAgentConnectLock.notifyAll();
+        }
+    }
+
+    /**
+     * An application being installed will need a restore pass, then the {@link PackageManager} will
+     * need to be told when the restore is finished.
+     */
+    public void restoreAtInstall(String packageName, int token) {
+        if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+            Slog.w(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "Non-system process uid="
+                                    + Binder.getCallingUid()
+                                    + " attemping install-time restore"));
+            return;
+        }
+
+        boolean skip = false;
+
+        long restoreSet = getAvailableRestoreToken(packageName);
+        if (DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "restoreAtInstall pkg="
+                                    + packageName
+                                    + " token="
+                                    + Integer.toHexString(token)
+                                    + " restoreSet="
+                                    + Long.toHexString(restoreSet)));
+        }
+        if (restoreSet == 0) {
+            if (MORE_DEBUG) Slog.i(TAG, addUserIdToLogMessage(mUserId, "No restore set"));
+            skip = true;
+        }
+
+        TransportClient transportClient =
+                mTransportManager.getCurrentTransportClient("BMS.restoreAtInstall()");
+        if (transportClient == null) {
+            if (DEBUG) Slog.w(TAG, addUserIdToLogMessage(mUserId, "No transport client"));
+            skip = true;
+        }
+
+        if (!mAutoRestore) {
+            if (DEBUG) {
+                Slog.w(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Non-restorable state: auto=" + mAutoRestore));
+            }
+            skip = true;
+        }
+
+        if (!skip) {
+            try {
+                // okay, we're going to attempt a restore of this package from this restore set.
+                // The eventual message back into the Package Manager to run the post-install
+                // steps for 'token' will be issued from the restore handling code.
+
+                mWakelock.acquire();
+
+                OnTaskFinishedListener listener = caller -> {
+                    mTransportManager.disposeOfTransportClient(transportClient, caller);
+                    mWakelock.release();
+                };
+
+                if (MORE_DEBUG) {
+                    Slog.d(
+                            TAG,
+                            addUserIdToLogMessage(mUserId, "Restore at install of " + packageName));
+                }
+                Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
+                msg.obj =
+                        RestoreParams.createForRestoreAtInstall(
+                                transportClient,
+                                /* observer */ null,
+                                /* monitor */ null,
+                                restoreSet,
+                                packageName,
+                                token,
+                                listener);
+                mBackupHandler.sendMessage(msg);
+            } catch (Exception e) {
+                // Calling into the transport broke; back off and proceed with the installation.
+                Slog.e(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Unable to contact transport: " + e.getMessage()));
+                skip = true;
+            }
+        }
+
+        if (skip) {
+            // Auto-restore disabled or no way to attempt a restore
+
+            if (transportClient != null) {
+                mTransportManager.disposeOfTransportClient(
+                        transportClient, "BMS.restoreAtInstall()");
+            }
+
+            // Tell the PackageManager to proceed with the post-install handling for this package.
+            if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Finishing install immediately"));
+            try {
+                mPackageManagerBinder.finishPackageInstall(token, false);
+            } catch (RemoteException e) { /* can't happen */ }
+        }
+    }
+
+    /** Hand off a restore session. */
+    public IRestoreSession beginRestoreSession(String packageName, String transport) {
+        if (DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "beginRestoreSession: pkg=" + packageName + " transport=" + transport));
+        }
+
+        boolean needPermission = true;
+        if (transport == null) {
+            transport = mTransportManager.getCurrentTransportName();
+
+            if (packageName != null) {
+                PackageInfo app = null;
+                try {
+                    app = mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId);
+                } catch (NameNotFoundException nnf) {
+                    Slog.w(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Asked to restore nonexistent pkg " + packageName));
+                    throw new IllegalArgumentException("Package " + packageName + " not found");
+                }
+
+                if (app.applicationInfo.uid == Binder.getCallingUid()) {
+                    // So: using the current active transport, and the caller has asked
+                    // that its own package will be restored.  In this narrow use case
+                    // we do not require the caller to hold the permission.
+                    needPermission = false;
+                }
+            }
+        }
+
+        if (needPermission) {
+            mContext.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.BACKUP, "beginRestoreSession");
+        } else {
+            if (DEBUG) {
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "restoring self on current transport; no permission needed"));
+            }
+        }
+
+        synchronized (this) {
+            if (mActiveRestoreSession != null) {
+                Slog.i(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Restore session requested but one already active"));
+                return null;
+            }
+            if (mBackupRunning) {
+                Slog.i(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Restore session requested but currently running backups"));
+                return null;
+            }
+            mActiveRestoreSession = new ActiveRestoreSession(this, packageName, transport);
+            mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT,
+                    mAgentTimeoutParameters.getRestoreAgentTimeoutMillis());
+        }
+        return mActiveRestoreSession;
+    }
+
+    /** Clear the specified restore session. */
+    public void clearRestoreSession(ActiveRestoreSession currentSession) {
+        synchronized (this) {
+            if (currentSession != mActiveRestoreSession) {
+                Slog.e(TAG, addUserIdToLogMessage(mUserId, "ending non-current restore session"));
+            } else {
+                if (DEBUG) {
+                    Slog.v(
+                            TAG,
+                            addUserIdToLogMessage(
+                                    mUserId, "Clearing restore session and halting timeout"));
+                }
+                mActiveRestoreSession = null;
+                mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+            }
+        }
+    }
+
+    /**
+     * Note that a currently-active backup agent has notified us that it has completed the given
+     * outstanding asynchronous backup/restore operation.
+     */
+    public void opComplete(int token, long result) {
+        if (MORE_DEBUG) {
+            Slog.v(
+                    TAG,
+                    addUserIdToLogMessage(
+                            mUserId,
+                            "opComplete: " + Integer.toHexString(token) + " result=" + result));
+        }
+        Operation op = null;
+        synchronized (mCurrentOpLock) {
+            op = mCurrentOperations.get(token);
+            if (op != null) {
+                if (op.state == OP_TIMEOUT) {
+                    // The operation already timed out, and this is a late response.  Tidy up
+                    // and ignore it; we've already dealt with the timeout.
+                    op = null;
+                    mCurrentOperations.delete(token);
+                } else if (op.state == OP_ACKNOWLEDGED) {
+                    if (DEBUG) {
+                        Slog.w(
+                                TAG,
+                                addUserIdToLogMessage(
+                                        mUserId,
+                                        "Received duplicate ack for token="
+                                                + Integer.toHexString(token)));
+                    }
+                    op = null;
+                    mCurrentOperations.remove(token);
+                } else if (op.state == OP_PENDING) {
+                    // Can't delete op from mCurrentOperations. waitUntilOperationComplete can be
+                    // called after we we receive this call.
+                    op.state = OP_ACKNOWLEDGED;
+                }
+            }
+            mCurrentOpLock.notifyAll();
+        }
+
+        // The completion callback, if any, is invoked on the handler
+        if (op != null && op.callback != null) {
+            Pair<BackupRestoreTask, Long> callbackAndResult = Pair.create(op.callback, result);
+            Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, callbackAndResult);
+            mBackupHandler.sendMessage(msg);
+        }
+    }
+
+    /** Checks if the package is eligible for backup. */
+    public boolean isAppEligibleForBackup(String packageName) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "isAppEligibleForBackup");
+
+        long oldToken = Binder.clearCallingIdentity();
+        try {
+            String callerLogString = "BMS.isAppEligibleForBackup";
+            TransportClient transportClient =
+                    mTransportManager.getCurrentTransportClient(callerLogString);
+            boolean eligible =
+                    AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(
+                            transportClient, packageName, mPackageManager, mUserId);
+            if (transportClient != null) {
+                mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+            }
+            return eligible;
+        } finally {
+            Binder.restoreCallingIdentity(oldToken);
+        }
+    }
+
+    /** Returns the inputted packages that are eligible for backup. */
+    public String[] filterAppsEligibleForBackup(String[] packages) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP, "filterAppsEligibleForBackup");
+
+        long oldToken = Binder.clearCallingIdentity();
+        try {
+            String callerLogString = "BMS.filterAppsEligibleForBackup";
+            TransportClient transportClient =
+                    mTransportManager.getCurrentTransportClient(callerLogString);
+            List<String> eligibleApps = new LinkedList<>();
+            for (String packageName : packages) {
+                if (AppBackupUtils
+                        .appIsRunningAndEligibleForBackupWithTransport(
+                                transportClient, packageName, mPackageManager, mUserId)) {
+                    eligibleApps.add(packageName);
+                }
+            }
+            if (transportClient != null) {
+                mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+            }
+            return eligibleApps.toArray(new String[eligibleApps.size()]);
+        } finally {
+            Binder.restoreCallingIdentity(oldToken);
+        }
+    }
+
+    /** Prints service state for 'dumpsys backup'. */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        long identityToken = Binder.clearCallingIdentity();
+        try {
+            if (args != null) {
+                for (String arg : args) {
+                    if ("agents".startsWith(arg)) {
+                        dumpAgents(pw);
+                        return;
+                    } else if ("transportclients".equals(arg.toLowerCase())) {
+                        mTransportManager.dumpTransportClients(pw);
+                        return;
+                    } else if ("transportstats".equals(arg.toLowerCase())) {
+                        mTransportManager.dumpTransportStats(pw);
+                        return;
+                    }
+                }
+            }
+            dumpInternal(pw);
+        } finally {
+            Binder.restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private void dumpAgents(PrintWriter pw) {
+        List<PackageInfo> agentPackages = allAgentPackages();
+        pw.println("Defined backup agents:");
+        for (PackageInfo pkg : agentPackages) {
+            pw.print("  ");
+            pw.print(pkg.packageName);
+            pw.println(':');
+            pw.print("      ");
+            pw.println(pkg.applicationInfo.backupAgentName);
+        }
+    }
+
+    private void dumpInternal(PrintWriter pw) {
+        // Add prefix for only non-system users so that system user dumpsys is the same as before
+        String userPrefix = mUserId == UserHandle.USER_SYSTEM ? "" : "User " + mUserId + ":";
+        synchronized (mQueueLock) {
+            pw.println(userPrefix + "Backup Manager is " + (mEnabled ? "enabled" : "disabled")
+                    + " / " + (!mSetupComplete ? "not " : "") + "setup complete / "
+                    + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init");
+            pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled"));
+            if (mBackupRunning) pw.println("Backup currently running");
+            pw.println(isBackupOperationInProgress() ? "Backup in progress" : "No backups running");
+            pw.println("Last backup pass started: " + mLastBackupPass
+                    + " (now = " + System.currentTimeMillis() + ')');
+            pw.println("  next scheduled: " + KeyValueBackupJob.nextScheduled(mUserId));
+
+            pw.println(userPrefix + "Transport whitelist:");
+            for (ComponentName transport : mTransportManager.getTransportWhitelist()) {
+                pw.print("    ");
+                pw.println(transport.flattenToShortString());
+            }
+
+            pw.println(userPrefix + "Available transports:");
+            final String[] transports = listAllTransports();
+            if (transports != null) {
+                for (String t : transports) {
+                    pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? "  * "
+                            : "    ") + t);
+                    try {
+                        File dir = new File(mBaseStateDir,
+                                mTransportManager.getTransportDirName(t));
+                        pw.println("       destination: "
+                                + mTransportManager.getTransportCurrentDestinationString(t));
+                        pw.println("       intent: "
+                                + mTransportManager.getTransportConfigurationIntent(t));
+                        for (File f : dir.listFiles()) {
+                            pw.println(
+                                    "       " + f.getName() + " - " + f.length() + " state bytes");
+                        }
+                    } catch (Exception e) {
+                        Slog.e(TAG, addUserIdToLogMessage(mUserId, "Error in transport"), e);
+                        pw.println("        Error: " + e);
+                    }
+                }
+            }
+
+            mTransportManager.dumpTransportClients(pw);
+
+            pw.println(userPrefix + "Pending init: " + mPendingInits.size());
+            for (String s : mPendingInits) {
+                pw.println("    " + s);
+            }
+
+            pw.print(userPrefix + "Ancestral: ");
+            pw.println(Long.toHexString(mAncestralToken));
+            pw.print(userPrefix + "Current:   ");
+            pw.println(Long.toHexString(mCurrentToken));
+
+            int numPackages = mBackupParticipants.size();
+            pw.println(userPrefix + "Participants:");
+            for (int i = 0; i < numPackages; i++) {
+                int uid = mBackupParticipants.keyAt(i);
+                pw.print("  uid: ");
+                pw.println(uid);
+                HashSet<String> participants = mBackupParticipants.valueAt(i);
+                for (String app : participants) {
+                    pw.println("    " + app);
+                }
+            }
+
+            pw.println(userPrefix + "Ancestral packages: "
+                    + (mAncestralPackages == null ? "none" : mAncestralPackages.size()));
+            if (mAncestralPackages != null) {
+                for (String pkg : mAncestralPackages) {
+                    pw.println("    " + pkg);
+                }
+            }
+
+            Set<String> processedPackages = mProcessedPackagesJournal.getPackagesCopy();
+            pw.println(userPrefix + "Ever backed up: " + processedPackages.size());
+            for (String pkg : processedPackages) {
+                pw.println("    " + pkg);
+            }
+
+            pw.println(userPrefix + "Pending key/value backup: " + mPendingBackups.size());
+            for (BackupRequest req : mPendingBackups.values()) {
+                pw.println("    " + req);
+            }
+
+            pw.println(userPrefix + "Full backup queue:" + mFullBackupQueue.size());
+            for (FullBackupEntry entry : mFullBackupQueue) {
+                pw.print("    ");
+                pw.print(entry.lastBackup);
+                pw.print(" : ");
+                pw.println(entry.packageName);
+            }
+        }
+    }
+
+    private static String addUserIdToLogMessage(int userId, String message) {
+        return "[UserID:" + userId + "] " + message;
+    }
+
+
+    public IBackupManager getBackupManagerBinder() {
+        return mBackupManagerBinder;
+    }
+}
diff --git a/com/android/server/backup/UserBackupManagerServiceTest.java b/com/android/server/backup/UserBackupManagerServiceTest.java
new file mode 100644
index 0000000..dfe75ed
--- /dev/null
+++ b/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -0,0 +1,1367 @@
+/*
+ * 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 com.android.server.backup.testing.BackupManagerServiceTestUtils.startSilentBackupThread;
+import static com.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.TransportData.d2dTransport;
+import static com.android.server.backup.testing.TransportData.localTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpCurrentTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransports;
+
+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.eq;
+import static org.mockito.Mockito.mock;
+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.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
+import android.app.backup.BackupManager;
+import android.app.backup.IBackupObserver;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.os.Binder;
+import android.os.HandlerThread;
+import android.os.PowerManager;
+import android.os.PowerSaveState;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import com.android.server.backup.testing.BackupManagerServiceTestUtils;
+import com.android.server.backup.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils.TransportMock;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.testing.shadows.ShadowAppBackupUtils;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowBinder;
+import com.android.server.testing.shadows.ShadowKeyValueBackupJob;
+import com.android.server.testing.shadows.ShadowKeyValueBackupTask;
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowContextWrapper;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for the per-user instance of the backup/restore system service {@link
+ * UserBackupManagerService} that performs operations for its target user.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAppBackupUtils.class,
+            ShadowApplicationPackageManager.class,
+            ShadowSystemServiceRegistry.class
+        })
+@Presubmit
+public class UserBackupManagerServiceTest {
+    private static final String TAG = "BMSTest";
+    private static final String PACKAGE_1 = "some.package.1";
+    private static final String PACKAGE_2 = "some.package.2";
+    private static final String USER_FACING_PACKAGE = "user.facing.package";
+    private static final int USER_ID = 10;
+
+    @Mock private TransportManager mTransportManager;
+    private HandlerThread mBackupThread;
+    private ShadowLooper mShadowBackupLooper;
+    private File mBaseStateDir;
+    private File mDataDir;
+    private ShadowContextWrapper mShadowContext;
+    private Context mContext;
+    private TransportData mTransport;
+    private String mTransportName;
+    private ShadowPackageManager mShadowPackageManager;
+
+    /**
+     * Initialize state that {@link UserBackupManagerService} operations interact with. This
+     * includes setting up the transport, starting the backup thread, and creating backup data
+     * directories.
+     */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTransport = backupTransport();
+        mTransportName = mTransport.transportName;
+
+        // Unrelated exceptions are thrown in the backup thread. Until we mock everything properly
+        // we should not fail tests because of this. This is not flakiness, the exceptions thrown
+        // don't interfere with the tests.
+        mBackupThread = startSilentBackupThread(TAG);
+        mShadowBackupLooper = shadowOf(mBackupThread.getLooper());
+
+        ContextWrapper context = RuntimeEnvironment.application;
+        mShadowPackageManager = shadowOf(context.getPackageManager());
+        mContext = context;
+        mShadowContext = shadowOf(context);
+
+        File cacheDir = mContext.getCacheDir();
+        // Corresponds to /data/backup
+        mBaseStateDir = new File(cacheDir, "base_state");
+        // Corresponds to /cache/backup_stage
+        mDataDir = new File(cacheDir, "data");
+    }
+
+    /**
+     * Clean up and reset state that was created for testing {@link UserBackupManagerService}
+     * operations.
+     */
+    @After
+    public void tearDown() throws Exception {
+        mBackupThread.quit();
+        ShadowAppBackupUtils.reset();
+        ShadowApplicationPackageManager.reset();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getDestinationString(String)} returns the
+     * current destination string of inputted transport if the transport is registered.
+     */
+    @Test
+    public void testDestinationString() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+                .thenReturn("destinationString");
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String destination = backupManagerService.getDestinationString(mTransportName);
+
+        assertThat(destination).isEqualTo("destinationString");
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getDestinationString(String)} returns
+     * {@code null} if the inputted transport is not registered.
+     */
+    @Test
+    public void testDestinationString_whenTransportNotRegistered() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+                .thenThrow(TransportNotRegisteredException.class);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String destination = backupManagerService.getDestinationString(mTransportName);
+
+        assertThat(destination).isNull();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getDestinationString(String)} throws a
+     * {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testDestinationString_withoutPermission() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+                .thenThrow(TransportNotRegisteredException.class);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () -> backupManagerService.getDestinationString(mTransportName));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#isAppEligibleForBackup(String)} returns
+     * {@code false} when the given app is not eligible for backup.
+     */
+    @Test
+    public void testIsAppEligibleForBackup_whenAppNotEligible() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpCurrentTransport(mTransportManager, mTransport);
+        registerPackages(PACKAGE_1);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        boolean result = backupManagerService.isAppEligibleForBackup(PACKAGE_1);
+
+        assertThat(result).isFalse();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#isAppEligibleForBackup(String)} returns
+     * {@code true} when the given app is eligible for backup.
+     */
+    @Test
+    public void testIsAppEligibleForBackup_whenAppEligible() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpCurrentTransport(mTransportManager, backupTransport());
+        registerPackages(PACKAGE_1);
+        ShadowAppBackupUtils.setAppRunningAndEligibleForBackupWithTransport(PACKAGE_1);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        boolean result = backupManagerService.isAppEligibleForBackup(PACKAGE_1);
+
+        assertThat(result).isTrue();
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#isAppEligibleForBackup(String)} throws a
+     * {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testIsAppEligibleForBackup_withoutPermission() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        setUpCurrentTransport(mTransportManager, mTransport);
+        registerPackages(PACKAGE_1);
+        ShadowAppBackupUtils.setAppRunningAndEligibleForBackupWithTransport(PACKAGE_1);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () -> backupManagerService.isAppEligibleForBackup(PACKAGE_1));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#filterAppsEligibleForBackup(String[])}
+     * returns an {@code array} of only apps that are eligible for backup from an {@array} of
+     * inputted apps.
+     */
+    @Test
+    public void testFilterAppsEligibleForBackup() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpCurrentTransport(mTransportManager, mTransport);
+        registerPackages(PACKAGE_1, PACKAGE_2);
+        ShadowAppBackupUtils.setAppRunningAndEligibleForBackupWithTransport(PACKAGE_1);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String[] filtered =
+                backupManagerService.filterAppsEligibleForBackup(
+                        new String[] {PACKAGE_1, PACKAGE_2});
+
+        assertThat(filtered).asList().containsExactly(PACKAGE_1);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#filterAppsEligibleForBackup(String[])}
+     * returns an empty {@code array} if no inputted apps are eligible for backup.
+     */
+    @Test
+    public void testFilterAppsEligibleForBackup_whenNoneIsEligible() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        registerPackages(PACKAGE_1, PACKAGE_2);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String[] filtered =
+                backupManagerService.filterAppsEligibleForBackup(
+                        new String[] {PACKAGE_1, PACKAGE_2});
+
+        assertThat(filtered).isEmpty();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#filterAppsEligibleForBackup(String[])}
+     * throws a {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testFilterAppsEligibleForBackup_withoutPermission() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        setUpCurrentTransport(mTransportManager, mTransport);
+        registerPackages(PACKAGE_1, PACKAGE_2);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.filterAppsEligibleForBackup(
+                                new String[] {PACKAGE_1, PACKAGE_2}));
+    }
+
+    /* Tests for select transport */
+
+    private ComponentName mNewTransportComponent;
+    private TransportData mNewTransport;
+    private TransportMock mNewTransportMock;
+    private TransportData mOldTransport;
+    private TransportMock mOldTransportMock;
+
+    private void setUpForSelectTransport() throws Exception {
+        mNewTransport = backupTransport();
+        mNewTransportComponent = mNewTransport.getTransportComponent();
+        mOldTransport = d2dTransport();
+        List<TransportMock> transportMocks =
+                setUpTransports(mTransportManager, mNewTransport, mOldTransport, localTransport());
+        mNewTransportMock = transportMocks.get(0);
+        mOldTransportMock = transportMocks.get(1);
+        when(mTransportManager.selectTransport(eq(mNewTransport.transportName)))
+                .thenReturn(mOldTransport.transportName);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransport(String)}
+     * successfully switches the current transport to the inputted transport, returns the name of
+     * the old transport, and disposes of the transport client after the operation.
+     */
+    @Test
+    public void testSelectBackupTransport() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String oldTransport =
+                backupManagerService.selectBackupTransport(mNewTransport.transportName);
+
+        assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+        assertThat(oldTransport).isEqualTo(mOldTransport.transportName);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(mNewTransportMock.transportClient), any());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransport(String)} throws a
+     * {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testSelectBackupTransport_withoutPermission() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () -> backupManagerService.selectBackupTransport(mNewTransport.transportName));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransportAsync(ComponentName,
+     * ISelectBackupTransportCallback)} successfully switches the current transport to the inputted
+     * transport and disposes of the transport client after the operation.
+     */
+    @Test
+    public void testSelectBackupTransportAsync() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+                .thenReturn(BackupManager.SUCCESS);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+        backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+        verify(callback).onSuccess(eq(mNewTransport.transportName));
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(mNewTransportMock.transportClient), any());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransportAsync(ComponentName,
+     * ISelectBackupTransportCallback)} does not switch the current transport to the inputted
+     * transport and notifies the inputted callback of failure when it fails to register the
+     * transport.
+     */
+    @Test
+    public void testSelectBackupTransportAsync_whenRegistrationFails() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+                .thenReturn(BackupManager.ERROR_TRANSPORT_UNAVAILABLE);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+        backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(getSettingsTransport()).isNotEqualTo(mNewTransport.transportName);
+        verify(callback).onFailure(anyInt());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransportAsync(ComponentName,
+     * ISelectBackupTransportCallback)} does not switch the current transport to the inputted
+     * transport and notifies the inputted callback of failure when the transport gets unregistered.
+     */
+    @Test
+    public void testSelectBackupTransportAsync_whenTransportGetsUnregistered() throws Exception {
+        setUpTransports(mTransportManager, mTransport.unregistered());
+        ComponentName newTransportComponent = mTransport.getTransportComponent();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.registerAndSelectTransport(eq(newTransportComponent)))
+                .thenReturn(BackupManager.SUCCESS);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+        backupManagerService.selectBackupTransportAsync(newTransportComponent, callback);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(getSettingsTransport()).isNotEqualTo(mTransportName);
+        verify(callback).onFailure(anyInt());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransportAsync(ComponentName,
+     * ISelectBackupTransportCallback)} throws a {@link SecurityException} if the caller does not
+     * have backup permission.
+     */
+    @Test
+    public void testSelectBackupTransportAsync_withoutPermission() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        ComponentName newTransportComponent = mNewTransport.getTransportComponent();
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.selectBackupTransportAsync(
+                                newTransportComponent, mock(ISelectBackupTransportCallback.class)));
+    }
+
+    private String getSettingsTransport() {
+        return Settings.Secure.getString(
+                mContext.getContentResolver(), Settings.Secure.BACKUP_TRANSPORT);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getCurrentTransportComponent()} returns
+     * the {@link ComponentName} of the currently selected transport.
+     */
+    @Test
+    public void testGetCurrentTransportComponent() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getCurrentTransportComponent())
+                .thenReturn(mTransport.getTransportComponent());
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        ComponentName transportComponent = backupManagerService.getCurrentTransportComponent();
+
+        assertThat(transportComponent).isEqualTo(mTransport.getTransportComponent());
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getCurrentTransportComponent()} returns
+     * {@code null} if there is no currently selected transport.
+     */
+    @Test
+    public void testGetCurrentTransportComponent_whenNoTransportSelected() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getCurrentTransportComponent()).thenReturn(null);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        ComponentName transportComponent = backupManagerService.getCurrentTransportComponent();
+
+        assertThat(transportComponent).isNull();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getCurrentTransportComponent()} returns
+     * {@code null} if the currently selected transport is not registered.
+     */
+    @Test
+    public void testGetCurrentTransportComponent_whenTransportNotRegistered() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.getCurrentTransportComponent())
+                .thenThrow(TransportNotRegisteredException.class);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        ComponentName transportComponent = backupManagerService.getCurrentTransportComponent();
+
+        assertThat(transportComponent).isNull();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#getCurrentTransportComponent()} throws a
+     * {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testGetCurrentTransportComponent_withoutPermission() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(SecurityException.class, backupManagerService::getCurrentTransportComponent);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#excludeKeysFromRestore(String, List)}
+     * throws a {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testExcludeKeysFromRestore_withoutPermission() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.excludeKeysFromRestore(
+                                PACKAGE_1,
+                                new ArrayList<String>(){}));
+    }
+
+    /* Tests for updating transport attributes */
+
+    private static final int PACKAGE_UID = 10;
+    private ComponentName mTransportComponent;
+    private int mTransportUid;
+
+    private void setUpForUpdateTransportAttributes() throws Exception {
+        mTransportComponent = mTransport.getTransportComponent();
+        String transportPackage = mTransportComponent.getPackageName();
+        PackageInfo packageInfo = getPackageInfo(transportPackage);
+
+        ShadowPackageManager shadowPackageManager = shadowOf(mContext.getPackageManager());
+        shadowPackageManager.installPackage(packageInfo);
+        shadowPackageManager.setPackagesForUid(PACKAGE_UID, transportPackage);
+        // Set up for user invocations on ApplicationPackageManager.
+        ShadowApplicationPackageManager.addInstalledPackage(transportPackage, packageInfo);
+        ShadowApplicationPackageManager.setPackageUid(transportPackage, PACKAGE_UID);
+
+        mTransportUid = mContext.getPackageManager().getPackageUid(transportPackage, 0);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} succeeds if the uid of the transport
+     * is same as the uid of the caller.
+     */
+    @Test
+    public void
+            testUpdateTransportAttributes_whenTransportUidEqualsCallingUid_callsTransportManager()
+                    throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        Intent configurationIntent = new Intent();
+        Intent dataManagementIntent = new Intent();
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        backupManagerService.updateTransportAttributes(
+                mTransportUid,
+                mTransportComponent,
+                mTransportName,
+                configurationIntent,
+                "currentDestinationString",
+                dataManagementIntent,
+                "dataManagementLabel");
+
+        verify(mTransportManager)
+                .updateTransportAttributes(
+                        eq(mTransportComponent),
+                        eq(mTransportName),
+                        eq(configurationIntent),
+                        eq("currentDestinationString"),
+                        eq(dataManagementIntent),
+                        eq("dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link SecurityException} if
+     * the uid of the transport is not equal to the uid of the caller.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenTransportUidNotEqualToCallingUid_throwsException()
+            throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid + 1,
+                                mTransportComponent,
+                                mTransportName,
+                                new Intent(),
+                                "currentDestinationString",
+                                new Intent(),
+                                "dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link RuntimeException} if
+     * given a {@code null} transport component.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenTransportComponentNull_throwsException()
+            throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                RuntimeException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                null,
+                                mTransportName,
+                                new Intent(),
+                                "currentDestinationString",
+                                new Intent(),
+                                "dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link RuntimeException} if
+     * given a {@code null} transport name.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenNameNull_throwsException() throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                RuntimeException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                mTransportComponent,
+                                null,
+                                new Intent(),
+                                "currentDestinationString",
+                                new Intent(),
+                                "dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link RuntimeException} if
+     * given a {@code null} destination string.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenCurrentDestinationStringNull_throwsException()
+            throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                RuntimeException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                mTransportComponent,
+                                mTransportName,
+                                new Intent(),
+                                null,
+                                new Intent(),
+                                "dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link RuntimeException} if
+     * given either a {@code null} data management label or {@code null} data management intent, but
+     * not both.
+     */
+    @Test
+    public void
+            testUpdateTransportAttributes_whenDataManagementArgsNullityDontMatch_throwsException()
+                    throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                RuntimeException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                mTransportComponent,
+                                mTransportName,
+                                new Intent(),
+                                "currentDestinationString",
+                                null,
+                                "dataManagementLabel"));
+
+        expectThrows(
+                RuntimeException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                mTransportComponent,
+                                mTransportName,
+                                new Intent(),
+                                "currentDestinationString",
+                                new Intent(),
+                                null));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} succeeds if the caller has backup
+     * permission.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenPermissionGranted_callsThroughToTransportManager()
+            throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        Intent configurationIntent = new Intent();
+        Intent dataManagementIntent = new Intent();
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        backupManagerService.updateTransportAttributes(
+                mTransportUid,
+                mTransportComponent,
+                mTransportName,
+                configurationIntent,
+                "currentDestinationString",
+                dataManagementIntent,
+                "dataManagementLabel");
+
+        verify(mTransportManager)
+                .updateTransportAttributes(
+                        eq(mTransportComponent),
+                        eq(mTransportName),
+                        eq(configurationIntent),
+                        eq("currentDestinationString"),
+                        eq(dataManagementIntent),
+                        eq("dataManagementLabel"));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#updateTransportAttributes(int,
+     * ComponentName, String, Intent, String, Intent, String)} throws a {@link SecurityException} if
+     * the caller does not have backup permission.
+     */
+    @Test
+    public void testUpdateTransportAttributes_whenPermissionDenied_throwsSecurityException()
+            throws Exception {
+        setUpForUpdateTransportAttributes();
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () ->
+                        backupManagerService.updateTransportAttributes(
+                                mTransportUid,
+                                mTransportComponent,
+                                mTransportName,
+                                new Intent(),
+                                "currentDestinationString",
+                                new Intent(),
+                                "dataManagementLabel"));
+    }
+
+    /* Tests for request backup */
+
+    @Mock private IBackupObserver mObserver;
+
+    private void setUpForRequestBackup(String... packages) throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        for (String packageName : packages) {
+            registerPackages(packageName);
+            ShadowAppBackupUtils.setAppRunningAndEligibleForBackupWithTransport(packageName);
+        }
+        setUpCurrentTransport(mTransportManager, mTransport);
+    }
+
+    private void tearDownForRequestBackup() {
+        ShadowKeyValueBackupTask.reset();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} throws a {@link SecurityException} if the caller does not have backup permission.
+     */
+    @Test
+    public void testRequestBackup_whenPermissionDenied() throws Exception {
+        mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                SecurityException.class,
+                () -> backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0));
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} throws an {@link IllegalArgumentException} if passed {@null} for packages.
+     */
+    @Test
+    public void testRequestBackup_whenPackagesNull() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                IllegalArgumentException.class,
+                () -> backupManagerService.requestBackup(null, mObserver, 0));
+        verify(mObserver).backupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} throws an {@link IllegalArgumentException} if passed an empty {@code array} for
+     * packages.
+     */
+    @Test
+    public void testRequestBackup_whenPackagesEmpty() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        expectThrows(
+                IllegalArgumentException.class,
+                () -> backupManagerService.requestBackup(new String[0], mObserver, 0));
+        verify(mObserver).backupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#ERROR_BACKUP_NOT_ALLOWED} if backup is disabled.
+     */
+    @Test
+    public void testRequestBackup_whenBackupDisabled() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        backupManagerService.setEnabled(false);
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        assertThat(result).isEqualTo(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+        verify(mObserver).backupFinished(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#ERROR_BACKUP_NOT_ALLOWED} if the system user hasn't gone
+     * through SUW.
+     */
+    @Test
+    public void testRequestBackup_whenNotProvisioned() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        backupManagerService.setSetupComplete(false);
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        assertThat(result).isEqualTo(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+        verify(mObserver).backupFinished(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#ERROR_TRANSPORT_ABORTED} if the current transport is not
+     * registered.
+     */
+    @Test
+    public void testRequestBackup_whenTransportNotRegistered() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpCurrentTransport(mTransportManager, mTransport.unregistered());
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        backupManagerService.setEnabled(true);
+        backupManagerService.setSetupComplete(true);
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        assertThat(result).isEqualTo(BackupManager.ERROR_TRANSPORT_ABORTED);
+        verify(mObserver).backupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#SUCCESS} and notifies the observer of {@link
+     * BackupManager#ERROR_BACKUP_NOT_ALLOWED} if the specified app is not eligible for backup.
+     */
+    @Test
+    public void testRequestBackup_whenAppNotEligibleForBackup() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        registerPackages(PACKAGE_1);
+        setUpCurrentTransport(mTransportManager, mTransport);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        backupManagerService.setEnabled(true);
+        backupManagerService.setSetupComplete(true);
+        // Haven't set PACKAGE_1 as eligible
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        assertThat(result).isEqualTo(BackupManager.SUCCESS);
+        verify(mObserver).onResult(PACKAGE_1, BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+        // TODO: We probably don't need to kick-off KeyValueBackupTask when list is empty
+        tearDownForRequestBackup();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#SUCCESS} and updates bookkeeping if backup for a key value
+     * package succeeds.
+     */
+    @Test
+    @Config(shadows = ShadowKeyValueBackupTask.class)
+    public void testRequestBackup_whenPackageIsKeyValue() throws Exception {
+        setUpForRequestBackup(PACKAGE_1);
+        UserBackupManagerService backupManagerService =
+                createBackupManagerServiceForRequestBackup();
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(BackupManager.SUCCESS);
+        ShadowKeyValueBackupTask shadowTask = ShadowKeyValueBackupTask.getLastCreated();
+        assertThat(shadowTask.getQueue()).containsExactly(PACKAGE_1);
+        assertThat(shadowTask.getPendingFullBackups()).isEmpty();
+        // TODO: Assert more about KeyValueBackupTask
+        tearDownForRequestBackup();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#requestBackup(String[], IBackupObserver,
+     * int)} returns {@link BackupManager#SUCCESS} and updates bookkeeping if backup for a full
+     * backup package succeeds.
+     */
+    @Test
+    @Config(shadows = ShadowKeyValueBackupTask.class)
+    public void testRequestBackup_whenPackageIsFullBackup() throws Exception {
+        setUpForRequestBackup(PACKAGE_1);
+        ShadowAppBackupUtils.setAppGetsFullBackup(PACKAGE_1);
+        UserBackupManagerService backupManagerService =
+                createBackupManagerServiceForRequestBackup();
+
+        int result = backupManagerService.requestBackup(new String[] {PACKAGE_1}, mObserver, 0);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(BackupManager.SUCCESS);
+        ShadowKeyValueBackupTask shadowTask = ShadowKeyValueBackupTask.getLastCreated();
+        assertThat(shadowTask.getQueue()).isEmpty();
+        assertThat(shadowTask.getPendingFullBackups()).containsExactly(PACKAGE_1);
+        // TODO: Assert more about KeyValueBackupTask
+        tearDownForRequestBackup();
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#backupNow()} clears the calling identity
+     * for scheduling a job and then restores the original calling identity after the operation.
+     */
+    @Test
+    @Config(shadows = {ShadowBinder.class, ShadowKeyValueBackupJob.class})
+    public void testBackupNow_clearsCallingIdentityForJobScheduler() {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        setUpPowerManager(backupManagerService);
+        ShadowBinder.setCallingUid(1);
+
+        backupManagerService.backupNow();
+
+        assertThat(ShadowKeyValueBackupJob.getCallingUid()).isEqualTo(ShadowBinder.LOCAL_UID);
+        assertThat(Binder.getCallingUid()).isEqualTo(1);
+    }
+
+    /**
+     * Test verifying that {@link UserBackupManagerService#backupNow()} restores the original
+     * calling identity if an exception is thrown during execution.
+     */
+    @Test
+    @Config(shadows = {ShadowBinder.class, ShadowKeyValueBackupJobException.class})
+    public void testBackupNow_whenExceptionThrown_restoresCallingIdentity() {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        setUpPowerManager(backupManagerService);
+        ShadowBinder.setCallingUid(1);
+
+        expectThrows(IllegalArgumentException.class, backupManagerService::backupNow);
+        assertThat(ShadowKeyValueBackupJobException.getCallingUid())
+                .isEqualTo(ShadowBinder.LOCAL_UID);
+        assertThat(Binder.getCallingUid()).isEqualTo(1);
+    }
+
+    private UserBackupManagerService createBackupManagerServiceForRequestBackup() {
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+        backupManagerService.setEnabled(true);
+        backupManagerService.setSetupComplete(true);
+        return backupManagerService;
+    }
+
+    /**
+     * Test verifying that creating a new instance posts a transport registration task to the backup
+     * thread.
+     */
+    @Test
+    public void testCreateAndInitializeService_postRegisterTransports() {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+
+        UserBackupManagerService.createAndInitializeService(
+                USER_ID,
+                mContext,
+                new BackupManagerService(mContext),
+                mBackupThread,
+                mBaseStateDir,
+                mDataDir,
+                mTransportManager);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        verify(mTransportManager).registerTransports();
+    }
+
+    /**
+     * Test verifying that creating a new instance does not directly register transports on the main
+     * thread.
+     */
+    @Test
+    public void testCreateAndInitializeService_doesNotRegisterTransportsSynchronously() {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+
+        UserBackupManagerService.createAndInitializeService(
+                USER_ID,
+                mContext,
+                new BackupManagerService(mContext),
+                mBackupThread,
+                mBaseStateDir,
+                mDataDir,
+                mTransportManager);
+
+        // Operations posted to mBackupThread only run with mShadowBackupLooper.runToEndOfTasks()
+        verify(mTransportManager, never()).registerTransports();
+    }
+
+    /** Test checking non-null argument on instance creation. */
+    @Test
+    public void testCreateAndInitializeService_withNullContext_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                /* context */ null,
+                                new BackupManagerService(mContext),
+                                mBackupThread,
+                                mBaseStateDir,
+                                mDataDir,
+                                mTransportManager));
+    }
+
+    /** Test checking non-null argument on instance creation. */
+    @Test
+    public void testCreateAndInitializeService_withNullTrampoline_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                mContext,
+                                /* trampoline */ null,
+                                mBackupThread,
+                                mBaseStateDir,
+                                mDataDir,
+                                mTransportManager));
+    }
+
+    /** Test checking non-null argument on instance creation. */
+    @Test
+    public void testCreateAndInitializeService_withNullBackupThread_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                mContext,
+                                new BackupManagerService(mContext),
+                                /* backupThread */ null,
+                                mBaseStateDir,
+                                mDataDir,
+                                mTransportManager));
+    }
+
+    /** Test checking non-null argument on instance creation. */
+    @Test
+    public void testCreateAndInitializeService_withNullStateDir_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                mContext,
+                                new BackupManagerService(mContext),
+                                mBackupThread,
+                                /* baseStateDir */ null,
+                                mDataDir,
+                                mTransportManager));
+    }
+
+    /**
+     * Test checking non-null argument on {@link
+     * UserBackupManagerService#createAndInitializeService(int, Context, BackupManagerService,
+     * HandlerThread, File, File, TransportManager)}.
+     */
+    @Test
+    public void testCreateAndInitializeService_withNullDataDir_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                mContext,
+                                new BackupManagerService(mContext),
+                                mBackupThread,
+                                mBaseStateDir,
+                                /* dataDir */ null,
+                                mTransportManager));
+    }
+
+    /**
+     * Test checking non-null argument on {@link
+     * UserBackupManagerService#createAndInitializeService(int, Context, BackupManagerService,
+     * HandlerThread, File, File, TransportManager)}.
+     */
+    @Test
+    public void testCreateAndInitializeService_withNullTransportManager_throws() {
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        UserBackupManagerService.createAndInitializeService(
+                                USER_ID,
+                                mContext,
+                                new BackupManagerService(mContext),
+                                mBackupThread,
+                                mBaseStateDir,
+                                mDataDir,
+                                /* transportManager */ null));
+    }
+
+    /**
+     * Test verifying that creating a new instance registers the broadcast receiver for package
+     * tracking
+     */
+    @Test
+    public void testCreateAndInitializeService_registersPackageTrackingReceiver() throws Exception {
+        Context contextSpy = Mockito.spy(mContext);
+
+        UserBackupManagerService service = UserBackupManagerService.createAndInitializeService(
+                USER_ID,
+                contextSpy,
+                new BackupManagerService(mContext),
+                mBackupThread,
+                mBaseStateDir,
+                mDataDir,
+                mTransportManager);
+
+        BroadcastReceiver packageTrackingReceiver = service.getPackageTrackingReceiver();
+        assertThat(packageTrackingReceiver).isNotNull();
+
+        // One call for package changes and one call for sd card events.
+        verify(contextSpy, times(2)).registerReceiverAsUser(
+                eq(packageTrackingReceiver), eq(UserHandle.of(USER_ID)), any(), any(), any());
+    }
+
+    @Test
+    public void testFilterUserFacingPackages_shouldSkipUserFacing_filtersUserFacing() {
+        List<PackageInfo> packages = Arrays.asList(getPackageInfo(USER_FACING_PACKAGE),
+                getPackageInfo(PACKAGE_1));
+        UserBackupManagerService backupManagerService = spy(
+                createUserBackupManagerServiceAndRunTasks());
+        when(backupManagerService.shouldSkipUserFacingData()).thenReturn(true);
+        when(backupManagerService.shouldSkipPackage(eq(USER_FACING_PACKAGE))).thenReturn(true);
+
+        List<PackageInfo> filteredPackages = backupManagerService.filterUserFacingPackages(
+                packages);
+
+        assertFalse(containsPackage(filteredPackages, USER_FACING_PACKAGE));
+        assertTrue(containsPackage(filteredPackages, PACKAGE_1));
+    }
+
+    @Test
+    public void testFilterUserFacingPackages_shouldNotSkipUserFacing_doesNotFilterUserFacing() {
+        List<PackageInfo> packages = Arrays.asList(getPackageInfo(USER_FACING_PACKAGE),
+                getPackageInfo(PACKAGE_1));
+        UserBackupManagerService backupManagerService = spy(
+                createUserBackupManagerServiceAndRunTasks());
+        when(backupManagerService.shouldSkipUserFacingData()).thenReturn(false);
+        when(backupManagerService.shouldSkipPackage(eq(USER_FACING_PACKAGE))).thenReturn(true);
+
+        List<PackageInfo> filteredPackages = backupManagerService.filterUserFacingPackages(
+                packages);
+
+        assertTrue(containsPackage(filteredPackages, USER_FACING_PACKAGE));
+        assertTrue(containsPackage(filteredPackages, PACKAGE_1));
+    }
+
+    private static boolean containsPackage(List<PackageInfo> packages, String targetPackage) {
+        for (PackageInfo packageInfo : packages) {
+            if (targetPackage.equals(packageInfo.packageName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private UserBackupManagerService createUserBackupManagerServiceAndRunTasks() {
+        return BackupManagerServiceTestUtils.createUserBackupManagerServiceAndRunTasks(
+                USER_ID, mContext, mBackupThread, mBaseStateDir, mDataDir, mTransportManager);
+    }
+
+    private void setUpPowerManager(UserBackupManagerService backupManagerService) {
+        PowerManager powerManagerMock = mock(PowerManager.class);
+        when(powerManagerMock.getPowerSaveState(anyInt()))
+                .thenReturn(new PowerSaveState.Builder().setBatterySaverEnabled(true).build());
+        backupManagerService.setPowerManager(powerManagerMock);
+    }
+
+    private PackageInfo getPackageInfo(String packageName) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.packageName = packageName;
+        return packageInfo;
+    }
+
+    private void registerPackages(String... packages) {
+        for (String packageName : packages) {
+            PackageInfo packageInfo = getPackageInfo(packageName);
+            mShadowPackageManager.installPackage(packageInfo);
+            ShadowApplicationPackageManager.addInstalledPackage(packageName, packageInfo);
+        }
+    }
+
+    /**
+     * Test that {@link UserBackupManagerService#getAncestralSerialNumber()} returns {@code -1}
+     * when value not set.
+     */
+    @Test
+    public void testGetAncestralSerialNumber_notSet_returnsMinusOne() {
+        UserBackupManagerService service = createUserBackupManagerServiceAndRunTasks();
+
+        assertThat(service.getAncestralSerialNumber()).isEqualTo(-1L);
+    }
+
+    /**
+     * Test that {@link UserBackupManagerService#getAncestralSerialNumber()} returns correct value
+     * when value set.
+     */
+    @Test
+    public void testGetAncestralSerialNumber_set_returnsCorrectValue() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService service = createUserBackupManagerServiceAndRunTasks();
+        service.setAncestralSerialNumberFile(createTestFile());
+
+        long testSerialNumber = 20L;
+        service.setAncestralSerialNumber(testSerialNumber);
+
+        assertThat(service.getAncestralSerialNumber()).isEqualTo(testSerialNumber);
+    }
+
+    /**
+     * Test that {@link UserBackupManagerService#getAncestralSerialNumber()} returns correct value
+     * when value set.
+     */
+    @Test
+    public void testGetAncestralSerialNumber_setTwice_returnsCorrectValue() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService service = createUserBackupManagerServiceAndRunTasks();
+        service.setAncestralSerialNumberFile(createTestFile());
+
+        long testSerialNumber = 20L;
+        long testSerialNumber2 = 21L;
+        service.setAncestralSerialNumber(testSerialNumber);
+        service.setAncestralSerialNumber(testSerialNumber2);
+
+        assertThat(service.getAncestralSerialNumber()).isEqualTo(testSerialNumber2);
+    }
+
+    /**
+     * Test that {@link UserBackupManagerService#dump()} for system user does not prefix dump with
+     * "User 0:".
+     */
+    @Test
+    public void testDump_forSystemUser_DoesNotHaveUserPrefix() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService service =
+                BackupManagerServiceTestUtils.createUserBackupManagerServiceAndRunTasks(
+                        UserHandle.USER_SYSTEM,
+                        mContext,
+                        mBackupThread,
+                        mBaseStateDir,
+                        mDataDir,
+                        mTransportManager);
+
+        StringWriter dump = new StringWriter();
+        service.dump(new FileDescriptor(), new PrintWriter(dump), new String[0]);
+
+        assertThat(dump.toString()).startsWith("Backup Manager is ");
+    }
+
+    /**
+     * Test that {@link UserBackupManagerService#dump()} for non-system user prefixes dump with
+     * "User <userid>:".
+     */
+    @Test
+    public void testDump_forNonSystemUser_HasUserPrefix() throws Exception {
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        UserBackupManagerService service = createUserBackupManagerServiceAndRunTasks();
+
+        StringWriter dump = new StringWriter();
+        service.dump(new FileDescriptor(), new PrintWriter(dump), new String[0]);
+
+        assertThat(dump.toString()).startsWith("User " + USER_ID + ":" + "Backup Manager is ");
+    }
+
+    private File createTestFile() throws IOException {
+        File testFile = new File(mContext.getFilesDir(), "test");
+        testFile.createNewFile();
+        return testFile;
+    }
+
+    /**
+     * We can't mock the void method {@link #schedule(Context, long, BackupManagerConstants)} so we
+     * extend {@link ShadowKeyValueBackupJob} and throw an exception at the end of the method.
+     */
+    @Implements(KeyValueBackupJob.class)
+    public static class ShadowKeyValueBackupJobException extends ShadowKeyValueBackupJob {
+        /**
+         * Implementation of {@link ShadowKeyValueBackupJob#schedule(Context, long,
+         * BackupManagerConstants)} that throws an {@link IllegalArgumentException}.
+         */
+        public static void schedule(int userId, Context ctx, long delay,
+                BackupManagerConstants constants) {
+            ShadowKeyValueBackupJob.schedule(userId, ctx, delay, constants);
+            throw new IllegalArgumentException();
+        }
+    }
+}
diff --git a/com/android/server/backup/UserBackupPreferences.java b/com/android/server/backup/UserBackupPreferences.java
new file mode 100644
index 0000000..bb8bf52
--- /dev/null
+++ b/com/android/server/backup/UserBackupPreferences.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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 android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Manages the persisted backup preferences per user. */
+public class UserBackupPreferences {
+    private static final String PREFERENCES_FILE = "backup_preferences";
+
+    private final SharedPreferences mPreferences;
+    private final SharedPreferences.Editor mEditor;
+
+    UserBackupPreferences(Context conext, File storageDir) {
+        File excludedKeysFile = new File(storageDir, PREFERENCES_FILE);
+        mPreferences = conext.getSharedPreferences(excludedKeysFile, Context.MODE_PRIVATE);
+        mEditor = mPreferences.edit();
+    }
+
+    void addExcludedKeys(String packageName, List<String> keys) {
+        Set<String> existingKeys =
+                new HashSet<>(mPreferences.getStringSet(packageName, Collections.emptySet()));
+        existingKeys.addAll(keys);
+        mEditor.putStringSet(packageName, existingKeys);
+        mEditor.commit();
+    }
+
+    Set<String> getExcludedRestoreKeysForPackage(String packageName) {
+        return mPreferences.getStringSet(packageName, Collections.emptySet());
+    }
+}
diff --git a/com/android/server/backup/encryption/BackupEncryptionService.java b/com/android/server/backup/encryption/BackupEncryptionService.java
new file mode 100644
index 0000000..84fb0e6
--- /dev/null
+++ b/com/android/server/backup/encryption/BackupEncryptionService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.encryption.transport.IntermediateEncryptingTransport;
+import com.android.server.backup.encryption.transport.IntermediateEncryptingTransportManager;
+
+/**
+ * This service provides encryption of backup data. For an intent used to bind to this service, it
+ * provides an {@link IntermediateEncryptingTransport} which is an implementation of {@link
+ * IBackupTransport} that encrypts (or decrypts) the data when sending it (or receiving it) from the
+ * real {@link IBackupTransport}.
+ */
+public class BackupEncryptionService extends Service {
+    public static final String TAG = "BackupEncryption";
+    private static IntermediateEncryptingTransportManager sTransportManager = null;
+
+    @Override
+    public void onCreate() {
+        Log.i(TAG, "onCreate:" + this);
+        if (sTransportManager == null) {
+            Log.i(TAG, "Creating IntermediateEncryptingTransportManager");
+            sTransportManager = new IntermediateEncryptingTransportManager(this);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.i(TAG, "onDestroy:" + this);
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        // TODO (b141536117): Check connection with TransportClient.connect and return null on fail.
+        return sTransportManager.get(intent);
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        sTransportManager.cleanup(intent);
+        return false;
+    }
+}
diff --git a/com/android/server/backup/encryption/CryptoSettings.java b/com/android/server/backup/encryption/CryptoSettings.java
new file mode 100644
index 0000000..bb1336f
--- /dev/null
+++ b/com/android/server/backup/encryption/CryptoSettings.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.KeyStoreException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * State about encrypted backups that needs to be remembered.
+ */
+public class CryptoSettings {
+
+    private static final String TAG = "CryptoSettings";
+
+    private static final String SHARED_PREFERENCES_NAME = "crypto_settings";
+
+    private static final String KEY_IS_INITIALIZED = "isInitialized";
+    private static final String KEY_ACTIVE_SECONDARY_ALIAS = "activeSecondary";
+    private static final String KEY_NEXT_SECONDARY_ALIAS = "nextSecondary";
+    private static final String SECONDARY_KEY_LAST_ROTATED_AT = "secondaryKeyLastRotatedAt";
+    private static final String[] SETTINGS_FOR_BACKUP = {
+        KEY_IS_INITIALIZED,
+        KEY_ACTIVE_SECONDARY_ALIAS,
+        KEY_NEXT_SECONDARY_ALIAS,
+        SECONDARY_KEY_LAST_ROTATED_AT
+    };
+
+    private static final long DEFAULT_SECONDARY_KEY_ROTATION_PERIOD =
+            TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);
+
+    private static final String KEY_ANCESTRAL_SECONDARY_KEY_VERSION =
+            "ancestral_secondary_key_version";
+
+    private final SharedPreferences mSharedPreferences;
+    private final Context mContext;
+
+    /**
+     * A new instance.
+     *
+     * @param context For looking up the {@link SharedPreferences}, for storing state.
+     * @return The instance.
+     */
+    public static CryptoSettings getInstance(Context context) {
+        // We need single process mode because CryptoSettings may be used from several processes
+        // simultaneously.
+        SharedPreferences sharedPreferences =
+                context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return new CryptoSettings(sharedPreferences, context);
+    }
+
+    /**
+     * A new instance using {@link SharedPreferences} in the default mode.
+     *
+     * <p>This will not work across multiple processes but will work in tests.
+     */
+    @VisibleForTesting
+    public static CryptoSettings getInstanceForTesting(Context context) {
+        SharedPreferences sharedPreferences =
+                context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return new CryptoSettings(sharedPreferences, context);
+    }
+
+    private CryptoSettings(SharedPreferences sharedPreferences, Context context) {
+        mSharedPreferences = Objects.requireNonNull(sharedPreferences);
+        mContext = Objects.requireNonNull(context);
+    }
+
+    /**
+     * The alias of the current active secondary key. This should be used to retrieve the key from
+     * AndroidKeyStore.
+     */
+    public Optional<String> getActiveSecondaryKeyAlias() {
+        return getStringInSharedPrefs(KEY_ACTIVE_SECONDARY_ALIAS);
+    }
+
+    /**
+     * The alias of the secondary key to which the client is rotating. The rotation is not
+     * immediate, which is why this setting is needed. Once the next key is created, it can take up
+     * to 72 hours potentially (or longer if the user has no network) for the next key to be synced
+     * with the keystore. Only after that has happened does the client attempt to re-wrap all
+     * tertiary keys and commit the rotation.
+     */
+    public Optional<String> getNextSecondaryKeyAlias() {
+        return getStringInSharedPrefs(KEY_NEXT_SECONDARY_ALIAS);
+    }
+
+    /**
+     * If the settings have been initialized.
+     */
+    public boolean getIsInitialized() {
+        return mSharedPreferences.getBoolean(KEY_IS_INITIALIZED, false);
+    }
+
+    /**
+     * Sets the alias of the currently active secondary key.
+     *
+     * @param activeAlias The alias, as in AndroidKeyStore.
+     * @throws IllegalArgumentException if the alias is not in the user's keystore.
+     */
+    public void setActiveSecondaryKeyAlias(String activeAlias) throws IllegalArgumentException {
+        assertIsValidAlias(activeAlias);
+        mSharedPreferences.edit().putString(KEY_ACTIVE_SECONDARY_ALIAS, activeAlias).apply();
+    }
+
+    /**
+     * Sets the alias of the secondary key to which the client is rotating.
+     *
+     * @param nextAlias The alias, as in AndroidKeyStore.
+     * @throws KeyStoreException if unable to check whether alias is valid in the keystore.
+     * @throws IllegalArgumentException if the alias is not in the user's keystore.
+     */
+    public void setNextSecondaryAlias(String nextAlias) throws IllegalArgumentException {
+        assertIsValidAlias(nextAlias);
+        mSharedPreferences.edit().putString(KEY_NEXT_SECONDARY_ALIAS, nextAlias).apply();
+    }
+
+    /**
+     * Unsets the alias of the key to which the client is rotating. This is generally performed once
+     * a rotation is complete.
+     */
+    public void removeNextSecondaryKeyAlias() {
+        mSharedPreferences.edit().remove(KEY_NEXT_SECONDARY_ALIAS).apply();
+    }
+
+    /**
+     * Sets the timestamp of when the secondary key was last rotated.
+     *
+     * @param timestamp The timestamp to set.
+     */
+    public void setSecondaryLastRotated(long timestamp) {
+        mSharedPreferences.edit().putLong(SECONDARY_KEY_LAST_ROTATED_AT, timestamp).apply();
+    }
+
+    /**
+     * Returns a timestamp of when the secondary key was last rotated.
+     *
+     * @return The timestamp.
+     */
+    public Optional<Long> getSecondaryLastRotated() {
+        if (!mSharedPreferences.contains(SECONDARY_KEY_LAST_ROTATED_AT)) {
+            return Optional.empty();
+        }
+        return Optional.of(mSharedPreferences.getLong(SECONDARY_KEY_LAST_ROTATED_AT, -1));
+    }
+
+    /**
+     * Sets the settings to have been initialized. (Otherwise loading should try to initialize
+     * again.)
+     */
+    private void setIsInitialized() {
+        mSharedPreferences.edit().putBoolean(KEY_IS_INITIALIZED, true).apply();
+    }
+
+    /**
+     * Initializes with the given key alias.
+     *
+     * @param alias The secondary key alias to be set as active.
+     * @throws IllegalArgumentException if the alias does not reference a valid key.
+     * @throws IllegalStateException if attempting to initialize an already initialized settings.
+     */
+    public void initializeWithKeyAlias(String alias) throws IllegalArgumentException {
+        checkState(
+                !getIsInitialized(), "Attempting to initialize an already initialized settings.");
+        setActiveSecondaryKeyAlias(alias);
+        setIsInitialized();
+    }
+
+    /** Returns the secondary key version of the encrypted backup set to restore from (if set). */
+    public Optional<String> getAncestralSecondaryKeyVersion() {
+        return Optional.ofNullable(
+                mSharedPreferences.getString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, null));
+    }
+
+    /** Sets the secondary key version of the encrypted backup set to restore from. */
+    public void setAncestralSecondaryKeyVersion(String ancestralSecondaryKeyVersion) {
+        mSharedPreferences
+                .edit()
+                .putString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, ancestralSecondaryKeyVersion)
+                .apply();
+    }
+
+    /** The number of milliseconds between secondary key rotation */
+    public long backupSecondaryKeyRotationIntervalMs() {
+        return DEFAULT_SECONDARY_KEY_ROTATION_PERIOD;
+    }
+
+    /** Deletes all crypto settings related to backup (as opposed to restore). */
+    public void clearAllSettingsForBackup() {
+        Editor sharedPrefsEditor = mSharedPreferences.edit();
+        for (String backupSettingKey : SETTINGS_FOR_BACKUP) {
+            sharedPrefsEditor.remove(backupSettingKey);
+        }
+        sharedPrefsEditor.apply();
+
+        Slog.d(TAG, "Cleared crypto settings for backup");
+    }
+
+    /**
+     * Throws {@link IllegalArgumentException} if the alias does not refer to a key that is in
+     * the {@link RecoveryController}.
+     */
+    private void assertIsValidAlias(String alias) throws IllegalArgumentException {
+        try {
+            if (!RecoveryController.getInstance(mContext).getAliases().contains(alias)) {
+                throw new IllegalArgumentException(alias + " is not in RecoveryController");
+            }
+        } catch (InternalRecoveryServiceException e) {
+            throw new IllegalArgumentException("Problem accessing recovery service", e);
+        }
+    }
+
+    private Optional<String> getStringInSharedPrefs(String key) {
+        return Optional.ofNullable(mSharedPreferences.getString(key, null));
+    }
+}
diff --git a/com/android/server/backup/encryption/CryptoSettingsTest.java b/com/android/server/backup/encryption/CryptoSettingsTest.java
new file mode 100644
index 0000000..979b3d5
--- /dev/null
+++ b/com/android/server/backup/encryption/CryptoSettingsTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.Application;
+import android.security.keystore.recovery.RecoveryController;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Optional;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowRecoveryController.class)
+public class CryptoSettingsTest {
+
+    private static final String TEST_KEY_ALIAS =
+            "com.android.server.backup.encryption/keystore/08120c326b928ff34c73b9c58581da63";
+
+    private CryptoSettings mCryptoSettings;
+    private Application mApplication;
+
+    @Before
+    public void setUp() {
+        ShadowRecoveryController.reset();
+
+        mApplication = ApplicationProvider.getApplicationContext();
+        mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
+    }
+
+    @Test
+    public void getActiveSecondaryAlias_isInitiallyAbsent() {
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
+    }
+
+    @Test
+    public void getActiveSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS);
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
+    }
+
+    @Test
+    public void getNextSecondaryAlias_isInitiallyAbsent() {
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
+    }
+
+    @Test
+    public void getNextSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
+    }
+
+    @Test
+    public void isInitialized_isInitiallyFalse() {
+        assertThat(mCryptoSettings.getIsInitialized()).isFalse();
+    }
+
+    @Test
+    public void setActiveSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS));
+    }
+
+    @Test
+    public void setNextSecondaryAlias_inRecoveryController_setsAlias() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+
+        mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
+
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
+    }
+
+    @Test
+    public void setNextSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS));
+    }
+
+    @Test
+    public void removeNextSecondaryAlias_removesIt() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
+
+        mCryptoSettings.removeNextSecondaryKeyAlias();
+
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
+    }
+
+    @Test
+    public void initializeWithKeyAlias_setsAsInitialized() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
+        assertThat(mCryptoSettings.getIsInitialized()).isTrue();
+    }
+
+    @Test
+    public void initializeWithKeyAlias_setsActiveAlias() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
+    }
+
+    @Test
+    public void initializeWithKeyAlias_throwsIfKeyIsNotInRecoveryController() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
+    }
+
+    @Test
+    public void initializeWithKeyAlias_throwsIfAlreadyInitialized() throws Exception {
+        setAliasIsInRecoveryController(TEST_KEY_ALIAS);
+        mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
+    }
+
+    @Test
+    public void getSecondaryLastRotated_returnsEmptyInitially() {
+        assertThat(mCryptoSettings.getSecondaryLastRotated()).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void getSecondaryLastRotated_returnsTimestampAfterItIsSet() {
+        long timestamp = 1000001;
+
+        mCryptoSettings.setSecondaryLastRotated(timestamp);
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(timestamp);
+    }
+
+    @Test
+    public void getAncestralSecondaryKeyVersion_notSet_returnsOptionalAbsent() {
+        assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().isPresent()).isFalse();
+    }
+
+    @Test
+    public void getAncestralSecondaryKeyVersion_isSet_returnsSetValue() {
+        String secondaryKeyVersion = "some_secondary_key";
+        mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion);
+
+        assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
+                .isEqualTo(secondaryKeyVersion);
+    }
+
+    @Test
+    public void getAncestralSecondaryKeyVersion_isSetMultipleTimes_returnsLastSetValue() {
+        String secondaryKeyVersion1 = "some_secondary_key";
+        String secondaryKeyVersion2 = "another_secondary_key";
+        mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion1);
+        mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion2);
+
+        assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
+                .isEqualTo(secondaryKeyVersion2);
+    }
+
+    @Test
+    public void clearAllSettingsForBackup_clearsStateForBackup() throws Exception {
+        String key1 = "key1";
+        String key2 = "key2";
+        String ancestralKey = "ancestral_key";
+        setAliasIsInRecoveryController(key1);
+        setAliasIsInRecoveryController(key2);
+        mCryptoSettings.setActiveSecondaryKeyAlias(key1);
+        mCryptoSettings.setNextSecondaryAlias(key2);
+        mCryptoSettings.setSecondaryLastRotated(100001);
+        mCryptoSettings.setAncestralSecondaryKeyVersion(ancestralKey);
+
+        mCryptoSettings.clearAllSettingsForBackup();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
+        assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse();
+        assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()).isEqualTo(ancestralKey);
+    }
+
+    private void setAliasIsInRecoveryController(String alias) throws Exception {
+        RecoveryController recoveryController = RecoveryController.getInstance(mApplication);
+        recoveryController.generateKey(alias);
+    }
+}
diff --git a/com/android/server/backup/encryption/EncryptionKeyHelper.java b/com/android/server/backup/encryption/EncryptionKeyHelper.java
new file mode 100644
index 0000000..2035b66
--- /dev/null
+++ b/com/android/server/backup/encryption/EncryptionKeyHelper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+class EncryptionKeyHelper {
+    private static SecureRandom sSecureRandom = new  SecureRandom();
+
+    private final Context mContext;
+    private final RecoverableKeyStoreSecondaryKeyManager
+            .RecoverableKeyStoreSecondaryKeyManagerProvider
+            mSecondaryKeyManagerProvider;
+
+    EncryptionKeyHelper(Context context) {
+        mContext = context;
+        mSecondaryKeyManagerProvider =
+                () ->
+                        new RecoverableKeyStoreSecondaryKeyManager(
+                                RecoveryController.getInstance(mContext), sSecureRandom);
+    }
+
+    RecoverableKeyStoreSecondaryKeyManager
+            .RecoverableKeyStoreSecondaryKeyManagerProvider getKeyManagerProvider() {
+        return mSecondaryKeyManagerProvider;
+    }
+
+    RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
+            throws UnrecoverableKeyException, InternalRecoveryServiceException {
+        String keyAlias = CryptoSettings.getInstance(mContext).getActiveSecondaryKeyAlias().get();
+        return mSecondaryKeyManagerProvider.get().get(keyAlias).get();
+    }
+
+    SecretKey getTertiaryKey(
+            String packageName,
+            RecoverableKeyStoreSecondaryKey secondaryKey)
+            throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
+            NoSuchAlgorithmException, IOException, NoSuchPaddingException,
+            InvalidKeyException {
+        TertiaryKeyManager tertiaryKeyManager =
+                new TertiaryKeyManager(
+                        mContext,
+                        sSecureRandom,
+                        TertiaryKeyRotationScheduler.getInstance(mContext),
+                        secondaryKey,
+                        packageName);
+        return tertiaryKeyManager.getKey();
+    }
+}
diff --git a/com/android/server/backup/encryption/FullBackupDataProcessor.java b/com/android/server/backup/encryption/FullBackupDataProcessor.java
new file mode 100644
index 0000000..f3ab2bd
--- /dev/null
+++ b/com/android/server/backup/encryption/FullBackupDataProcessor.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import android.app.backup.BackupTransport;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Accepts the full backup data stream and sends it to the server. */
+public interface FullBackupDataProcessor {
+    /**
+     * Prepares the upload.
+     *
+     * <p>After this, call {@link #start()} to establish the connection.
+     *
+     * @param inputStream to read the backup data from, calling {@link #finish} or {@link #cancel}
+     *     will close the stream
+     * @return {@code true} if the connection was set up successfully, otherwise {@code false}
+     */
+    boolean initiate(InputStream inputStream) throws IOException;
+
+    /**
+     * Starts the upload, establishing the connection to the server.
+     *
+     * <p>After this, call {@link #pushData(int)} to request that the processor reads data from the
+     * socket, and uploads it to the server.
+     *
+     * <p>After this you must call one of {@link #cancel()}, {@link #finish()}, {@link
+     * #handleCheckSizeRejectionZeroBytes()}, {@link #handleCheckSizeRejectionQuotaExceeded()} or
+     * {@link #handleSendBytesQuotaExceeded()} to close the upload.
+     */
+    void start();
+
+    /**
+     * Requests that the processor read {@code numBytes} from the input stream passed in {@link
+     * #initiate(InputStream)} and upload them to the server.
+     *
+     * @return {@link BackupTransport#TRANSPORT_OK} if the upload succeeds, or {@link
+     *     BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
+     *     quota, or {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
+     */
+    int pushData(int numBytes);
+
+    /** Cancels the upload and tears down the connection. */
+    void cancel();
+
+    /**
+     * Finish the upload and tear down the connection.
+     *
+     * <p>Call this after there is no more data to push with {@link #pushData(int)}.
+     *
+     * @return One of {@link BackupTransport#TRANSPORT_OK} if the app upload succeeds, {@link
+     *     BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
+     *     quota, {@link BackupTransport#TRANSPORT_ERROR} for server 500s, or {@link
+     *     BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
+     */
+    int finish();
+
+    /**
+     * Notifies the processor that the current upload should be terminated because the estimated
+     * size is zero.
+     */
+    void handleCheckSizeRejectionZeroBytes();
+
+    /**
+     * Notifies the processor that the current upload should be terminated because the estimated
+     * size exceeds the quota.
+     */
+    void handleCheckSizeRejectionQuotaExceeded();
+
+    /**
+     * Notifies this class that the current upload should be terminated because the quota was
+     * exceeded during upload.
+     */
+    void handleSendBytesQuotaExceeded();
+
+    /**
+     * Attaches {@link FullBackupCallbacks} which the processor will notify when the backup
+     * succeeds.
+     */
+    void attachCallbacks(FullBackupCallbacks fullBackupCallbacks);
+
+    /**
+     * Implemented by the caller of the processor to receive notification of when the backup
+     * succeeds.
+     */
+    interface FullBackupCallbacks {
+        /** The processor calls this to indicate that the current backup has succeeded. */
+        void onSuccess();
+
+        /** The processor calls this if the upload failed for a non-transient reason. */
+        void onTransferFailed();
+    }
+}
diff --git a/com/android/server/backup/encryption/FullRestoreDataProcessor.java b/com/android/server/backup/encryption/FullRestoreDataProcessor.java
new file mode 100644
index 0000000..e4c4049
--- /dev/null
+++ b/com/android/server/backup/encryption/FullRestoreDataProcessor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import java.io.IOException;
+
+/**
+ * Retrieves the data during a full restore, decrypting it if necessary.
+ *
+ * <p>Use {@link FullRestoreDataProcessorFactory} to construct the encrypted or unencrypted
+ * processor as appropriate during restore.
+ */
+public interface FullRestoreDataProcessor {
+    /** Return value of {@link #readNextChunk} when there is no more data to download. */
+    int END_OF_STREAM = -1;
+
+    /**
+     * Reads the next chunk of restore data and writes it to the given buffer.
+     *
+     * <p>Where necessary, will open the connection to the server and/or decrypt the backup file.
+     *
+     * <p>The implementation may retry various errors. If the retries fail it will throw the
+     * relevant exception.
+     *
+     * @return the number of bytes read, or {@link #END_OF_STREAM} if there is no more data
+     * @throws IOException when downloading from the network or writing to disk
+     */
+    int readNextChunk(byte[] buffer) throws IOException;
+
+    /**
+     * Closes the connection to the server, deletes any temporary files and optionally sends a log
+     * with the given finish type.
+     *
+     * @param finishType one of {@link FullRestoreDownloader.FinishType}
+     */
+    void finish(FullRestoreDownloader.FinishType finishType);
+}
diff --git a/com/android/server/backup/encryption/FullRestoreDownloader.java b/com/android/server/backup/encryption/FullRestoreDownloader.java
new file mode 100644
index 0000000..afcca79
--- /dev/null
+++ b/com/android/server/backup/encryption/FullRestoreDownloader.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import java.io.IOException;
+
+/** Interface for classes which will provide backup data */
+public abstract class FullRestoreDownloader {
+    /** Enum to provide information on why a download finished */
+    public enum FinishType {
+        UNKNOWN_FINISH(0),
+        // Finish the downloading and successfully write data to Android OS.
+        FINISHED(1),
+        // Download failed with any kind of exception.
+        TRANSFER_FAILURE(2),
+        // Download failed due to auth failure on the device.
+        AUTH_FAILURE(3),
+        // Aborted by Android Framework.
+        FRAMEWORK_ABORTED(4);
+
+        private int mValue;
+
+        FinishType(int value) {
+            mValue = value;
+        }
+    }
+
+    /** Get the next data chunk from the backing store */
+    public abstract int readNextChunk(byte[] buffer) throws IOException;
+
+    /** Called when we've finished restoring the data */
+    public abstract void finish(FinishType finishType);
+}
diff --git a/com/android/server/backup/encryption/KeyValueEncrypter.java b/com/android/server/backup/encryption/KeyValueEncrypter.java
new file mode 100644
index 0000000..1d841b4
--- /dev/null
+++ b/com/android/server/backup/encryption/KeyValueEncrypter.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import static com.android.server.backup.encryption.BackupEncryptionService.TAG;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+import java.util.Map;
+
+public class KeyValueEncrypter {
+    private final Context mContext;
+    private final EncryptionKeyHelper mKeyHelper;
+
+    public KeyValueEncrypter(Context context) {
+        mContext = context;
+        mKeyHelper = new EncryptionKeyHelper(mContext);
+    }
+
+    public void encryptKeyValueData(
+            String packageName, ParcelFileDescriptor inputFd, OutputStream outputStream)
+            throws Exception {
+        EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
+                new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
+        EncryptedKvBackupTask backupTask =
+                backupTaskFactory.newInstance(
+                        mContext,
+                        new SecureRandom(),
+                        new FileBackupServer(outputStream),
+                        CryptoSettings.getInstance(mContext),
+                        mKeyHelper.getKeyManagerProvider(),
+                        inputFd,
+                        packageName);
+        backupTask.performBackup(/* incremental */ false);
+    }
+
+    public void decryptKeyValueData(String packageName,
+            InputStream encryptedInputStream, ParcelFileDescriptor outputFd) throws Exception {
+        RecoverableKeyStoreSecondaryKey secondaryKey = mKeyHelper.getActiveSecondaryKey();
+
+        EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
+                new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
+        EncryptedKvRestoreTask restoreTask =
+                restoreTaskFactory.newInstance(
+                        mContext,
+                        mKeyHelper.getKeyManagerProvider(),
+                        new InputStreamFullRestoreDownloader(encryptedInputStream),
+                        secondaryKey.getAlias(),
+                        KeyWrapUtils.wrap(
+                                secondaryKey.getSecretKey(),
+                                mKeyHelper.getTertiaryKey(packageName, secondaryKey)));
+
+        restoreTask.getRestoreData(outputFd);
+    }
+
+    // TODO(b/142455725): Extract into a commong class.
+    private static class FileBackupServer implements CryptoBackupServer {
+        private static final String EMPTY_DOC_ID = "";
+
+        private final OutputStream mOutputStream;
+
+        FileBackupServer(OutputStream outputStream) {
+            mOutputStream = outputStream;
+        }
+
+        @Override
+        public String uploadIncrementalBackup(
+                String packageName,
+                String oldDocId,
+                byte[] diffScript,
+                WrappedKeyProto.WrappedKey tertiaryKey) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String uploadNonIncrementalBackup(
+                String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
+            try {
+                mOutputStream.write(data);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to write encrypted data to file: ", e);
+            }
+
+            return EMPTY_DOC_ID;
+        }
+
+        @Override
+        public void setActiveSecondaryKeyAlias(
+                String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
+            // Do nothing.
+        }
+    }
+
+    // TODO(b/142455725): Extract into a commong class.
+    private static class InputStreamFullRestoreDownloader extends FullRestoreDownloader {
+        private final InputStream mInputStream;
+
+        InputStreamFullRestoreDownloader(InputStream inputStream) {
+            mInputStream = inputStream;
+        }
+
+        @Override
+        public int readNextChunk(byte[] buffer) throws IOException {
+            return mInputStream.read(buffer);
+        }
+
+        @Override
+        public void finish(FinishType finishType) {
+            try {
+                mInputStream.close();
+            } catch (IOException e) {
+                Log.w(TAG, "Error while reading restore data");
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/RoundTripTest.java b/com/android/server/backup/encryption/RoundTripTest.java
new file mode 100644
index 0000000..a432d91
--- /dev/null
+++ b/com/android/server/backup/encryption/RoundTripTest.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.encryption.tasks.EncryptedFullBackupTask;
+import com.android.server.backup.encryption.tasks.EncryptedFullRestoreTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
+import com.android.server.testing.shadows.DataEntity;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Optional;
+import java.util.Map;
+import java.util.Set;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+@Config(
+        shadows = {
+            ShadowBackupDataInput.class,
+            ShadowBackupDataOutput.class,
+            ShadowRecoveryController.class
+        })
+@RunWith(RobolectricTestRunner.class)
+public class RoundTripTest {
+    private static final DataEntity[] KEY_VALUE_DATA = {
+        new DataEntity("test_key_1", "test_value_1"),
+        new DataEntity("test_key_2", "test_value_2"),
+        new DataEntity("test_key_3", "test_value_3")
+    };
+
+    /** Amount of data we want to round trip in this test */
+    private static final int TEST_DATA_SIZE = 1024 * 1024; // 1MB
+
+    /** Buffer size used when reading data from the restore task */
+    private static final int READ_BUFFER_SIZE = 1024; // 1024 byte buffer.
+
+    /** Key parameters used for the secondary encryption key */
+    private static final String KEY_ALGORITHM = "AES";
+
+    private static final int KEY_SIZE_BITS = 256;
+
+    /** Package name for our test package */
+    private static final String TEST_PACKAGE_NAME = "com.android.backup.test";
+
+    /** The name we use to refer to our secondary key */
+    private static final String TEST_KEY_ALIAS = "test/backup/KEY_ALIAS";
+
+    /** Original data used for comparison after round trip */
+    private final byte[] mOriginalData = new byte[TEST_DATA_SIZE];
+
+    /** App context, used to store the key data and chunk listings */
+    private Context mContext;
+
+    /** The secondary key we're using for the test */
+    private RecoverableKeyStoreSecondaryKey mSecondaryKey;
+
+    /** Source of random material which is considered non-predictable in its' generation */
+    private final SecureRandom mSecureRandom = new SecureRandom();
+
+    private RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider
+            mSecondaryKeyManagerProvider;
+    private DummyServer mDummyServer;
+    private RecoveryController mRecoveryController;
+
+    @Mock private ParcelFileDescriptor mParcelFileDescriptor;
+
+    @Before
+    public void setUp() throws NoSuchAlgorithmException, InternalRecoveryServiceException {
+        MockitoAnnotations.initMocks(this);
+
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataOutput.reset();
+
+        mContext = ApplicationProvider.getApplicationContext();
+        mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_KEY_ALIAS, generateAesKey());
+        mDummyServer = new DummyServer();
+        mSecondaryKeyManagerProvider =
+                () ->
+                        new RecoverableKeyStoreSecondaryKeyManager(
+                                RecoveryController.getInstance(mContext), mSecureRandom);
+
+        fillBuffer(mOriginalData);
+    }
+
+    @Test
+    public void testFull_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
+        byte[] backupData = performFullBackup(mOriginalData);
+        assertThat(backupData).isNotEqualTo(mOriginalData);
+        byte[] restoredData = performFullRestore(backupData);
+        assertThat(restoredData).isEqualTo(mOriginalData);
+    }
+
+    @Test
+    public void testKeyValue_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
+        byte[] backupData = performNonIncrementalKeyValueBackup(KEY_VALUE_DATA);
+
+        // Get the secondary key used to do backup.
+        Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
+                mSecondaryKeyManagerProvider.get().get(mDummyServer.mSecondaryKeyAlias);
+        assertThat(secondaryKey.isPresent()).isTrue();
+
+        Set<DataEntity> restoredData = performKeyValueRestore(backupData, secondaryKey.get());
+
+        assertThat(restoredData).containsExactly(KEY_VALUE_DATA).inOrder();
+    }
+
+    /** Perform a key/value backup and return the backed-up representation of the data */
+    private byte[] performNonIncrementalKeyValueBackup(DataEntity[] backupData)
+            throws Exception {
+        // Populate test key/value data.
+        for (DataEntity entity : backupData) {
+            ShadowBackupDataInput.addEntity(entity);
+        }
+
+        EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
+                new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
+        EncryptedKvBackupTask backupTask =
+                backupTaskFactory.newInstance(
+                        mContext,
+                        mSecureRandom,
+                        mDummyServer,
+                        CryptoSettings.getInstance(mContext),
+                        mSecondaryKeyManagerProvider,
+                        mParcelFileDescriptor,
+                        TEST_PACKAGE_NAME);
+
+        backupTask.performBackup(/* incremental */ false);
+
+        return mDummyServer.mStoredData;
+    }
+
+    /** Perform a full backup and return the backed-up representation of the data */
+    private byte[] performFullBackup(byte[] backupData) throws Exception {
+        DummyServer dummyServer = new DummyServer();
+        EncryptedFullBackupTask backupTask =
+                EncryptedFullBackupTask.newInstance(
+                        mContext,
+                        dummyServer,
+                        mSecureRandom,
+                        mSecondaryKey,
+                        TEST_PACKAGE_NAME,
+                        new ByteArrayInputStream(backupData));
+        backupTask.call();
+        return dummyServer.mStoredData;
+    }
+
+    private Set<DataEntity> performKeyValueRestore(
+            byte[] backupData, RecoverableKeyStoreSecondaryKey secondaryKey) throws Exception {
+        EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
+                new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
+        EncryptedKvRestoreTask restoreTask =
+                restoreTaskFactory.newInstance(
+                        mContext,
+                        mSecondaryKeyManagerProvider,
+                        new FakeFullRestoreDownloader(backupData),
+                        secondaryKey.getAlias(),
+                        KeyWrapUtils.wrap(
+                                secondaryKey.getSecretKey(), getTertiaryKey(secondaryKey)));
+        restoreTask.getRestoreData(mParcelFileDescriptor);
+        return ShadowBackupDataOutput.getEntities();
+    }
+
+    /** Perform a full restore and return the bytes obtained from the restore process */
+    private byte[] performFullRestore(byte[] backupData)
+            throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
+                    InvalidAlgorithmParameterException, InvalidKeyException,
+                    IllegalBlockSizeException {
+        ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
+
+        EncryptedFullRestoreTask restoreTask =
+                EncryptedFullRestoreTask.newInstance(
+                        mContext,
+                        new FakeFullRestoreDownloader(backupData),
+                        getTertiaryKey(mSecondaryKey));
+
+        byte[] buffer = new byte[READ_BUFFER_SIZE];
+        int bytesRead = restoreTask.readNextChunk(buffer);
+        while (bytesRead != -1) {
+            decryptedOutput.write(buffer, 0, bytesRead);
+            bytesRead = restoreTask.readNextChunk(buffer);
+        }
+
+        return decryptedOutput.toByteArray();
+    }
+
+    /** Get the tertiary key for our test package from the key manager */
+    private SecretKey getTertiaryKey(RecoverableKeyStoreSecondaryKey secondaryKey)
+            throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
+                    NoSuchAlgorithmException, IOException, NoSuchPaddingException,
+                    InvalidKeyException {
+        TertiaryKeyManager tertiaryKeyManager =
+                new TertiaryKeyManager(
+                        mContext,
+                        mSecureRandom,
+                        TertiaryKeyRotationScheduler.getInstance(mContext),
+                        secondaryKey,
+                        TEST_PACKAGE_NAME);
+        return tertiaryKeyManager.getKey();
+    }
+
+    /** Fill a buffer with data in a predictable way */
+    private void fillBuffer(byte[] buffer) {
+        byte loopingCounter = 0;
+        for (int i = 0; i < buffer.length; i++) {
+            buffer[i] = loopingCounter;
+            loopingCounter++;
+        }
+    }
+
+    /** Generate a new, random, AES key */
+    public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+        keyGenerator.init(KEY_SIZE_BITS);
+        return keyGenerator.generateKey();
+    }
+
+    /**
+     * Dummy backup data endpoint. This stores the data so we can use it in subsequent test steps.
+     */
+    private static class DummyServer implements CryptoBackupServer {
+        private static final String DUMMY_DOC_ID = "DummyDoc";
+
+        byte[] mStoredData = null;
+        String mSecondaryKeyAlias;
+
+        @Override
+        public String uploadIncrementalBackup(
+                String packageName,
+                String oldDocId,
+                byte[] diffScript,
+                WrappedKeyProto.WrappedKey tertiaryKey) {
+            throw new RuntimeException("Not Implemented");
+        }
+
+        @Override
+        public String uploadNonIncrementalBackup(
+                String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
+            assertThat(packageName).isEqualTo(TEST_PACKAGE_NAME);
+            mStoredData = data;
+            return DUMMY_DOC_ID;
+        }
+
+        @Override
+        public void setActiveSecondaryKeyAlias(
+                String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
+            mSecondaryKeyAlias = keyAlias;
+        }
+    }
+
+    /** Fake package wrapper which returns data from a byte array. */
+    private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
+        private final ByteArrayInputStream mData;
+
+        FakeFullRestoreDownloader(byte[] data) {
+            // We override all methods of the superclass, so it does not require any collaborators.
+            super();
+            mData = new ByteArrayInputStream(data);
+        }
+
+        @Override
+        public int readNextChunk(byte[] buffer) throws IOException {
+            return mData.read(buffer);
+        }
+
+        @Override
+        public void finish(FinishType finishType) {
+            // Do nothing.
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/StreamUtils.java b/com/android/server/backup/encryption/StreamUtils.java
new file mode 100644
index 0000000..66be25b
--- /dev/null
+++ b/com/android/server/backup/encryption/StreamUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Utility methods for dealing with Streams */
+public class StreamUtils {
+    private static final int MAX_COPY_BUFFER_SIZE = 1024; // 1k copy buffer size.
+
+    /**
+     * Close a Closeable and silently ignore any IOExceptions.
+     *
+     * @param closeable The closeable to close
+     */
+    public static void closeQuietly(Closeable closeable) {
+        try {
+            closeable.close();
+        } catch (IOException ioe) {
+            // Silently ignore
+        }
+    }
+
+    /**
+     * Copy data from an InputStream to an OutputStream upto a given number of bytes.
+     *
+     * @param in The source InputStream
+     * @param out The destination OutputStream
+     * @param limit The maximum number of bytes to copy
+     * @throws IOException Thrown if there is a problem performing the copy.
+     */
+    public static void copyStream(InputStream in, OutputStream out, int limit) throws IOException {
+        int bufferSize = Math.min(MAX_COPY_BUFFER_SIZE, limit);
+        byte[] buffer = new byte[bufferSize];
+
+        int copied = 0;
+        while (copied < limit) {
+            int maxReadSize = Math.min(bufferSize, limit - copied);
+            int read = in.read(buffer, 0, maxReadSize);
+            if (read < 0) {
+                return; // Reached the stream end before the limit
+            }
+            out.write(buffer, 0, read);
+            copied += read;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/StreamUtilsTest.java b/com/android/server/backup/encryption/StreamUtilsTest.java
new file mode 100644
index 0000000..a95e87e
--- /dev/null
+++ b/com/android/server/backup/encryption/StreamUtilsTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 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.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+@RunWith(RobolectricTestRunner.class)
+public class StreamUtilsTest {
+    private static final int SOURCE_DATA_SIZE = 64;
+
+    private byte[] mSourceData;
+
+    private InputStream mSource;
+    private ByteArrayOutputStream mDestination;
+
+    @Before
+    public void setUp() {
+        mSourceData = new byte[SOURCE_DATA_SIZE];
+        for (byte i = 0; i < SOURCE_DATA_SIZE; i++) {
+            mSourceData[i] = i;
+        }
+        mSource = new ByteArrayInputStream(mSourceData);
+        mDestination = new ByteArrayOutputStream();
+    }
+
+    @Test
+    public void copyStream_copiesAllBytesIfAsked() throws IOException {
+        StreamUtils.copyStream(mSource, mDestination, mSourceData.length);
+        assertOutputHasBytes(mSourceData.length);
+    }
+
+    @Test
+    public void copyStream_stopsShortIfAsked() throws IOException {
+        StreamUtils.copyStream(mSource, mDestination, mSourceData.length - 10);
+        assertOutputHasBytes(mSourceData.length - 10);
+    }
+
+    @Test
+    public void copyStream_stopsShortIfAskedToCopyMoreThanAvailable() throws IOException {
+        StreamUtils.copyStream(mSource, mDestination, mSourceData.length + 10);
+        assertOutputHasBytes(mSourceData.length);
+    }
+
+    private void assertOutputHasBytes(int count) {
+        byte[] output = mDestination.toByteArray();
+        assertThat(output.length).isEqualTo(count);
+        for (int i = 0; i < count; i++) {
+            assertThat(output[i]).isEqualTo(mSourceData[i]);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/ChunkHash.java b/com/android/server/backup/encryption/chunk/ChunkHash.java
new file mode 100644
index 0000000..1630eb8
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/ChunkHash.java
@@ -0,0 +1,90 @@
+/*
+ * 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.encryption.chunk;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Base64;
+
+/**
+ * Represents the SHA-256 hash of the plaintext of a chunk, which is frequently used as a key.
+ *
+ * <p>This class is {@link Comparable} and implements {@link #equals(Object)} and {@link
+ * #hashCode()}.
+ */
+public class ChunkHash implements Comparable<ChunkHash> {
+    /** The length of the hash in bytes. The hash is a SHA-256, so this is 256 bits. */
+    public static final int HASH_LENGTH_BYTES = 256 / 8;
+
+    private static final int UNSIGNED_MASK = 0xFF;
+
+    private final byte[] mHash;
+
+    /** Constructs a new instance which wraps the given SHA-256 hash bytes. */
+    public ChunkHash(byte[] hash) {
+        Preconditions.checkArgument(hash.length == HASH_LENGTH_BYTES, "Hash must have 256 bits");
+        mHash = hash;
+    }
+
+    public byte[] getHash() {
+        return mHash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ChunkHash)) {
+            return false;
+        }
+
+        ChunkHash chunkHash = (ChunkHash) o;
+        return Arrays.equals(mHash, chunkHash.mHash);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mHash);
+    }
+
+    @Override
+    public int compareTo(ChunkHash other) {
+        return lexicographicalCompareUnsignedBytes(getHash(), other.getHash());
+    }
+
+    @Override
+    public String toString() {
+        return Base64.getEncoder().encodeToString(mHash);
+    }
+
+    private static int lexicographicalCompareUnsignedBytes(byte[] left, byte[] right) {
+        int minLength = Math.min(left.length, right.length);
+        for (int i = 0; i < minLength; i++) {
+            int result = toInt(left[i]) - toInt(right[i]);
+            if (result != 0) {
+                return result;
+            }
+        }
+        return left.length - right.length;
+    }
+
+    private static int toInt(byte value) {
+        return value & UNSIGNED_MASK;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/ChunkHashTest.java b/com/android/server/backup/encryption/chunk/ChunkHashTest.java
new file mode 100644
index 0000000..c12464c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/ChunkHashTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.encryption.chunk;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ChunkHashTest {
+    private static final int HASH_LENGTH_BYTES = 256 / 8;
+    private static final byte[] TEST_HASH_1 = Arrays.copyOf(new byte[] {1}, HASH_LENGTH_BYTES);
+    private static final byte[] TEST_HASH_2 = Arrays.copyOf(new byte[] {2}, HASH_LENGTH_BYTES);
+
+    @Test
+    public void testGetHash_returnsHash() {
+        ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
+
+        byte[] hash = chunkHash.getHash();
+
+        assertThat(hash).asList().containsExactlyElementsIn(Bytes.asList(TEST_HASH_1)).inOrder();
+    }
+
+    @Test
+    public void testEquals() {
+        ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
+
+        assertThat(chunkHash1).isEqualTo(equalChunkHash1);
+        assertThat(chunkHash1).isNotEqualTo(chunkHash2);
+    }
+
+    @Test
+    public void testHashCode() {
+        ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
+
+        int hash1 = chunkHash1.hashCode();
+        int equalHash1 = equalChunkHash1.hashCode();
+        int hash2 = chunkHash2.hashCode();
+
+        assertThat(hash1).isEqualTo(equalHash1);
+        assertThat(hash1).isNotEqualTo(hash2);
+    }
+
+    @Test
+    public void testCompareTo_whenEqual_returnsZero() {
+        ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
+        ChunkHash equalChunkHash = new ChunkHash(TEST_HASH_1);
+
+        int result = chunkHash.compareTo(equalChunkHash);
+
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void testCompareTo_whenArgumentGreater_returnsNegative() {
+        ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
+
+        int result = chunkHash1.compareTo(chunkHash2);
+
+        assertThat(result).isLessThan(0);
+    }
+
+    @Test
+    public void testCompareTo_whenArgumentSmaller_returnsPositive() {
+        ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
+        ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
+
+        int result = chunkHash2.compareTo(chunkHash1);
+
+        assertThat(result).isGreaterThan(0);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/ChunkListingMap.java b/com/android/server/backup/encryption/chunk/ChunkListingMap.java
new file mode 100644
index 0000000..51d7d53
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/ChunkListingMap.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunk;
+
+import android.annotation.Nullable;
+
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Chunk listing in a format optimized for quick look up of chunks via their hash keys. This is
+ * useful when building an incremental backup. After a chunk has been produced, the algorithm can
+ * quickly look up whether the chunk existed in the previous backup by checking this chunk listing.
+ * It can then tell the server to use that chunk, through telling it the position and length of the
+ * chunk in the previous backup's blob.
+ */
+public class ChunkListingMap {
+
+    private final Map<ChunkHash, Entry> mChunksByHash;
+
+    /** Construct a map from a {@link ChunksMetadataProto.ChunkListing} protobuf */
+    public static ChunkListingMap fromProto(ChunksMetadataProto.ChunkListing chunkListingProto) {
+        Map<ChunkHash, Entry> entries = new HashMap<>();
+
+        long start = 0;
+
+        for (ChunksMetadataProto.Chunk chunk : chunkListingProto.chunks) {
+            entries.put(new ChunkHash(chunk.hash), new Entry(start, chunk.length));
+            start += chunk.length;
+        }
+
+        return new ChunkListingMap(entries);
+    }
+
+    private ChunkListingMap(Map<ChunkHash, Entry> chunksByHash) {
+        // This is only called from the {@link #fromProto} method, so we don't
+        // need to take a copy.
+        this.mChunksByHash = chunksByHash;
+    }
+
+    /** Returns {@code true} if there is a chunk with the given SHA-256 MAC key in the listing. */
+    public boolean hasChunk(ChunkHash hash) {
+        return mChunksByHash.containsKey(hash);
+    }
+
+    /**
+     * Returns the entry for the chunk with the given hash.
+     *
+     * @param hash The SHA-256 MAC of the plaintext of the chunk.
+     * @return The entry, containing position and length of the chunk in the backup blob, or null if
+     *     it does not exist.
+     */
+    @Nullable
+    public Entry getChunkEntry(ChunkHash hash) {
+        return mChunksByHash.get(hash);
+    }
+
+    /** Information about a chunk entry in a backup blob - i.e., its position and length. */
+    public static final class Entry {
+        private final int mLength;
+        private final long mStart;
+
+        private Entry(long start, int length) {
+            mLength = length;
+            mStart = start;
+        }
+
+        /** Returns the length of the chunk in bytes. */
+        public int getLength() {
+            return mLength;
+        }
+
+        /** Returns the start position of the chunk in the backup blob, in bytes. */
+        public long getStart() {
+            return mStart;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java b/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java
new file mode 100644
index 0000000..c5f78c2
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/ChunkListingMapTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunk;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import com.google.common.base.Charsets;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ChunkListingMapTest {
+    private static final ChunkHash CHUNK_A_HASH = getHash("CHUNK_A");
+    private static final ChunkHash CHUNK_B_HASH = getHash("CHUNK_B");
+    private static final ChunkHash CHUNK_C_HASH = getHash("CHUNK_C");
+
+    private static final int CHUNK_A_LENGTH = 256;
+    private static final int CHUNK_B_LENGTH = 1024;
+    private static final int CHUNK_C_LENGTH = 4055;
+
+    private static final int CHUNK_A_START = 0;
+    private static final int CHUNK_B_START = CHUNK_A_START + CHUNK_A_LENGTH;
+    private static final int CHUNK_C_START = CHUNK_B_START + CHUNK_B_LENGTH;
+
+    private ChunkListingMap mChunkListingMap;
+
+    @Before
+    public void setUp() {
+        mChunkListingMap = createFromFixture();
+    }
+
+    @Test
+    public void hasChunk_isTrueForExistingChunks() {
+        assertThat(mChunkListingMap.hasChunk(CHUNK_A_HASH)).isTrue();
+        assertThat(mChunkListingMap.hasChunk(CHUNK_B_HASH)).isTrue();
+        assertThat(mChunkListingMap.hasChunk(CHUNK_C_HASH)).isTrue();
+    }
+
+    @Test
+    public void hasChunk_isFalseForNonexistentChunks() {
+        assertThat(mChunkListingMap.hasChunk(getHash("CHUNK_D"))).isFalse();
+        assertThat(mChunkListingMap.hasChunk(getHash(""))).isFalse();
+    }
+
+    @Test
+    public void getChunkListing_hasCorrectLengths() {
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getLength())
+                .isEqualTo(CHUNK_A_LENGTH);
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getLength())
+                .isEqualTo(CHUNK_B_LENGTH);
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getLength())
+                .isEqualTo(CHUNK_C_LENGTH);
+    }
+
+    @Test
+    public void getChunkListing_hasCorrectStarts() {
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getStart())
+                .isEqualTo(CHUNK_A_START);
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getStart())
+                .isEqualTo(CHUNK_B_START);
+        assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getStart())
+                .isEqualTo(CHUNK_C_START);
+    }
+
+    @Test
+    public void getChunkListing_isNullForNonExistentChunks() {
+        assertThat(mChunkListingMap.getChunkEntry(getHash("Hey"))).isNull();
+    }
+
+    private static ChunkListingMap createFromFixture() {
+        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
+        chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
+        chunkListing.chunks[0] = newChunk(CHUNK_A_HASH.getHash(), CHUNK_A_LENGTH);
+        chunkListing.chunks[1] = newChunk(CHUNK_B_HASH.getHash(), CHUNK_B_LENGTH);
+        chunkListing.chunks[2] = newChunk(CHUNK_C_HASH.getHash(), CHUNK_C_LENGTH);
+        return ChunkListingMap.fromProto(chunkListing);
+    }
+
+    private static ChunkHash getHash(String name) {
+        return new ChunkHash(
+                Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
+    }
+
+    public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
+        ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
+        newChunk.hash = Arrays.copyOf(hash, hash.length);
+        newChunk.length = length;
+        return newChunk;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/ChunkOrderingType.java b/com/android/server/backup/encryption/chunk/ChunkOrderingType.java
new file mode 100644
index 0000000..9cda339
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/ChunkOrderingType.java
@@ -0,0 +1,31 @@
+/*
+ * 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.encryption.chunk;
+
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.EXPLICIT_STARTS;
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.INLINE_LENGTHS;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** IntDef corresponding to the ChunkOrderingType enum in the ChunksMetadataProto protobuf. */
+@IntDef({CHUNK_ORDERING_TYPE_UNSPECIFIED, EXPLICIT_STARTS, INLINE_LENGTHS})
+@Retention(RetentionPolicy.SOURCE)
+public @interface ChunkOrderingType {}
diff --git a/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java b/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
new file mode 100644
index 0000000..edf1b9a
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
@@ -0,0 +1,68 @@
+/*
+ * 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.encryption.chunk;
+
+import java.util.Arrays;
+
+/**
+ * Holds the bytes of an encrypted {@link ChunksMetadataProto.ChunkOrdering}.
+ *
+ * <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
+ * encryptedChunkOrdering() to getBytes().
+ */
+public class EncryptedChunkOrdering {
+    /**
+     * Constructs a new object holding the given bytes of an encrypted {@link
+     * ChunksMetadataProto.ChunkOrdering}.
+     *
+     * <p>Note that this just holds an ordering which is already encrypted, it does not encrypt the
+     * ordering.
+     */
+    public static EncryptedChunkOrdering create(byte[] encryptedChunkOrdering) {
+        return new EncryptedChunkOrdering(encryptedChunkOrdering);
+    }
+
+    private final byte[] mEncryptedChunkOrdering;
+
+    /** Get the encrypted chunk ordering */
+    public byte[] encryptedChunkOrdering() {
+        return mEncryptedChunkOrdering;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof EncryptedChunkOrdering)) {
+            return false;
+        }
+
+        EncryptedChunkOrdering encryptedChunkOrdering = (EncryptedChunkOrdering) o;
+        return Arrays.equals(
+                mEncryptedChunkOrdering, encryptedChunkOrdering.mEncryptedChunkOrdering);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mEncryptedChunkOrdering);
+    }
+
+    private EncryptedChunkOrdering(byte[] encryptedChunkOrdering) {
+        mEncryptedChunkOrdering = encryptedChunkOrdering;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java b/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
new file mode 100644
index 0000000..c6b29b7
--- /dev/null
+++ b/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.encryption.chunk;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class EncryptedChunkOrderingTest {
+    private static final byte[] TEST_BYTE_ARRAY_1 = new byte[] {1, 2, 3, 4, 5};
+    private static final byte[] TEST_BYTE_ARRAY_2 = new byte[] {5, 4, 3, 2, 1};
+
+    @Test
+    public void testEncryptedChunkOrdering_returnsValue() {
+        EncryptedChunkOrdering encryptedChunkOrdering =
+                EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
+
+        byte[] bytes = encryptedChunkOrdering.encryptedChunkOrdering();
+
+        assertThat(bytes)
+                .asList()
+                .containsExactlyElementsIn(Bytes.asList(TEST_BYTE_ARRAY_1))
+                .inOrder();
+    }
+
+    @Test
+    public void testEquals() {
+        EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
+        EncryptedChunkOrdering equalChunkOrdering1 =
+                EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
+        EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
+
+        assertThat(chunkOrdering1).isEqualTo(equalChunkOrdering1);
+        assertThat(chunkOrdering1).isNotEqualTo(chunkOrdering2);
+    }
+
+    @Test
+    public void testHashCode() {
+        EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
+        EncryptedChunkOrdering equalChunkOrdering1 =
+                EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
+        EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
+
+        int hash1 = chunkOrdering1.hashCode();
+        int equalHash1 = equalChunkOrdering1.hashCode();
+        int hash2 = chunkOrdering2.hashCode();
+
+        assertThat(hash1).isEqualTo(equalHash1);
+        assertThat(hash1).isNotEqualTo(hash2);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/BackupFileBuilder.java b/com/android/server/backup/encryption/chunking/BackupFileBuilder.java
new file mode 100644
index 0000000..4010bfd
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/BackupFileBuilder.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunk.ChunkListingMap;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Writes batches of {@link EncryptedChunk} to a diff script, and generates the associated {@link
+ * ChunksMetadataProto.ChunkListing} and {@link ChunksMetadataProto.ChunkOrdering}.
+ */
+public class BackupFileBuilder {
+    private static final String TAG = "BackupFileBuilder";
+
+    private static final int BYTES_PER_KILOBYTE = 1024;
+
+    private final BackupWriter mBackupWriter;
+    private final EncryptedChunkEncoder mEncryptedChunkEncoder;
+    private final ChunkListingMap mOldChunkListing;
+    private final ChunksMetadataProto.ChunkListing mNewChunkListing;
+    private final ChunksMetadataProto.ChunkOrdering mChunkOrdering;
+    private final List<ChunksMetadataProto.Chunk> mKnownChunks = new ArrayList<>();
+    private final List<Integer> mKnownStarts = new ArrayList<>();
+    private final Map<ChunkHash, Long> mChunkStartPositions;
+
+    private long mNewChunksSizeBytes;
+    private boolean mFinished;
+
+    /**
+     * Constructs a new instance which writes raw data to the given {@link OutputStream}, without
+     * generating a diff.
+     *
+     * <p>This class never closes the output stream.
+     */
+    public static BackupFileBuilder createForNonIncremental(OutputStream outputStream) {
+        return new BackupFileBuilder(
+                new RawBackupWriter(outputStream), new ChunksMetadataProto.ChunkListing());
+    }
+
+    /**
+     * Constructs a new instance which writes a diff script to the given {@link OutputStream} using
+     * a {@link SingleStreamDiffScriptWriter}.
+     *
+     * <p>This class never closes the output stream.
+     *
+     * @param oldChunkListing against which the diff will be generated.
+     */
+    public static BackupFileBuilder createForIncremental(
+            OutputStream outputStream, ChunksMetadataProto.ChunkListing oldChunkListing) {
+        return new BackupFileBuilder(
+                DiffScriptBackupWriter.newInstance(outputStream), oldChunkListing);
+    }
+
+    private BackupFileBuilder(
+            BackupWriter backupWriter, ChunksMetadataProto.ChunkListing oldChunkListing) {
+        this.mBackupWriter = backupWriter;
+        // TODO(b/77188289): Use InlineLengthsEncryptedChunkEncoder for key-value backups
+        this.mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
+        this.mOldChunkListing = ChunkListingMap.fromProto(oldChunkListing);
+
+        mNewChunkListing = new ChunksMetadataProto.ChunkListing();
+        mNewChunkListing.cipherType = ChunksMetadataProto.AES_256_GCM;
+        mNewChunkListing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+
+        mChunkOrdering = new ChunksMetadataProto.ChunkOrdering();
+        mChunkStartPositions = new HashMap<>();
+    }
+
+    /**
+     * Writes the given chunks to the output stream, and adds them to the new chunk listing and
+     * chunk ordering.
+     *
+     * <p>Sorts the chunks in lexicographical order before writing.
+     *
+     * @param allChunks The hashes of all the chunks, in the order they appear in the plaintext.
+     * @param newChunks A map from hash to {@link EncryptedChunk} containing the new chunks not
+     *     present in the previous backup.
+     */
+    public void writeChunks(List<ChunkHash> allChunks, Map<ChunkHash, EncryptedChunk> newChunks)
+            throws IOException {
+        checkState(!mFinished, "Cannot write chunks after flushing.");
+
+        List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
+        Collections.sort(sortedChunks);
+        for (ChunkHash chunkHash : sortedChunks) {
+            // As we have already included this chunk in the backup file, don't add it again to
+            // deduplicate identical chunks.
+            if (!mChunkStartPositions.containsKey(chunkHash)) {
+                // getBytesWritten() gives us the start of the chunk.
+                mChunkStartPositions.put(chunkHash, mBackupWriter.getBytesWritten());
+
+                writeChunkToFileAndListing(chunkHash, newChunks);
+            }
+        }
+
+        long totalSizeKb = mBackupWriter.getBytesWritten() / BYTES_PER_KILOBYTE;
+        long newChunksSizeKb = mNewChunksSizeBytes / BYTES_PER_KILOBYTE;
+        Slog.d(
+                TAG,
+                "Total backup size: "
+                        + totalSizeKb
+                        + " kb, new chunks size: "
+                        + newChunksSizeKb
+                        + " kb");
+
+        for (ChunkHash chunkHash : allChunks) {
+            mKnownStarts.add(mChunkStartPositions.get(chunkHash).intValue());
+        }
+    }
+
+    /**
+     * Returns a new listing for all of the chunks written so far, setting the given fingerprint
+     * mixer salt (this overrides the {@link ChunksMetadataProto.ChunkListing#fingerprintMixerSalt}
+     * in the old {@link ChunksMetadataProto.ChunkListing} passed into the
+     * {@link #BackupFileBuilder).
+     */
+    public ChunksMetadataProto.ChunkListing getNewChunkListing(
+            @Nullable byte[] fingerprintMixerSalt) {
+        // TODO: b/141537803 Add check to ensure this is called only once per instance
+        mNewChunkListing.fingerprintMixerSalt =
+                fingerprintMixerSalt != null
+                        ? Arrays.copyOf(fingerprintMixerSalt, fingerprintMixerSalt.length)
+                        : new byte[0];
+        mNewChunkListing.chunks = mKnownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
+        return mNewChunkListing;
+    }
+
+    /** Returns a new ordering for all of the chunks written so far, setting the given checksum. */
+    public ChunksMetadataProto.ChunkOrdering getNewChunkOrdering(byte[] checksum) {
+        // TODO: b/141537803 Add check to ensure this is called only once per instance
+        mChunkOrdering.starts = new int[mKnownStarts.size()];
+        for (int i = 0; i < mKnownStarts.size(); i++) {
+            mChunkOrdering.starts[i] = mKnownStarts.get(i).intValue();
+        }
+        mChunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
+        return mChunkOrdering;
+    }
+
+    /**
+     * Finishes the backup file by writing the chunk metadata and metadata position.
+     *
+     * <p>Once this is called, calling {@link #writeChunks(List, Map)} will throw {@link
+     * IllegalStateException}.
+     */
+    public void finish(ChunksMetadataProto.ChunksMetadata metadata) throws IOException {
+        Objects.requireNonNull(metadata, "Metadata cannot be null");
+
+        long startOfMetadata = mBackupWriter.getBytesWritten();
+        mBackupWriter.writeBytes(ChunksMetadataProto.ChunksMetadata.toByteArray(metadata));
+        mBackupWriter.writeBytes(toByteArray(startOfMetadata));
+
+        mBackupWriter.flush();
+        mFinished = true;
+    }
+
+    /**
+     * Checks if the given chunk hash references an existing chunk or a new chunk, and adds this
+     * chunk to the backup file and new chunk listing.
+     */
+    private void writeChunkToFileAndListing(
+            ChunkHash chunkHash, Map<ChunkHash, EncryptedChunk> newChunks) throws IOException {
+        Objects.requireNonNull(chunkHash, "Hash cannot be null");
+
+        if (mOldChunkListing.hasChunk(chunkHash)) {
+            ChunkListingMap.Entry oldChunk = mOldChunkListing.getChunkEntry(chunkHash);
+            mBackupWriter.writeChunk(oldChunk.getStart(), oldChunk.getLength());
+
+            checkArgument(oldChunk.getLength() >= 0, "Chunk must have zero or positive length");
+            addChunk(chunkHash.getHash(), oldChunk.getLength());
+        } else if (newChunks.containsKey(chunkHash)) {
+            EncryptedChunk newChunk = newChunks.get(chunkHash);
+            mEncryptedChunkEncoder.writeChunkToWriter(mBackupWriter, newChunk);
+            int length = mEncryptedChunkEncoder.getEncodedLengthOfChunk(newChunk);
+            mNewChunksSizeBytes += length;
+
+            checkArgument(length >= 0, "Chunk must have zero or positive length");
+            addChunk(chunkHash.getHash(), length);
+        } else {
+            throw new IllegalArgumentException(
+                    "Chunk did not exist in old chunks or new chunks: " + chunkHash);
+        }
+    }
+
+    private void addChunk(byte[] chunkHash, int length) {
+        ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
+        chunk.hash = Arrays.copyOf(chunkHash, chunkHash.length);
+        chunk.length = length;
+        mKnownChunks.add(chunk);
+    }
+
+    private static byte[] toByteArray(long value) {
+        // Note that this code needs to stay compatible with GWT, which has known
+        // bugs when narrowing byte casts of long values occur.
+        byte[] result = new byte[8];
+        for (int i = 7; i >= 0; i--) {
+            result[i] = (byte) (value & 0xffL);
+            value >>= 8;
+        }
+        return result;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java b/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java
new file mode 100644
index 0000000..590938e
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/BackupFileBuilderTest.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.AES_256_GCM;
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+import static com.android.server.backup.testing.CryptoTestUtils.newChunk;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static junit.framework.Assert.fail;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.testing.DiffScriptProcessor;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Longs;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupFileBuilderTest {
+    private static final String TEST_DATA_1 =
+            "I'm already there or close to [T7-9/executive level] in terms of big-picture vision";
+    private static final String TEST_DATA_2 =
+            "I was known for Real Games and should have been brought in for advice";
+    private static final String TEST_DATA_3 =
+            "Pride is rooted in the delusional belief held by all humans in an unchanging self";
+
+    private static final byte[] TEST_FINGERPRINT_MIXER_SALT =
+            Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
+
+    private static final ChunkHash TEST_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {0}, EncryptedChunk.KEY_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_3 =
+            new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
+
+    private static final byte[] TEST_NONCE =
+            Arrays.copyOf(new byte[] {3}, EncryptedChunk.NONCE_LENGTH_BYTES);
+
+    private static final EncryptedChunk TEST_CHUNK_1 =
+            EncryptedChunk.create(TEST_HASH_1, TEST_NONCE, TEST_DATA_1.getBytes(UTF_8));
+    private static final EncryptedChunk TEST_CHUNK_2 =
+            EncryptedChunk.create(TEST_HASH_2, TEST_NONCE, TEST_DATA_2.getBytes(UTF_8));
+    private static final EncryptedChunk TEST_CHUNK_3 =
+            EncryptedChunk.create(TEST_HASH_3, TEST_NONCE, TEST_DATA_3.getBytes(UTF_8));
+
+    private static final byte[] TEST_CHECKSUM = {1, 2, 3, 4, 5, 6};
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private File mOldFile;
+    private ChunksMetadataProto.ChunkListing mOldChunkListing;
+    private EncryptedChunkEncoder mEncryptedChunkEncoder;
+
+    @Before
+    public void setUp() {
+        mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
+    }
+
+    @Test
+    public void writeChunks_nonIncremental_writesCorrectRawData() throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
+
+        byte[] actual = output.toByteArray();
+        byte[] expected =
+                Bytes.concat(
+                        TEST_CHUNK_1.nonce(),
+                        TEST_CHUNK_1.encryptedBytes(),
+                        TEST_CHUNK_2.nonce(),
+                        TEST_CHUNK_2.encryptedBytes());
+        assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
+    }
+
+    @Test
+    public void writeChunks_nonIncrementalWithDuplicates_writesEachChunkOnlyOnce()
+            throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_1),
+                getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
+
+        byte[] actual = output.toByteArray();
+        byte[] expected =
+                Bytes.concat(
+                        TEST_CHUNK_1.nonce(),
+                        TEST_CHUNK_1.encryptedBytes(),
+                        TEST_CHUNK_2.nonce(),
+                        TEST_CHUNK_2.encryptedBytes());
+        assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
+    }
+
+    @Test
+    public void writeChunks_incremental_writesParsableDiffScript() throws Exception {
+        // We will insert chunk 2 in between chunks 1 and 3.
+        setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
+        ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
+                getNewChunkMap(TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        byte[] actual =
+                stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
+        byte[] expected =
+                Bytes.concat(
+                        TEST_CHUNK_1.nonce(),
+                        TEST_CHUNK_1.encryptedBytes(),
+                        TEST_CHUNK_2.nonce(),
+                        TEST_CHUNK_2.encryptedBytes(),
+                        TEST_CHUNK_3.nonce(),
+                        TEST_CHUNK_3.encryptedBytes());
+        assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
+    }
+
+    @Test
+    public void writeChunks_incrementalWithDuplicates_writesEachChunkOnlyOnce() throws Exception {
+        // We will insert chunk 2 twice in between chunks 1 and 3.
+        setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
+        ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2, TEST_HASH_3),
+                getNewChunkMap(TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        byte[] actual =
+                stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
+        byte[] expected =
+                Bytes.concat(
+                        TEST_CHUNK_1.nonce(),
+                        TEST_CHUNK_1.encryptedBytes(),
+                        TEST_CHUNK_2.nonce(),
+                        TEST_CHUNK_2.encryptedBytes(),
+                        TEST_CHUNK_3.nonce(),
+                        TEST_CHUNK_3.encryptedBytes());
+        assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
+    }
+
+    @Test
+    public void writeChunks_writesChunksInOrderOfHash() throws Exception {
+        setUpOldBackupWithChunks(ImmutableList.of());
+        ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
+
+        // Write chunks out of order.
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_2, TEST_HASH_1),
+                getNewChunkMap(TEST_HASH_2, TEST_HASH_1));
+        backupFileBuilder.finish(getTestMetadata());
+
+        byte[] actual =
+                stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
+        byte[] expected =
+                Bytes.concat(
+                        TEST_CHUNK_1.nonce(),
+                        TEST_CHUNK_1.encryptedBytes(),
+                        TEST_CHUNK_2.nonce(),
+                        TEST_CHUNK_2.encryptedBytes());
+        assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
+    }
+
+    @Test
+    public void writeChunks_alreadyFlushed_throwsException() throws Exception {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
+        backupFileBuilder.finish(getTestMetadata());
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> backupFileBuilder.writeChunks(ImmutableList.of(), getNewChunkMap()));
+    }
+
+    @Test
+    public void getNewChunkListing_hasChunksInOrderOfKey() throws Exception {
+        // We will insert chunk 2 in between chunks 1 and 3.
+        setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        // Write chunks out of order.
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
+        ChunksMetadataProto.ChunkListing actual =
+                backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
+        assertListingsEqual(actual, expected);
+    }
+
+    @Test
+    public void getNewChunkListing_writeChunksInTwoBatches_returnsListingContainingAllChunks()
+            throws Exception {
+        // We will insert chunk 2 in between chunks 1 and 3.
+        setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2), getNewChunkMap(TEST_HASH_2));
+        backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
+        ChunksMetadataProto.ChunkListing actual =
+                backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
+        assertListingsEqual(actual, expected);
+    }
+
+    @Test
+    public void getNewChunkListing_writeDuplicateChunks_writesEachChunkOnlyOnce() throws Exception {
+        // We will append [2][3][3][2] onto [1].
+        setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1));
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
+                getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_3, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
+        ChunksMetadataProto.ChunkListing actual =
+                backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
+        assertListingsEqual(actual, expected);
+    }
+
+    @Test
+    public void getNewChunkListing_nonIncrementalWithNoSalt_doesNotThrowOnSerialisation() {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        // Does not throw.
+        ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
+    }
+
+    @Test
+    public void getNewChunkListing_incrementalWithNoSalt_doesNotThrowOnSerialisation()
+            throws Exception {
+
+        setUpOldBackupWithChunks(ImmutableList.of());
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        // Does not throw.
+        ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
+    }
+
+    @Test
+    public void getNewChunkListing_nonIncrementalWithNoSalt_hasEmptySalt() {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
+    }
+
+    @Test
+    public void getNewChunkListing_incrementalWithNoSalt_hasEmptySalt() throws Exception {
+        setUpOldBackupWithChunks(ImmutableList.of());
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
+    }
+
+    @Test
+    public void getNewChunkListing_nonIncrementalWithSalt_hasGivenSalt() {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
+
+        assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
+    }
+
+    @Test
+    public void getNewChunkListing_incrementalWithSalt_hasGivenSalt() throws Exception {
+        setUpOldBackupWithChunks(ImmutableList.of());
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
+
+        assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
+    }
+
+    @Test
+    public void getNewChunkListing_nonIncremental_hasCorrectCipherTypeAndChunkOrderingType() {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
+        assertThat(newChunkListing.chunkOrderingType)
+                .isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
+    }
+
+    @Test
+    public void getNewChunkListing_incremental_hasCorrectCipherTypeAndChunkOrderingType()
+            throws Exception {
+        setUpOldBackupWithChunks(ImmutableList.of());
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), mOldChunkListing);
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
+
+        assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
+        assertThat(newChunkListing.chunkOrderingType)
+                .isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
+    }
+
+    @Test
+    public void getNewChunkOrdering_chunksHaveCorrectStartPositions() throws Exception {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
+
+        // Write out of order by key to check that ordering is maintained.
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkOrdering actual =
+                backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
+        // The chunks are listed in the order they are written above, but the start positions are
+        // determined by the order in the encrypted blob (which is lexicographical by key).
+        int chunk1Start = 0;
+        int chunk2Start =
+                chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
+        int chunk3Start =
+                chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
+
+        int[] expected = {chunk1Start, chunk3Start, chunk2Start};
+        assertThat(actual.starts.length).isEqualTo(expected.length);
+        for (int i = 0; i < actual.starts.length; i++) {
+            assertThat(expected[i]).isEqualTo(actual.starts[i]);
+        }
+    }
+
+    @Test
+    public void getNewChunkOrdering_duplicateChunks_writesDuplicates() throws Exception {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
+
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
+        backupFileBuilder.writeChunks(
+                ImmutableList.of(TEST_HASH_3, TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkOrdering actual =
+                backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
+        int chunk1Start = 0;
+        int chunk2Start =
+                chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
+        int chunk3Start =
+                chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
+
+        int[] expected = {chunk1Start, chunk2Start, chunk2Start, chunk3Start, chunk3Start};
+        assertThat(actual.starts.length).isEqualTo(expected.length);
+        for (int i = 0; i < actual.starts.length; i++) {
+            assertThat(expected[i]).isEqualTo(actual.starts[i]);
+        }
+    }
+
+    @Test
+    public void getNewChunkOrdering_returnsOrderingWithChecksum() throws Exception {
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
+
+        backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
+        backupFileBuilder.finish(getTestMetadata());
+
+        ChunksMetadataProto.ChunkOrdering actual =
+                backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
+        assertThat(actual.checksum).isEqualTo(TEST_CHECKSUM);
+    }
+
+    @Test
+    public void finish_writesMetadata() throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
+        ChunksMetadataProto.ChunksMetadata expectedMetadata = getTestMetadata();
+
+        builder.finish(expectedMetadata);
+
+        // The output is [metadata]+[long giving size of metadata].
+        byte[] metadataBytes =
+                Arrays.copyOfRange(output.toByteArray(), 0, output.size() - Long.BYTES);
+        ChunksMetadataProto.ChunksMetadata actualMetadata =
+                ChunksMetadataProto.ChunksMetadata.parseFrom(metadataBytes);
+        assertThat(actualMetadata.checksumType).isEqualTo(ChunksMetadataProto.SHA_256);
+        assertThat(actualMetadata.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
+    }
+
+    @Test
+    public void finish_writesMetadataPosition() throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
+
+        builder.writeChunks(
+                ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
+                getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
+        builder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
+        builder.finish(getTestMetadata());
+
+        long expectedPosition =
+                (long) mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1)
+                        + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2)
+                        + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_3);
+        long actualPosition =
+                Longs.fromByteArray(
+                        Arrays.copyOfRange(
+                                output.toByteArray(), output.size() - Long.BYTES, output.size()));
+        assertThat(actualPosition).isEqualTo(expectedPosition);
+    }
+
+    @Test
+    public void finish_flushesOutputStream() throws Exception {
+        OutputStream diffOutputStream = mock(OutputStream.class);
+        BackupFileBuilder backupFileBuilder =
+                BackupFileBuilder.createForIncremental(
+                        diffOutputStream, new ChunksMetadataProto.ChunkListing());
+
+        backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
+        diffOutputStream.flush();
+
+        verify(diffOutputStream).flush();
+    }
+
+    private void setUpOldBackupWithChunks(List<EncryptedChunk> chunks) throws Exception {
+        mOldFile = mTemporaryFolder.newFile();
+        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
+        chunkListing.fingerprintMixerSalt =
+                Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
+        chunkListing.cipherType = AES_256_GCM;
+        chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
+
+        List<ChunksMetadataProto.Chunk> knownChunks = new ArrayList<>();
+        try (FileOutputStream outputStream = new FileOutputStream(mOldFile)) {
+            for (EncryptedChunk chunk : chunks) {
+                // Chunks are encoded in the format [nonce]+[data].
+                outputStream.write(chunk.nonce());
+                outputStream.write(chunk.encryptedBytes());
+
+                knownChunks.add(createChunkFor(chunk));
+            }
+
+            outputStream.flush();
+        }
+
+        chunkListing.chunks = knownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
+        mOldChunkListing = chunkListing;
+    }
+
+    private byte[] parseDiffScript(byte[] diffScript) throws Exception {
+        File newFile = mTemporaryFolder.newFile();
+        new DiffScriptProcessor(mOldFile, newFile).process(new ByteArrayInputStream(diffScript));
+        return Files.toByteArray(newFile);
+    }
+
+    private void assertListingsEqual(
+            ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
+        assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
+        for (int i = 0; i < result.chunks.length; i++) {
+            assertWithMessage("Chunk " + i)
+                    .that(result.chunks[i].length)
+                    .isEqualTo(expected.chunks[i].length);
+            assertWithMessage("Chunk " + i)
+                    .that(result.chunks[i].hash)
+                    .isEqualTo(expected.chunks[i].hash);
+        }
+    }
+
+    private static ImmutableMap<ChunkHash, EncryptedChunk> getNewChunkMap(ChunkHash... hashes) {
+        ImmutableMap.Builder<ChunkHash, EncryptedChunk> builder = ImmutableMap.builder();
+        for (ChunkHash hash : hashes) {
+            if (TEST_HASH_1.equals(hash)) {
+                builder.put(TEST_HASH_1, TEST_CHUNK_1);
+            } else if (TEST_HASH_2.equals(hash)) {
+                builder.put(TEST_HASH_2, TEST_CHUNK_2);
+            } else if (TEST_HASH_3.equals(hash)) {
+                builder.put(TEST_HASH_3, TEST_CHUNK_3);
+            } else {
+                fail("Hash was not recognised: " + hash);
+            }
+        }
+        return builder.build();
+    }
+
+    private static ChunksMetadataProto.ChunksMetadata getTestMetadata() {
+        ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
+        metadata.checksumType = ChunksMetadataProto.SHA_256;
+        metadata.cipherType = AES_256_GCM;
+        return metadata;
+    }
+
+    private static byte[] stripMetadataAndPositionFromOutput(byte[] output) {
+        long metadataStart =
+                Longs.fromByteArray(
+                        Arrays.copyOfRange(output, output.length - Long.BYTES, output.length));
+        return Arrays.copyOfRange(output, 0, (int) metadataStart);
+    }
+
+    private ChunksMetadataProto.ChunkListing expectedChunkListing() {
+        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
+        chunkListing.fingerprintMixerSalt =
+                Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
+        chunkListing.cipherType = AES_256_GCM;
+        chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
+        chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
+        chunkListing.chunks[0] = createChunkFor(TEST_CHUNK_1);
+        chunkListing.chunks[1] = createChunkFor(TEST_CHUNK_2);
+        chunkListing.chunks[2] = createChunkFor(TEST_CHUNK_3);
+        return chunkListing;
+    }
+
+    private ChunksMetadataProto.Chunk createChunkFor(EncryptedChunk encryptedChunk) {
+        byte[] chunkHash = encryptedChunk.key().getHash();
+        byte[] hashCopy = Arrays.copyOf(chunkHash, chunkHash.length);
+        return newChunk(hashCopy, mEncryptedChunkEncoder.getEncodedLengthOfChunk(encryptedChunk));
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/BackupWriter.java b/com/android/server/backup/encryption/chunking/BackupWriter.java
new file mode 100644
index 0000000..baa820c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/BackupWriter.java
@@ -0,0 +1,43 @@
+/*
+ * 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.encryption.chunking;
+
+import java.io.IOException;
+
+/** Writes backup data either as a diff script or as raw data, determined by the implementation. */
+public interface BackupWriter {
+    /** Writes the given bytes to the output. */
+    void writeBytes(byte[] bytes) throws IOException;
+
+    /**
+     * Writes an existing chunk from the previous backup to the output.
+     *
+     * <p>Note: not all implementations support this method.
+     */
+    void writeChunk(long start, int length) throws IOException;
+
+    /** Returns the number of bytes written, included bytes copied from the old file. */
+    long getBytesWritten();
+
+    /**
+     * Indicates that no more bytes or chunks will be written.
+     *
+     * <p>After calling this, you may not call {@link #writeBytes(byte[])} or {@link
+     * #writeChunk(long, int)}
+     */
+    void flush() throws IOException;
+}
diff --git a/com/android/server/backup/encryption/chunking/ByteRange.java b/com/android/server/backup/encryption/chunking/ByteRange.java
new file mode 100644
index 0000000..004d9e3
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ByteRange.java
@@ -0,0 +1,80 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.internal.util.Preconditions;
+
+/** Representation of a range of bytes to be downloaded. */
+final class ByteRange {
+    private final long mStart;
+    private final long mEnd;
+
+    /** Creates a range of bytes which includes {@code mStart} and {@code mEnd}. */
+    ByteRange(long start, long end) {
+        Preconditions.checkArgument(start >= 0);
+        Preconditions.checkArgument(end >= start);
+        mStart = start;
+        mEnd = end;
+    }
+
+    /** Returns the start of the {@code ByteRange}. The start is included in the range. */
+    long getStart() {
+        return mStart;
+    }
+
+    /** Returns the end of the {@code ByteRange}. The end is included in the range. */
+    long getEnd() {
+        return mEnd;
+    }
+
+    /** Returns the number of bytes included in the {@code ByteRange}. */
+    int getLength() {
+        return (int) (mEnd - mStart + 1);
+    }
+
+    /** Creates a new {@link ByteRange} from {@code mStart} to {@code mEnd + length}. */
+    ByteRange extend(long length) {
+        Preconditions.checkArgument(length > 0);
+        return new ByteRange(mStart, mEnd + length);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ByteRange)) {
+            return false;
+        }
+
+        ByteRange byteRange = (ByteRange) o;
+        return (mEnd == byteRange.mEnd && mStart == byteRange.mStart);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + (int) (mStart ^ (mStart >>> 32));
+        result = 31 * result + (int) (mEnd ^ (mEnd >>> 32));
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ByteRange{mStart=%d, mEnd=%d}", mStart, mEnd);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ByteRangeTest.java b/com/android/server/backup/encryption/chunking/ByteRangeTest.java
new file mode 100644
index 0000000..8df0826
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ByteRangeTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.encryption.chunking;
+
+import static org.junit.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link ByteRange}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ByteRangeTest {
+    @Test
+    public void getLength_includesEnd() throws Exception {
+        ByteRange byteRange = new ByteRange(5, 10);
+
+        int length = byteRange.getLength();
+
+        assertEquals(6, length);
+    }
+
+    @Test
+    public void constructor_rejectsNegativeStart() {
+        assertThrows(IllegalArgumentException.class, () -> new ByteRange(-1, 10));
+    }
+
+    @Test
+    public void constructor_rejectsEndBeforeStart() {
+        assertThrows(IllegalArgumentException.class, () -> new ByteRange(10, 9));
+    }
+
+    @Test
+    public void extend_withZeroLength_throwsException() {
+        ByteRange byteRange = new ByteRange(5, 10);
+
+        assertThrows(IllegalArgumentException.class, () -> byteRange.extend(0));
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ChunkEncryptor.java b/com/android/server/backup/encryption/chunking/ChunkEncryptor.java
new file mode 100644
index 0000000..48abc8c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ChunkEncryptor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+/** Encrypts chunks of a file using AES/GCM. */
+public class ChunkEncryptor {
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+
+    private final SecretKey mSecretKey;
+    private final SecureRandom mSecureRandom;
+
+    /**
+     * A new instance using {@code mSecretKey} to encrypt chunks and {@code mSecureRandom} to
+     * generate nonces.
+     */
+    public ChunkEncryptor(SecretKey secretKey, SecureRandom secureRandom) {
+        this.mSecretKey = secretKey;
+        this.mSecureRandom = secureRandom;
+    }
+
+    /**
+     * Transforms {@code plaintext} into an {@link EncryptedChunk}.
+     *
+     * @param plaintextHash The hash of the plaintext to encrypt, to attach as the key of the chunk.
+     * @param plaintext Bytes to encrypt.
+     * @throws InvalidKeyException If the given secret key is not a valid AES key for decryption.
+     * @throws IllegalBlockSizeException If the input data cannot be encrypted using
+     *     AES/GCM/NoPadding. This should never be the case.
+     */
+    public EncryptedChunk encrypt(ChunkHash plaintextHash, byte[] plaintext)
+            throws InvalidKeyException, IllegalBlockSizeException {
+        byte[] nonce = generateNonce();
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+            cipher.init(
+                    Cipher.ENCRYPT_MODE,
+                    mSecretKey,
+                    new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, nonce));
+        } catch (NoSuchAlgorithmException
+                | NoSuchPaddingException
+                | InvalidAlgorithmParameterException e) {
+            // This can not happen - AES/GCM/NoPadding is supported.
+            throw new AssertionError(e);
+        }
+        byte[] encryptedBytes;
+        try {
+            encryptedBytes = cipher.doFinal(plaintext);
+        } catch (BadPaddingException e) {
+            // This can not happen - BadPaddingException can only be thrown in decrypt mode.
+            throw new AssertionError("Impossible: threw BadPaddingException in encrypt mode.");
+        }
+
+        return EncryptedChunk.create(/*key=*/ plaintextHash, nonce, encryptedBytes);
+    }
+
+    private byte[] generateNonce() {
+        byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
+        mSecureRandom.nextBytes(nonce);
+        return nonce;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java b/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java
new file mode 100644
index 0000000..19e3b28
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ChunkEncryptorTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ChunkEncryptorTest {
+    private static final String MAC_ALGORITHM = "HmacSHA256";
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final String CHUNK_PLAINTEXT =
+            "A little Learning is a dang'rous Thing;\n"
+                    + "Drink deep, or taste not the Pierian Spring:\n"
+                    + "There shallow Draughts intoxicate the Brain,\n"
+                    + "And drinking largely sobers us again.";
+    private static final byte[] PLAINTEXT_BYTES = CHUNK_PLAINTEXT.getBytes(UTF_8);
+    private static final byte[] NONCE_1 = "0123456789abc".getBytes(UTF_8);
+    private static final byte[] NONCE_2 = "123456789abcd".getBytes(UTF_8);
+
+    private static final byte[][] NONCES = new byte[][] {NONCE_1, NONCE_2};
+
+    @Mock private SecureRandom mSecureRandomMock;
+    private SecretKey mSecretKey;
+    private ChunkHash mPlaintextHash;
+    private ChunkEncryptor mChunkEncryptor;
+
+    @Before
+    public void setUp() throws Exception {
+        mSecretKey = generateAesKey();
+        ChunkHasher chunkHasher = new ChunkHasher(mSecretKey);
+        mPlaintextHash = chunkHasher.computeHash(PLAINTEXT_BYTES);
+        mSecureRandomMock = mock(SecureRandom.class);
+        mChunkEncryptor = new ChunkEncryptor(mSecretKey, mSecureRandomMock);
+
+        // Return NONCE_1, then NONCE_2 for invocations of mSecureRandomMock.nextBytes().
+        doAnswer(
+                    new Answer<Void>() {
+                        private int mInvocation = 0;
+
+                        @Override
+                        public Void answer(InvocationOnMock invocation) {
+                            byte[] nonceDestination = invocation.getArgument(0);
+                            System.arraycopy(
+                                    NONCES[this.mInvocation],
+                                    0,
+                                    nonceDestination,
+                                    0,
+                                    GCM_NONCE_LENGTH_BYTES);
+                            this.mInvocation++;
+                            return null;
+                        }
+                    })
+                .when(mSecureRandomMock)
+                .nextBytes(any(byte[].class));
+    }
+
+    @Test
+    public void encrypt_withHash_resultContainsHashAsKey() throws Exception {
+        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        assertThat(chunk.key()).isEqualTo(mPlaintextHash);
+    }
+
+    @Test
+    public void encrypt_generatesHmacOfPlaintext() throws Exception {
+        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        byte[] generatedHash = chunk.key().getHash();
+        Mac mac = Mac.getInstance(MAC_ALGORITHM);
+        mac.init(mSecretKey);
+        byte[] plaintextHmac = mac.doFinal(PLAINTEXT_BYTES);
+        assertThat(generatedHash).isEqualTo(plaintextHmac);
+    }
+
+    @Test
+    public void encrypt_whenInvokedAgain_generatesNewNonce() throws Exception {
+        EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        assertThat(chunk1.nonce()).isNotEqualTo(chunk2.nonce());
+    }
+
+    @Test
+    public void encrypt_whenInvokedAgain_generatesNewCiphertext() throws Exception {
+        EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        assertThat(chunk1.encryptedBytes()).isNotEqualTo(chunk2.encryptedBytes());
+    }
+
+    @Test
+    public void encrypt_generates12ByteNonce() throws Exception {
+        EncryptedChunk encryptedChunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        byte[] nonce = encryptedChunk.nonce();
+        assertThat(nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
+    }
+
+    @Test
+    public void encrypt_decryptedResultCorrespondsToPlaintext() throws Exception {
+        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);
+
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(
+                Cipher.DECRYPT_MODE,
+                mSecretKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, chunk.nonce()));
+        byte[] decrypted = cipher.doFinal(chunk.encryptedBytes());
+        assertThat(decrypted).isEqualTo(PLAINTEXT_BYTES);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ChunkHasher.java b/com/android/server/backup/encryption/chunking/ChunkHasher.java
new file mode 100644
index 0000000..02d498c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ChunkHasher.java
@@ -0,0 +1,49 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+
+/** Computes the SHA-256 HMAC of a chunk of bytes. */
+public class ChunkHasher {
+    private static final String MAC_ALGORITHM = "HmacSHA256";
+
+    private final SecretKey mSecretKey;
+
+    /** Constructs a new hasher which computes the HMAC using the given secret key. */
+    public ChunkHasher(SecretKey secretKey) {
+        this.mSecretKey = secretKey;
+    }
+
+    /** Returns the SHA-256 over the given bytes. */
+    public ChunkHash computeHash(byte[] plaintext) throws InvalidKeyException {
+        try {
+            Mac mac = Mac.getInstance(MAC_ALGORITHM);
+            mac.init(mSecretKey);
+            return new ChunkHash(mac.doFinal(plaintext));
+        } catch (NoSuchAlgorithmException e) {
+            // This can not happen - AES/GCM/NoPadding is available as part of the framework.
+            throw new AssertionError(e);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ChunkHasherTest.java b/com/android/server/backup/encryption/chunking/ChunkHasherTest.java
new file mode 100644
index 0000000..72a927d
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ChunkHasherTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ChunkHasherTest {
+    private static final String KEY_ALGORITHM = "AES";
+    private static final String MAC_ALGORITHM = "HmacSHA256";
+
+    private static final byte[] TEST_KEY = {100, 120};
+    private static final byte[] TEST_DATA = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
+
+    private SecretKey mSecretKey;
+    private ChunkHasher mChunkHasher;
+
+    @Before
+    public void setUp() throws Exception {
+        mSecretKey = new SecretKeySpec(TEST_KEY, KEY_ALGORITHM);
+        mChunkHasher = new ChunkHasher(mSecretKey);
+    }
+
+    @Test
+    public void computeHash_returnsHmacForData() throws Exception {
+        ChunkHash chunkHash = mChunkHasher.computeHash(TEST_DATA);
+
+        byte[] hash = chunkHash.getHash();
+        Mac mac = Mac.getInstance(MAC_ALGORITHM);
+        mac.init(mSecretKey);
+        byte[] expectedHash = mac.doFinal(TEST_DATA);
+        assertThat(hash).isEqualTo(expectedHash);
+    }
+
+    @Test
+    public void computeHash_generates256BitHmac() throws Exception {
+        int expectedLength = 256 / Byte.SIZE;
+
+        byte[] hash = mChunkHasher.computeHash(TEST_DATA).getHash();
+
+        assertThat(hash).hasLength(expectedLength);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/Chunker.java b/com/android/server/backup/encryption/chunking/Chunker.java
new file mode 100644
index 0000000..c9a6293
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/Chunker.java
@@ -0,0 +1,46 @@
+/*
+ * 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.encryption.chunking;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+
+/** Splits an input stream into chunks, which are to be encrypted separately. */
+public interface Chunker {
+    /**
+     * Splits the input stream into chunks.
+     *
+     * @param inputStream The input stream.
+     * @param chunkConsumer A function that processes each chunk as it is produced.
+     * @throws IOException If there is a problem reading the input stream.
+     * @throws GeneralSecurityException if the consumer function throws an error.
+     */
+    void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
+            throws IOException, GeneralSecurityException;
+
+    /** Function that consumes chunks. */
+    interface ChunkConsumer {
+        /**
+         * Invoked for each chunk.
+         *
+         * @param chunk Plaintext bytes of chunk.
+         * @throws GeneralSecurityException if there is an issue encrypting the chunk.
+         */
+        void accept(byte[] chunk) throws GeneralSecurityException;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java b/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
new file mode 100644
index 0000000..ae2e150
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Writes plaintext chunks to a file, building a digest of the plaintext of the resulting file. */
+public class DecryptedChunkFileOutput implements DecryptedChunkOutput {
+    @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
+
+    private final File mOutputFile;
+    private final MessageDigest mMessageDigest;
+    @Nullable private FileOutputStream mFileOutputStream;
+    private boolean mClosed;
+    @Nullable private byte[] mDigest;
+
+    /**
+     * Constructs a new instance which writes chunks to the given file and uses the default message
+     * digest algorithm.
+     */
+    public DecryptedChunkFileOutput(File outputFile) {
+        mOutputFile = outputFile;
+        try {
+            mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError(
+                    "Impossible condition: JCE thinks it does not support AES.", e);
+        }
+    }
+
+    @Override
+    public DecryptedChunkOutput open() throws IOException {
+        checkState(mFileOutputStream == null, "Cannot open twice");
+        mFileOutputStream = new FileOutputStream(mOutputFile);
+        return this;
+    }
+
+    @Override
+    public void processChunk(byte[] plaintextBuffer, int length) throws IOException {
+        checkState(mFileOutputStream != null, "Must open before processing chunks");
+        mFileOutputStream.write(plaintextBuffer, /*off=*/ 0, length);
+        mMessageDigest.update(plaintextBuffer, /*offset=*/ 0, length);
+    }
+
+    @Override
+    public byte[] getDigest() {
+        checkState(mClosed, "Must close before getting mDigest");
+
+        // After the first call to mDigest() the MessageDigest is reset, thus we must store the
+        // result.
+        if (mDigest == null) {
+            mDigest = mMessageDigest.digest();
+        }
+        return mDigest;
+    }
+
+    @Override
+    public void close() throws IOException {
+        mFileOutputStream.close();
+        mClosed = true;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java b/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
new file mode 100644
index 0000000..823a63c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import com.google.common.io.Files;
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class DecryptedChunkFileOutputTest {
+    private static final byte[] TEST_CHUNK_1 = {1, 2, 3};
+    private static final byte[] TEST_CHUNK_2 = {4, 5, 6, 7, 8, 9, 10};
+    private static final int TEST_BUFFER_LENGTH =
+            Math.max(TEST_CHUNK_1.length, TEST_CHUNK_2.length);
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private File mOutputFile;
+    private DecryptedChunkFileOutput mDecryptedChunkFileOutput;
+
+    @Before
+    public void setUp() throws Exception {
+        mOutputFile = temporaryFolder.newFile();
+        mDecryptedChunkFileOutput = new DecryptedChunkFileOutput(mOutputFile);
+    }
+
+    @Test
+    public void open_returnsInstance() throws Exception {
+        DecryptedChunkOutput result = mDecryptedChunkFileOutput.open();
+        assertThat(result).isEqualTo(mDecryptedChunkFileOutput);
+    }
+
+    @Test
+    public void open_nonExistentOutputFolder_throwsException() throws Exception {
+        mDecryptedChunkFileOutput =
+                new DecryptedChunkFileOutput(
+                        new File(temporaryFolder.newFolder(), "mOutput/directory"));
+        assertThrows(FileNotFoundException.class, () -> mDecryptedChunkFileOutput.open());
+    }
+
+    @Test
+    public void open_whenRunTwice_throwsException() throws Exception {
+        mDecryptedChunkFileOutput.open();
+        assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.open());
+    }
+
+    @Test
+    public void processChunk_beforeOpen_throwsException() throws Exception {
+        assertThrows(IllegalStateException.class,
+                () -> mDecryptedChunkFileOutput.processChunk(new byte[0], 0));
+    }
+
+    @Test
+    public void processChunk_writesChunksToFile() throws Exception {
+        processTestChunks();
+
+        assertThat(Files.toByteArray(mOutputFile))
+                .isEqualTo(Bytes.concat(TEST_CHUNK_1, TEST_CHUNK_2));
+    }
+
+    @Test
+    public void getDigest_beforeClose_throws() throws Exception {
+        mDecryptedChunkFileOutput.open();
+        assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.getDigest());
+    }
+
+    @Test
+    public void getDigest_returnsCorrectDigest() throws Exception {
+        processTestChunks();
+
+        byte[] actualDigest = mDecryptedChunkFileOutput.getDigest();
+
+        MessageDigest expectedDigest =
+                MessageDigest.getInstance(DecryptedChunkFileOutput.DIGEST_ALGORITHM);
+        expectedDigest.update(TEST_CHUNK_1);
+        expectedDigest.update(TEST_CHUNK_2);
+        assertThat(actualDigest).isEqualTo(expectedDigest.digest());
+    }
+
+    @Test
+    public void getDigest_whenRunTwice_returnsIdenticalDigestBothTimes() throws Exception {
+        processTestChunks();
+
+        byte[] digest1 = mDecryptedChunkFileOutput.getDigest();
+        byte[] digest2 = mDecryptedChunkFileOutput.getDigest();
+
+        assertThat(digest1).isEqualTo(digest2);
+    }
+
+    private void processTestChunks() throws IOException {
+        mDecryptedChunkFileOutput.open();
+        mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_1, TEST_BUFFER_LENGTH),
+                TEST_CHUNK_1.length);
+        mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_2, TEST_BUFFER_LENGTH),
+                TEST_CHUNK_2.length);
+        mDecryptedChunkFileOutput.close();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java b/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java
new file mode 100644
index 0000000..69fb5cb
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/DiffScriptBackupWriter.java
@@ -0,0 +1,75 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Writes backup data to a diff script, using a {@link SingleStreamDiffScriptWriter}. */
+public class DiffScriptBackupWriter implements BackupWriter {
+    /**
+     * The maximum size of a chunk in the diff script. The diff script writer {@code mWriter} will
+     * buffer this many bytes in memory.
+     */
+    private static final int ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES = 1024 * 1024;
+
+    private final SingleStreamDiffScriptWriter mWriter;
+    private long mBytesWritten;
+
+    /**
+     * Constructs a new writer which writes the diff script to the given output stream, using the
+     * maximum new chunk size {@code ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES}.
+     */
+    public static DiffScriptBackupWriter newInstance(OutputStream outputStream) {
+        SingleStreamDiffScriptWriter writer =
+                new SingleStreamDiffScriptWriter(
+                        outputStream, ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES);
+        return new DiffScriptBackupWriter(writer);
+    }
+
+    @VisibleForTesting
+    DiffScriptBackupWriter(SingleStreamDiffScriptWriter writer) {
+        mWriter = writer;
+    }
+
+    @Override
+    public void writeBytes(byte[] bytes) throws IOException {
+        for (byte b : bytes) {
+            mWriter.writeByte(b);
+        }
+
+        mBytesWritten += bytes.length;
+    }
+
+    @Override
+    public void writeChunk(long start, int length) throws IOException {
+        mWriter.writeChunk(start, length);
+        mBytesWritten += length;
+    }
+
+    @Override
+    public long getBytesWritten() {
+        return mBytesWritten;
+    }
+
+    @Override
+    public void flush() throws IOException {
+        mWriter.flush();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java b/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java
new file mode 100644
index 0000000..2af6f2b
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/DiffScriptBackupWriterTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+
+/** Tests for {@link DiffScriptBackupWriter}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class DiffScriptBackupWriterTest {
+    private static final byte[] TEST_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+    @Captor private ArgumentCaptor<Byte> mBytesCaptor;
+    @Mock private SingleStreamDiffScriptWriter mDiffScriptWriter;
+    private BackupWriter mBackupWriter;
+
+    @Before
+    public void setUp() {
+        mDiffScriptWriter = mock(SingleStreamDiffScriptWriter.class);
+        mBackupWriter = new DiffScriptBackupWriter(mDiffScriptWriter);
+        mBytesCaptor = ArgumentCaptor.forClass(Byte.class);
+    }
+
+    @Test
+    public void writeBytes_writesBytesToWriter() throws Exception {
+        mBackupWriter.writeBytes(TEST_BYTES);
+
+        verify(mDiffScriptWriter, atLeastOnce()).writeByte(mBytesCaptor.capture());
+        assertThat(mBytesCaptor.getAllValues())
+                .containsExactlyElementsIn(Bytes.asList(TEST_BYTES))
+                .inOrder();
+    }
+
+    @Test
+    public void writeChunk_writesChunkToWriter() throws Exception {
+        mBackupWriter.writeChunk(0, 10);
+
+        verify(mDiffScriptWriter).writeChunk(0, 10);
+    }
+
+    @Test
+    public void getBytesWritten_returnsTotalSum() throws Exception {
+        mBackupWriter.writeBytes(TEST_BYTES);
+        mBackupWriter.writeBytes(TEST_BYTES);
+        mBackupWriter.writeChunk(/*start=*/ 0, /*length=*/ 10);
+
+        long bytesWritten = mBackupWriter.getBytesWritten();
+
+        assertThat(bytesWritten).isEqualTo(2 * TEST_BYTES.length + 10);
+    }
+
+    @Test
+    public void flush_flushesWriter() throws IOException {
+        mBackupWriter.flush();
+
+        verify(mDiffScriptWriter).flush();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/DiffScriptWriter.java b/com/android/server/backup/encryption/chunking/DiffScriptWriter.java
new file mode 100644
index 0000000..49d1571
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/DiffScriptWriter.java
@@ -0,0 +1,36 @@
+/*
+ * 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.encryption.chunking;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Writer that formats a Diff Script and writes it to an output source. */
+interface DiffScriptWriter {
+    /** Adds a new byte to the diff script. */
+    void writeByte(byte b) throws IOException;
+
+    /** Adds a known chunk to the diff script. */
+    void writeChunk(long chunkStart, int chunkLength) throws IOException;
+
+    /** Indicates that no more bytes or chunks will be added to the diff script. */
+    void flush() throws IOException;
+
+    interface Factory {
+        DiffScriptWriter create(OutputStream outputStream);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/EncryptedChunk.java b/com/android/server/backup/encryption/chunking/EncryptedChunk.java
new file mode 100644
index 0000000..cde59fa
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/EncryptedChunk.java
@@ -0,0 +1,92 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A chunk of a file encrypted using AES/GCM.
+ *
+ * <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
+ * encryptedBytes(), key() and nonce().
+ */
+public class EncryptedChunk {
+    public static final int KEY_LENGTH_BYTES = ChunkHash.HASH_LENGTH_BYTES;
+    public static final int NONCE_LENGTH_BYTES = 12;
+
+    /**
+     * Constructs a new instance with the given key, nonce, and encrypted bytes.
+     *
+     * @param key SHA-256 Hmac of the chunk plaintext.
+     * @param nonce Nonce with which the bytes of the chunk were encrypted.
+     * @param encryptedBytes Encrypted bytes of the chunk.
+     */
+    public static EncryptedChunk create(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
+        Preconditions.checkArgument(
+                nonce.length == NONCE_LENGTH_BYTES, "Nonce does not have the correct length.");
+        return new EncryptedChunk(key, nonce, encryptedBytes);
+    }
+
+    private ChunkHash mKey;
+    private byte[] mNonce;
+    private byte[] mEncryptedBytes;
+
+    private EncryptedChunk(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
+        mKey = key;
+        mNonce = nonce;
+        mEncryptedBytes = encryptedBytes;
+    }
+
+    /** The SHA-256 Hmac of the plaintext bytes of the chunk. */
+    public ChunkHash key() {
+        return mKey;
+    }
+
+    /** The nonce with which the chunk was encrypted. */
+    public byte[] nonce() {
+        return mNonce;
+    }
+
+    /** The encrypted bytes of the chunk. */
+    public byte[] encryptedBytes() {
+        return mEncryptedBytes;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof EncryptedChunk)) {
+            return false;
+        }
+
+        EncryptedChunk encryptedChunkOrdering = (EncryptedChunk) o;
+        return Arrays.equals(mEncryptedBytes, encryptedChunkOrdering.mEncryptedBytes)
+                && Arrays.equals(mNonce, encryptedChunkOrdering.mNonce)
+                && mKey.equals(encryptedChunkOrdering.mKey);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey, Arrays.hashCode(mNonce), Arrays.hashCode(mEncryptedBytes));
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java b/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java
new file mode 100644
index 0000000..16beda3
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/EncryptedChunkEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.server.backup.encryption.chunk.ChunkOrderingType;
+
+import java.io.IOException;
+
+/** Encodes an {@link EncryptedChunk} as bytes to write to the encrypted backup file. */
+public interface EncryptedChunkEncoder {
+    /**
+     * Encodes the given chunk and asks the writer to write it.
+     *
+     * <p>The chunk will be encoded in the format [nonce]+[encrypted data].
+     *
+     * <p>TODO(b/116575321): Choose a more descriptive method name after the code move is done.
+     */
+    void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException;
+
+    /**
+     * Returns the length in bytes that this chunk would be if encoded with {@link
+     * #writeChunkToWriter}.
+     */
+    int getEncodedLengthOfChunk(EncryptedChunk chunk);
+
+    /**
+     * Returns the {@link ChunkOrderingType} that must be included in the backup file, when using
+     * this decoder, so that the file may be correctly decoded.
+     */
+    @ChunkOrderingType
+    int getChunkOrderingType();
+}
diff --git a/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java b/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java
new file mode 100644
index 0000000..325b601
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/EncryptedChunkTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class EncryptedChunkTest {
+    private static final byte[] CHUNK_HASH_1_BYTES =
+            Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES);
+    private static final byte[] NONCE_1 =
+            Arrays.copyOf(new byte[] {2}, EncryptedChunk.NONCE_LENGTH_BYTES);
+    private static final byte[] ENCRYPTED_BYTES_1 =
+            Arrays.copyOf(new byte[] {3}, EncryptedChunk.KEY_LENGTH_BYTES);
+
+    private static final byte[] CHUNK_HASH_2_BYTES =
+            Arrays.copyOf(new byte[] {4}, ChunkHash.HASH_LENGTH_BYTES);
+    private static final byte[] NONCE_2 =
+            Arrays.copyOf(new byte[] {5}, EncryptedChunk.NONCE_LENGTH_BYTES);
+    private static final byte[] ENCRYPTED_BYTES_2 =
+            Arrays.copyOf(new byte[] {6}, EncryptedChunk.KEY_LENGTH_BYTES);
+
+    @Test
+    public void testCreate_withIncorrectLength_throwsException() {
+        ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
+        byte[] shortNonce = Arrays.copyOf(new byte[] {2}, EncryptedChunk.NONCE_LENGTH_BYTES - 1);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> EncryptedChunk.create(chunkHash, shortNonce, ENCRYPTED_BYTES_1));
+    }
+
+    @Test
+    public void testEncryptedBytes_forNewlyCreatedObject_returnsCorrectValue() {
+        ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
+        EncryptedChunk encryptedChunk =
+                EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
+
+        byte[] returnedBytes = encryptedChunk.encryptedBytes();
+
+        assertThat(returnedBytes)
+                .asList()
+                .containsExactlyElementsIn(Bytes.asList(ENCRYPTED_BYTES_1))
+                .inOrder();
+    }
+
+    @Test
+    public void testKey_forNewlyCreatedObject_returnsCorrectValue() {
+        ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
+        EncryptedChunk encryptedChunk =
+                EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
+
+        ChunkHash returnedKey = encryptedChunk.key();
+
+        assertThat(returnedKey).isEqualTo(chunkHash);
+    }
+
+    @Test
+    public void testNonce_forNewlycreatedObject_returnCorrectValue() {
+        ChunkHash chunkHash = new ChunkHash(CHUNK_HASH_1_BYTES);
+        EncryptedChunk encryptedChunk =
+                EncryptedChunk.create(chunkHash, NONCE_1, ENCRYPTED_BYTES_1);
+
+        byte[] returnedNonce = encryptedChunk.nonce();
+
+        assertThat(returnedNonce).asList().containsExactlyElementsIn(Bytes.asList(NONCE_1));
+    }
+
+    @Test
+    public void testEquals() {
+        ChunkHash chunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
+        ChunkHash equalChunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
+        ChunkHash chunkHash2 = new ChunkHash(CHUNK_HASH_2_BYTES);
+        EncryptedChunk encryptedChunk1 =
+                EncryptedChunk.create(chunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
+        EncryptedChunk equalEncryptedChunk1 =
+                EncryptedChunk.create(equalChunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
+        EncryptedChunk encryptedChunk2 =
+                EncryptedChunk.create(chunkHash2, NONCE_2, ENCRYPTED_BYTES_2);
+
+        assertThat(encryptedChunk1).isEqualTo(equalEncryptedChunk1);
+        assertThat(encryptedChunk1).isNotEqualTo(encryptedChunk2);
+    }
+
+    @Test
+    public void testHashCode() {
+        ChunkHash chunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
+        ChunkHash equalChunkHash1 = new ChunkHash(CHUNK_HASH_1_BYTES);
+        ChunkHash chunkHash2 = new ChunkHash(CHUNK_HASH_2_BYTES);
+        EncryptedChunk encryptedChunk1 =
+                EncryptedChunk.create(chunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
+        EncryptedChunk equalEncryptedChunk1 =
+                EncryptedChunk.create(equalChunkHash1, NONCE_1, ENCRYPTED_BYTES_1);
+        EncryptedChunk encryptedChunk2 =
+                EncryptedChunk.create(chunkHash2, NONCE_2, ENCRYPTED_BYTES_2);
+
+        int hash1 = encryptedChunk1.hashCode();
+        int equalHash1 = equalEncryptedChunk1.hashCode();
+        int hash2 = encryptedChunk2.hashCode();
+
+        assertThat(hash1).isEqualTo(equalHash1);
+        assertThat(hash1).isNotEqualTo(hash2);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java b/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java
new file mode 100644
index 0000000..6b9be9f
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoder.java
@@ -0,0 +1,69 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.server.backup.encryption.chunk.ChunkOrderingType;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import java.io.IOException;
+
+/**
+ * Encodes an {@link EncryptedChunk} as bytes, prepending the length of the chunk.
+ *
+ * <p>This allows us to decode the backup file during restore without any extra information about
+ * the boundaries of the chunks. The backup file should contain a chunk ordering in mode {@link
+ * ChunksMetadataProto#INLINE_LENGTHS}.
+ *
+ * <p>We use this implementation during key value backup.
+ */
+public class InlineLengthsEncryptedChunkEncoder implements EncryptedChunkEncoder {
+    public static final int BYTES_LENGTH = Integer.SIZE / Byte.SIZE;
+
+    private final LengthlessEncryptedChunkEncoder mLengthlessEncryptedChunkEncoder =
+            new LengthlessEncryptedChunkEncoder();
+
+    @Override
+    public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
+        int length = mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
+        writer.writeBytes(toByteArray(length));
+        mLengthlessEncryptedChunkEncoder.writeChunkToWriter(writer, chunk);
+    }
+
+    @Override
+    public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
+        return BYTES_LENGTH + mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
+    }
+
+    @Override
+    @ChunkOrderingType
+    public int getChunkOrderingType() {
+        return ChunksMetadataProto.INLINE_LENGTHS;
+    }
+
+    /**
+     * Returns a big-endian representation of {@code value} in a 4-element byte array; equivalent to
+     * {@code ByteBuffer.allocate(4).putInt(value).array()}. For example, the input value {@code
+     * 0x12131415} would yield the byte array {@code {0x12, 0x13, 0x14, 0x15}}.
+     *
+     * <p>Equivalent to guava's Ints.toByteArray.
+     */
+    static byte[] toByteArray(int value) {
+        return new byte[] {
+            (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value
+        };
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java b/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java
new file mode 100644
index 0000000..7e1fded
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/InlineLengthsEncryptedChunkEncoderTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class InlineLengthsEncryptedChunkEncoderTest {
+
+    private static final byte[] TEST_NONCE =
+            Arrays.copyOf(new byte[] {1}, EncryptedChunk.NONCE_LENGTH_BYTES);
+    private static final byte[] TEST_KEY_DATA =
+            Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES);
+    private static final byte[] TEST_DATA = {5, 4, 5, 7, 10, 12, 1, 2, 9};
+
+    @Mock private BackupWriter mMockBackupWriter;
+    private ChunkHash mTestKey;
+    private EncryptedChunk mTestChunk;
+    private EncryptedChunkEncoder mEncoder;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockBackupWriter = mock(BackupWriter.class);
+        mTestKey = new ChunkHash(TEST_KEY_DATA);
+        mTestChunk = EncryptedChunk.create(mTestKey, TEST_NONCE, TEST_DATA);
+        mEncoder = new InlineLengthsEncryptedChunkEncoder();
+    }
+
+    @Test
+    public void writeChunkToWriter_writesLengthThenNonceThenData() throws Exception {
+        mEncoder.writeChunkToWriter(mMockBackupWriter, mTestChunk);
+
+        InOrder inOrder = inOrder(mMockBackupWriter);
+        inOrder.verify(mMockBackupWriter)
+                .writeBytes(
+                        InlineLengthsEncryptedChunkEncoder.toByteArray(
+                                TEST_NONCE.length + TEST_DATA.length));
+        inOrder.verify(mMockBackupWriter).writeBytes(TEST_NONCE);
+        inOrder.verify(mMockBackupWriter).writeBytes(TEST_DATA);
+    }
+
+    @Test
+    public void getEncodedLengthOfChunk_returnsSumOfNonceAndDataLengths() {
+        int encodedLength = mEncoder.getEncodedLengthOfChunk(mTestChunk);
+
+        assertThat(encodedLength).isEqualTo(Integer.BYTES + TEST_NONCE.length + TEST_DATA.length);
+    }
+
+    @Test
+    public void getChunkOrderingType_returnsExplicitStartsType() {
+        assertThat(mEncoder.getChunkOrderingType()).isEqualTo(ChunksMetadataProto.INLINE_LENGTHS);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java b/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java
new file mode 100644
index 0000000..e707350
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoder.java
@@ -0,0 +1,52 @@
+/*
+ * 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.encryption.chunking;
+
+import com.android.server.backup.encryption.chunk.ChunkOrderingType;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import java.io.IOException;
+
+/**
+ * Encodes an {@link EncryptedChunk} as bytes without including any information about the length of
+ * the chunk.
+ *
+ * <p>In order for us to decode the backup file during restore it must include a chunk ordering in
+ * mode {@link ChunksMetadataProto#EXPLICIT_STARTS}, which contains the boundaries of the chunks in
+ * the encrypted file. This information allows us to decode the backup file and divide it into
+ * chunks without including the length of each chunk inline.
+ *
+ * <p>We use this implementation during full backup.
+ */
+public class LengthlessEncryptedChunkEncoder implements EncryptedChunkEncoder {
+    @Override
+    public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
+        writer.writeBytes(chunk.nonce());
+        writer.writeBytes(chunk.encryptedBytes());
+    }
+
+    @Override
+    public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
+        return chunk.nonce().length + chunk.encryptedBytes().length;
+    }
+
+    @Override
+    @ChunkOrderingType
+    public int getChunkOrderingType() {
+        return ChunksMetadataProto.EXPLICIT_STARTS;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java b/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java
new file mode 100644
index 0000000..6f58ee1
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/LengthlessEncryptedChunkEncoderTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class LengthlessEncryptedChunkEncoderTest {
+    private static final byte[] TEST_NONCE =
+            Arrays.copyOf(new byte[] {1}, EncryptedChunk.NONCE_LENGTH_BYTES);
+    private static final byte[] TEST_KEY_DATA =
+            Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES);
+    private static final byte[] TEST_DATA = {5, 4, 5, 7, 10, 12, 1, 2, 9};
+
+    @Mock private BackupWriter mMockBackupWriter;
+    private ChunkHash mTestKey;
+    private EncryptedChunk mTestChunk;
+    private EncryptedChunkEncoder mEncoder;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockBackupWriter = mock(BackupWriter.class);
+        mTestKey = new ChunkHash(TEST_KEY_DATA);
+        mTestChunk = EncryptedChunk.create(mTestKey, TEST_NONCE, TEST_DATA);
+        mEncoder = new LengthlessEncryptedChunkEncoder();
+    }
+
+    @Test
+    public void writeChunkToWriter_writesNonceThenData() throws Exception {
+        mEncoder.writeChunkToWriter(mMockBackupWriter, mTestChunk);
+
+        InOrder inOrder = inOrder(mMockBackupWriter);
+        inOrder.verify(mMockBackupWriter).writeBytes(TEST_NONCE);
+        inOrder.verify(mMockBackupWriter).writeBytes(TEST_DATA);
+    }
+
+    @Test
+    public void getEncodedLengthOfChunk_returnsSumOfNonceAndDataLengths() {
+        int encodedLength = mEncoder.getEncodedLengthOfChunk(mTestChunk);
+
+        assertThat(encodedLength).isEqualTo(TEST_NONCE.length + TEST_DATA.length);
+    }
+
+    @Test
+    public void getChunkOrderingType_returnsExplicitStartsType() {
+        assertThat(mEncoder.getChunkOrderingType()).isEqualTo(ChunksMetadataProto.EXPLICIT_STARTS);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java b/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java
new file mode 100644
index 0000000..4aea601
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/OutputStreamWrapper.java
@@ -0,0 +1,25 @@
+/*
+ * 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.encryption.chunking;
+
+import java.io.OutputStream;
+
+/** An interface that wraps one {@link OutputStream} with another for filtration purposes. */
+public interface OutputStreamWrapper {
+    /** Wraps a given {@link OutputStream}. */
+    OutputStream wrap(OutputStream outputStream);
+}
diff --git a/com/android/server/backup/encryption/chunking/ProtoStore.java b/com/android/server/backup/encryption/chunking/ProtoStore.java
new file mode 100644
index 0000000..b0a562c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ProtoStore.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Stores a nano proto for each package, persisting the proto to disk.
+ *
+ * <p>This is used to store {@link ChunksMetadataProto.ChunkListing}.
+ *
+ * @param <T> the type of nano proto to store.
+ */
+public class ProtoStore<T extends MessageNano> {
+    private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings";
+    private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings";
+
+    private static final String TAG = "BupEncProtoStore";
+
+    private final File mStoreFolder;
+    private final Class<T> mClazz;
+
+    /** Creates a new instance which stores chunk listings at the default location. */
+    public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore(
+            Context context) throws IOException {
+        return new ProtoStore<>(
+                ChunksMetadataProto.ChunkListing.class,
+                new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER));
+    }
+
+    /** Creates a new instance which stores key value listings in the default location. */
+    public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore(
+            Context context) throws IOException {
+        return new ProtoStore<>(
+                KeyValueListingProto.KeyValueListing.class,
+                new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER));
+    }
+
+    /**
+     * Creates a new instance which stores protos in the given folder.
+     *
+     * @param storeFolder The location where the serialized form is stored.
+     */
+    @VisibleForTesting
+    ProtoStore(Class<T> clazz, File storeFolder) throws IOException {
+        mClazz = Objects.requireNonNull(clazz);
+        mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder);
+    }
+
+    private static File ensureDirectoryExistsOrThrow(File directory) throws IOException {
+        if (directory.exists() && !directory.isDirectory()) {
+            throw new IOException("Store folder already exists, but isn't a directory.");
+        }
+
+        if (!directory.exists() && !directory.mkdir()) {
+            throw new IOException("Unable to create store folder.");
+        }
+
+        return directory;
+    }
+
+    /**
+     * Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing
+     * exists.
+     */
+    public Optional<T> loadProto(String packageName)
+            throws IOException, IllegalAccessException, InstantiationException,
+            NoSuchMethodException, InvocationTargetException {
+        File file = getFileForPackage(packageName);
+
+        if (!file.exists()) {
+            Slog.d(
+                    TAG,
+                    "No chunk listing existed for " + packageName + ", returning empty listing.");
+            return Optional.empty();
+        }
+
+        AtomicFile protoStore = new AtomicFile(file);
+        byte[] data = protoStore.readFully();
+
+        Constructor<T> constructor = mClazz.getDeclaredConstructor();
+        T proto = constructor.newInstance();
+        MessageNano.mergeFrom(proto, data);
+        return Optional.of(proto);
+    }
+
+    /** Saves a proto to disk, associating it with the given package. */
+    public void saveProto(String packageName, T proto) throws IOException {
+        Objects.requireNonNull(proto);
+        File file = getFileForPackage(packageName);
+
+        try (FileOutputStream os = new FileOutputStream(file)) {
+            os.write(MessageNano.toByteArray(proto));
+        } catch (IOException e) {
+            Slog.e(
+                    TAG,
+                    "Exception occurred when saving the listing for "
+                            + packageName
+                            + ", deleting saved listing.",
+                    e);
+
+            // If a problem occurred when writing the listing then it might be corrupt, so delete
+            // it.
+            file.delete();
+
+            throw e;
+        }
+    }
+
+    /** Deletes the proto for the given package, or does nothing if the package has no proto. */
+    public void deleteProto(String packageName) {
+        File file = getFileForPackage(packageName);
+        file.delete();
+    }
+
+    /** Deletes every proto of this type, for all package names. */
+    public void deleteAllProtos() {
+        File[] files = mStoreFolder.listFiles();
+
+        // We ensure that the storeFolder exists in the constructor, but check just in case it has
+        // mysteriously disappeared.
+        if (files == null) {
+            return;
+        }
+
+        for (File file : files) {
+            file.delete();
+        }
+    }
+
+    private File getFileForPackage(String packageName) {
+        checkPackageName(packageName);
+        return new File(mStoreFolder, packageName);
+    }
+
+    private static void checkPackageName(String packageName) {
+        if (TextUtils.isEmpty(packageName) || packageName.contains("/")) {
+            throw new IllegalArgumentException(
+                    "Package name must not contain '/' or be empty: " + packageName);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/ProtoStoreTest.java b/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
new file mode 100644
index 0000000..d73c8e4
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2019 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ProtoStoreTest {
+    private static final String TEST_KEY_1 = "test_key_1";
+    private static final ChunkHash TEST_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
+    private static final int TEST_LENGTH_1 = 10;
+    private static final int TEST_LENGTH_2 = 18;
+
+    private static final String TEST_PACKAGE_1 = "com.example.test1";
+    private static final String TEST_PACKAGE_2 = "com.example.test2";
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private File mStoreFolder;
+    private ProtoStore<ChunksMetadataProto.ChunkListing> mProtoStore;
+
+    @Before
+    public void setUp() throws Exception {
+        mStoreFolder = mTemporaryFolder.newFolder();
+        mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
+    }
+
+    @Test
+    public void differentStoreTypes_operateSimultaneouslyWithoutInterfering() throws Exception {
+        ChunksMetadataProto.ChunkListing chunkListing =
+                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+        KeyValueListingProto.KeyValueListing keyValueListing =
+                new KeyValueListingProto.KeyValueListing();
+        keyValueListing.entries = new KeyValueListingProto.KeyValueEntry[1];
+        keyValueListing.entries[0] = new KeyValueListingProto.KeyValueEntry();
+        keyValueListing.entries[0].key = TEST_KEY_1;
+        keyValueListing.entries[0].hash = TEST_HASH_1.getHash();
+
+        Context application = ApplicationProvider.getApplicationContext();
+        ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore =
+                ProtoStore.createChunkListingStore(application);
+        ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore =
+                ProtoStore.createKeyValueListingStore(application);
+
+        chunkListingStore.saveProto(TEST_PACKAGE_1, chunkListing);
+        keyValueListingStore.saveProto(TEST_PACKAGE_1, keyValueListing);
+
+        ChunksMetadataProto.ChunkListing actualChunkListing =
+                chunkListingStore.loadProto(TEST_PACKAGE_1).get();
+        KeyValueListingProto.KeyValueListing actualKeyValueListing =
+                keyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+        assertListingsEqual(actualChunkListing, chunkListing);
+        assertThat(actualKeyValueListing.entries.length).isEqualTo(1);
+        assertThat(actualKeyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
+        assertThat(actualKeyValueListing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+    }
+
+    @Test
+    public void construct_storeLocationIsFile_throws() throws Exception {
+        assertThrows(
+                IOException.class,
+                () ->
+                        new ProtoStore<>(
+                                ChunksMetadataProto.ChunkListing.class,
+                                mTemporaryFolder.newFile()));
+    }
+
+    @Test
+    public void loadChunkListing_noListingExists_returnsEmptyListing() throws Exception {
+        Optional<ChunksMetadataProto.ChunkListing> chunkListing =
+                mProtoStore.loadProto(TEST_PACKAGE_1);
+        assertThat(chunkListing.isPresent()).isFalse();
+    }
+
+    @Test
+    public void loadChunkListing_listingExists_returnsExistingListing() throws Exception {
+        ChunksMetadataProto.ChunkListing expected =
+                createChunkListing(
+                        ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
+        mProtoStore.saveProto(TEST_PACKAGE_1, expected);
+
+        ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
+
+        assertListingsEqual(result, expected);
+    }
+
+    @Test
+    public void loadProto_emptyPackageName_throwsException() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(""));
+    }
+
+    @Test
+    public void loadProto_nullPackageName_throwsException() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(null));
+    }
+
+    @Test
+    public void loadProto_packageNameContainsSlash_throwsException() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class, () -> mProtoStore.loadProto(TEST_PACKAGE_1 + "/"));
+    }
+
+    @Test
+    public void saveProto_persistsToNewInstance() throws Exception {
+        ChunksMetadataProto.ChunkListing expected =
+                createChunkListing(
+                        ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
+        mProtoStore.saveProto(TEST_PACKAGE_1, expected);
+        mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
+
+        ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
+
+        assertListingsEqual(result, expected);
+    }
+
+    @Test
+    public void saveProto_emptyPackageName_throwsException() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mProtoStore.saveProto("", new ChunksMetadataProto.ChunkListing()));
+    }
+
+    @Test
+    public void saveProto_nullPackageName_throwsException() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mProtoStore.saveProto(null, new ChunksMetadataProto.ChunkListing()));
+    }
+
+    @Test
+    public void saveProto_packageNameContainsSlash_throwsException() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mProtoStore.saveProto(
+                                TEST_PACKAGE_1 + "/", new ChunksMetadataProto.ChunkListing()));
+    }
+
+    @Test
+    public void saveProto_nullListing_throwsException() throws Exception {
+        assertThrows(NullPointerException.class, () -> mProtoStore.saveProto(TEST_PACKAGE_1, null));
+    }
+
+    @Test
+    public void deleteProto_noListingExists_doesNothing() throws Exception {
+        ChunksMetadataProto.ChunkListing listing =
+                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+        mProtoStore.saveProto(TEST_PACKAGE_1, listing);
+
+        mProtoStore.deleteProto(TEST_PACKAGE_2);
+
+        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).get().chunks.length).isEqualTo(1);
+    }
+
+    @Test
+    public void deleteProto_listingExists_deletesListing() throws Exception {
+        ChunksMetadataProto.ChunkListing listing =
+                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+        mProtoStore.saveProto(TEST_PACKAGE_1, listing);
+
+        mProtoStore.deleteProto(TEST_PACKAGE_1);
+
+        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
+    }
+
+    @Test
+    public void deleteAllProtos_deletesAllProtos() throws Exception {
+        ChunksMetadataProto.ChunkListing listing1 =
+                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
+        ChunksMetadataProto.ChunkListing listing2 =
+                createChunkListing(ImmutableMap.of(TEST_HASH_2, TEST_LENGTH_2));
+        mProtoStore.saveProto(TEST_PACKAGE_1, listing1);
+        mProtoStore.saveProto(TEST_PACKAGE_2, listing2);
+
+        mProtoStore.deleteAllProtos();
+
+        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
+        assertThat(mProtoStore.loadProto(TEST_PACKAGE_2).isPresent()).isFalse();
+    }
+
+    @Test
+    public void deleteAllProtos_folderDeleted_doesNotCrash() throws Exception {
+        mStoreFolder.delete();
+
+        mProtoStore.deleteAllProtos();
+    }
+
+    private static ChunksMetadataProto.ChunkListing createChunkListing(
+            ImmutableMap<ChunkHash, Integer> chunks) {
+        ChunksMetadataProto.ChunkListing listing = new ChunksMetadataProto.ChunkListing();
+        listing.cipherType = ChunksMetadataProto.AES_256_GCM;
+        listing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+
+        List<ChunksMetadataProto.Chunk> chunkProtos = new ArrayList<>();
+        for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
+            ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
+            chunk.hash = entry.getKey().getHash();
+            chunk.length = entry.getValue();
+            chunkProtos.add(chunk);
+        }
+        listing.chunks = chunkProtos.toArray(new ChunksMetadataProto.Chunk[0]);
+        return listing;
+    }
+
+    private void assertListingsEqual(
+            ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
+        assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
+        for (int i = 0; i < result.chunks.length; i++) {
+            assertWithMessage("Chunk " + i)
+                    .that(result.chunks[i].length)
+                    .isEqualTo(expected.chunks[i].length);
+            assertWithMessage("Chunk " + i)
+                    .that(result.chunks[i].hash)
+                    .isEqualTo(expected.chunks[i].hash);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/RawBackupWriter.java b/com/android/server/backup/encryption/chunking/RawBackupWriter.java
new file mode 100644
index 0000000..b211b0f
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/RawBackupWriter.java
@@ -0,0 +1,52 @@
+/*
+ * 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.encryption.chunking;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Writes data straight to an output stream. */
+public class RawBackupWriter implements BackupWriter {
+    private final OutputStream mOutputStream;
+    private long mBytesWritten;
+
+    /** Constructs a new writer which writes bytes to the given output stream. */
+    public RawBackupWriter(OutputStream outputStream) {
+        this.mOutputStream = outputStream;
+    }
+
+    @Override
+    public void writeBytes(byte[] bytes) throws IOException {
+        mOutputStream.write(bytes);
+        mBytesWritten += bytes.length;
+    }
+
+    @Override
+    public void writeChunk(long start, int length) throws IOException {
+        throw new UnsupportedOperationException("RawBackupWriter cannot write existing chunks");
+    }
+
+    @Override
+    public long getBytesWritten() {
+        return mBytesWritten;
+    }
+
+    @Override
+    public void flush() throws IOException {
+        mOutputStream.flush();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/RawBackupWriterTest.java b/com/android/server/backup/encryption/chunking/RawBackupWriterTest.java
new file mode 100644
index 0000000..966d3e2
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/RawBackupWriterTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayOutputStream;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class RawBackupWriterTest {
+    private static final byte[] TEST_BYTES = {1, 2, 3, 4, 5, 6};
+
+    private BackupWriter mWriter;
+    private ByteArrayOutputStream mOutput;
+
+    @Before
+    public void setUp() {
+        mOutput = new ByteArrayOutputStream();
+        mWriter = new RawBackupWriter(mOutput);
+    }
+
+    @Test
+    public void writeBytes_writesToOutputStream() throws Exception {
+        mWriter.writeBytes(TEST_BYTES);
+
+        assertThat(mOutput.toByteArray())
+                .asList()
+                .containsExactlyElementsIn(Bytes.asList(TEST_BYTES))
+                .inOrder();
+    }
+
+    @Test
+    public void writeChunk_throwsUnsupportedOperationException() throws Exception {
+        assertThrows(UnsupportedOperationException.class, () -> mWriter.writeChunk(0, 0));
+    }
+
+    @Test
+    public void getBytesWritten_returnsTotalSum() throws Exception {
+        mWriter.writeBytes(TEST_BYTES);
+        mWriter.writeBytes(TEST_BYTES);
+
+        long bytesWritten = mWriter.getBytesWritten();
+
+        assertThat(bytesWritten).isEqualTo(2 * TEST_BYTES.length);
+    }
+
+    @Test
+    public void flush_flushesOutputStream() throws Exception {
+        mOutput = mock(ByteArrayOutputStream.class);
+        mWriter = new RawBackupWriter(mOutput);
+
+        mWriter.flush();
+
+        verify(mOutput).flush();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java b/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java
new file mode 100644
index 0000000..0e4bd58
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriter.java
@@ -0,0 +1,130 @@
+/*
+ * 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.encryption.chunking;
+
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+/**
+ * A {@link DiffScriptWriter} that writes an entire diff script to a single {@link OutputStream}.
+ */
+public class SingleStreamDiffScriptWriter implements DiffScriptWriter {
+    static final byte LINE_SEPARATOR = 0xA;
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+    private final int mMaxNewByteChunkSize;
+    private final OutputStream mOutputStream;
+    private final byte[] mByteBuffer;
+    private int mBufferSize = 0;
+    // Each chunk could be written immediately to the output stream. However,
+    // it is possible that chunks may overlap. We therefore cache the most recent
+    // reusable chunk and try to merge it with future chunks.
+    private ByteRange mReusableChunk;
+
+    public SingleStreamDiffScriptWriter(OutputStream outputStream, int maxNewByteChunkSize) {
+        mOutputStream = outputStream;
+        mMaxNewByteChunkSize = maxNewByteChunkSize;
+        mByteBuffer = new byte[maxNewByteChunkSize];
+    }
+
+    @Override
+    public void writeByte(byte b) throws IOException {
+        if (mReusableChunk != null) {
+            writeReusableChunk();
+        }
+        mByteBuffer[mBufferSize++] = b;
+        if (mBufferSize == mMaxNewByteChunkSize) {
+            writeByteBuffer();
+        }
+    }
+
+    @Override
+    public void writeChunk(long chunkStart, int chunkLength) throws IOException {
+        Preconditions.checkArgument(chunkStart >= 0);
+        Preconditions.checkArgument(chunkLength > 0);
+        if (mBufferSize != 0) {
+            writeByteBuffer();
+        }
+
+        if (mReusableChunk != null && mReusableChunk.getEnd() + 1 == chunkStart) {
+            // The new chunk overlaps the old, so combine them into a single byte range.
+            mReusableChunk = mReusableChunk.extend(chunkLength);
+        } else {
+            writeReusableChunk();
+            mReusableChunk = new ByteRange(chunkStart, chunkStart + chunkLength - 1);
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        Preconditions.checkState(!(mBufferSize != 0 && mReusableChunk != null));
+        if (mBufferSize != 0) {
+            writeByteBuffer();
+        }
+        if (mReusableChunk != null) {
+            writeReusableChunk();
+        }
+        mOutputStream.flush();
+    }
+
+    private void writeByteBuffer() throws IOException {
+        mOutputStream.write(Integer.toString(mBufferSize).getBytes(UTF_8));
+        mOutputStream.write(LINE_SEPARATOR);
+        mOutputStream.write(mByteBuffer, 0, mBufferSize);
+        mOutputStream.write(LINE_SEPARATOR);
+        mBufferSize = 0;
+    }
+
+    private void writeReusableChunk() throws IOException {
+        if (mReusableChunk != null) {
+            mOutputStream.write(
+                    String.format(
+                                    Locale.US,
+                                    "%d-%d",
+                                    mReusableChunk.getStart(),
+                                    mReusableChunk.getEnd())
+                            .getBytes(UTF_8));
+            mOutputStream.write(LINE_SEPARATOR);
+            mReusableChunk = null;
+        }
+    }
+
+    /** A factory that creates {@link SingleStreamDiffScriptWriter}s. */
+    public static class Factory implements DiffScriptWriter.Factory {
+        private final int mMaxNewByteChunkSize;
+        private final OutputStreamWrapper mOutputStreamWrapper;
+
+        public Factory(int maxNewByteChunkSize, @Nullable OutputStreamWrapper outputStreamWrapper) {
+            mMaxNewByteChunkSize = maxNewByteChunkSize;
+            mOutputStreamWrapper = outputStreamWrapper;
+        }
+
+        @Override
+        public SingleStreamDiffScriptWriter create(OutputStream outputStream) {
+            if (mOutputStreamWrapper != null) {
+                outputStream = mOutputStreamWrapper.wrap(outputStream);
+            }
+            return new SingleStreamDiffScriptWriter(outputStream, mMaxNewByteChunkSize);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java b/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java
new file mode 100644
index 0000000..73baf80
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/SingleStreamDiffScriptWriterTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Locale;
+
+/** Tests for {@link SingleStreamDiffScriptWriter}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class SingleStreamDiffScriptWriterTest {
+    private static final int MAX_CHUNK_SIZE_IN_BYTES = 256;
+    /** By default this Locale does not use Arabic numbers for %d formatting. */
+    private static final Locale HINDI = new Locale("hi", "IN");
+
+    private Locale mDefaultLocale;
+    private ByteArrayOutputStream mOutputStream;
+    private SingleStreamDiffScriptWriter mDiffScriptWriter;
+
+    @Before
+    public void setUp() {
+        mDefaultLocale = Locale.getDefault();
+        mOutputStream = new ByteArrayOutputStream();
+        mDiffScriptWriter =
+                new SingleStreamDiffScriptWriter(mOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
+    }
+
+    @After
+    public void tearDown() {
+        Locale.setDefault(mDefaultLocale);
+    }
+
+    @Test
+    public void writeChunk_withNegativeStart_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mDiffScriptWriter.writeChunk(-1, 50));
+    }
+
+    @Test
+    public void writeChunk_withZeroLength_throwsException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mDiffScriptWriter.writeChunk(0, 0));
+    }
+
+    @Test
+    public void writeChunk_withExistingBytesInBuffer_writesBufferFirst()
+            throws IOException {
+        String testString = "abcd";
+        writeStringAsBytesToWriter(testString, mDiffScriptWriter);
+
+        mDiffScriptWriter.writeChunk(0, 20);
+        mDiffScriptWriter.flush();
+
+        // Expected format: length of abcd, newline, abcd, newline, chunk start - chunk end
+        assertThat(mOutputStream.toString("UTF-8")).isEqualTo(
+                String.format("%d\n%s\n%d-%d\n", testString.length(), testString, 0, 19));
+    }
+
+    @Test
+    public void writeChunk_overlappingPreviousChunk_combinesChunks() throws IOException {
+        mDiffScriptWriter.writeChunk(3, 4);
+
+        mDiffScriptWriter.writeChunk(7, 5);
+        mDiffScriptWriter.flush();
+
+        assertThat(mOutputStream.toString("UTF-8")).isEqualTo(String.format("3-11\n"));
+    }
+
+    @Test
+    public void writeChunk_formatsByteIndexesUsingArabicNumbers() throws Exception {
+        Locale.setDefault(HINDI);
+
+        mDiffScriptWriter.writeChunk(0, 12345);
+        mDiffScriptWriter.flush();
+
+        assertThat(mOutputStream.toString("UTF-8")).isEqualTo("0-12344\n");
+    }
+
+    @Test
+    public void flush_flushesOutputStream() throws IOException {
+        ByteArrayOutputStream mockOutputStream = mock(ByteArrayOutputStream.class);
+        SingleStreamDiffScriptWriter diffScriptWriter =
+                new SingleStreamDiffScriptWriter(mockOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
+
+        diffScriptWriter.flush();
+
+        verify(mockOutputStream).flush();
+    }
+
+    private void writeStringAsBytesToWriter(String string, SingleStreamDiffScriptWriter writer)
+            throws IOException {
+        byte[] bytes = string.getBytes("UTF-8");
+        for (int i = 0; i < bytes.length; i++) {
+            writer.writeByte(bytes[i]);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java b/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java
new file mode 100644
index 0000000..18011f6
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunker.java
@@ -0,0 +1,136 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import com.android.server.backup.encryption.chunking.Chunker;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/** Splits a stream of bytes into variable-sized chunks, using content-defined chunking. */
+public class ContentDefinedChunker implements Chunker {
+    private static final int WINDOW_SIZE = 31;
+    private static final byte DEFAULT_OUT_BYTE = (byte) 0;
+
+    private final byte[] mChunkBuffer;
+    private final RabinFingerprint64 mRabinFingerprint64;
+    private final FingerprintMixer mFingerprintMixer;
+    private final BreakpointPredicate mBreakpointPredicate;
+    private final int mMinChunkSize;
+    private final int mMaxChunkSize;
+
+    /**
+     * Constructor.
+     *
+     * @param minChunkSize The minimum size of a chunk. No chunk will be produced of a size smaller
+     *     than this except possibly at the very end of the stream.
+     * @param maxChunkSize The maximum size of a chunk. No chunk will be produced of a larger size.
+     * @param rabinFingerprint64 Calculates fingerprints, with which to determine breakpoints.
+     * @param breakpointPredicate Given a Rabin fingerprint, returns whether this ought to be a
+     *     breakpoint.
+     */
+    public ContentDefinedChunker(
+            int minChunkSize,
+            int maxChunkSize,
+            RabinFingerprint64 rabinFingerprint64,
+            FingerprintMixer fingerprintMixer,
+            BreakpointPredicate breakpointPredicate) {
+        checkArgument(
+                minChunkSize >= WINDOW_SIZE,
+                "Minimum chunk size must be greater than window size.");
+        checkArgument(
+                maxChunkSize >= minChunkSize,
+                "Maximum chunk size cannot be smaller than minimum chunk size.");
+        mChunkBuffer = new byte[maxChunkSize];
+        mRabinFingerprint64 = rabinFingerprint64;
+        mBreakpointPredicate = breakpointPredicate;
+        mFingerprintMixer = fingerprintMixer;
+        mMinChunkSize = minChunkSize;
+        mMaxChunkSize = maxChunkSize;
+    }
+
+    /**
+     * Breaks the input stream into variable-sized chunks.
+     *
+     * @param inputStream The input bytes to break into chunks.
+     * @param chunkConsumer A function to process each chunk as it's generated.
+     * @throws IOException Thrown if there is an issue reading from the input stream.
+     * @throws GeneralSecurityException Thrown if the {@link ChunkConsumer} throws it.
+     */
+    @Override
+    public void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
+            throws IOException, GeneralSecurityException {
+        int chunkLength;
+        int initialReadLength = mMinChunkSize - WINDOW_SIZE;
+
+        // Performance optimization - there is no reason to calculate fingerprints for windows
+        // ending before the minimum chunk size.
+        while ((chunkLength =
+                        inputStream.read(mChunkBuffer, /*off=*/ 0, /*len=*/ initialReadLength))
+                != -1) {
+            int b;
+            long fingerprint = 0L;
+
+            while ((b = inputStream.read()) != -1) {
+                byte inByte = (byte) b;
+                byte outByte = getCurrentWindowStartByte(chunkLength);
+                mChunkBuffer[chunkLength++] = inByte;
+
+                fingerprint =
+                        mRabinFingerprint64.computeFingerprint64(inByte, outByte, fingerprint);
+
+                if (chunkLength >= mMaxChunkSize
+                        || (chunkLength >= mMinChunkSize
+                                && mBreakpointPredicate.isBreakpoint(
+                                        mFingerprintMixer.mix(fingerprint)))) {
+                    chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
+                    chunkLength = 0;
+                    break;
+                }
+            }
+
+            if (chunkLength > 0) {
+                chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
+            }
+        }
+    }
+
+    private byte getCurrentWindowStartByte(int chunkLength) {
+        if (chunkLength < mMinChunkSize) {
+            return DEFAULT_OUT_BYTE;
+        } else {
+            return mChunkBuffer[chunkLength - WINDOW_SIZE];
+        }
+    }
+
+    /** Whether the current fingerprint indicates the end of a chunk. */
+    public interface BreakpointPredicate {
+
+        /**
+         * Returns {@code true} if the fingerprint of the last {@code WINDOW_SIZE} bytes indicates
+         * the chunk ought to end at this position.
+         *
+         * @param fingerprint Fingerprint of the last {@code WINDOW_SIZE} bytes.
+         * @return Whether this ought to be a chunk breakpoint.
+         */
+        boolean isBreakpoint(long fingerprint);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java b/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java
new file mode 100644
index 0000000..77b7347
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/ContentDefinedChunkerTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Random;
+
+import javax.crypto.SecretKey;
+
+/** Tests for {@link ContentDefinedChunker}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ContentDefinedChunkerTest {
+    private static final int WINDOW_SIZE_BYTES = 31;
+    private static final int MIN_SIZE_BYTES = 40;
+    private static final int MAX_SIZE_BYTES = 300;
+    private static final String CHUNK_BOUNDARY = "<----------BOUNDARY----------->";
+    private static final byte[] CHUNK_BOUNDARY_BYTES = CHUNK_BOUNDARY.getBytes(UTF_8);
+    private static final String CHUNK_1 = "This is the first chunk";
+    private static final String CHUNK_2 = "And this is the second chunk";
+    private static final String CHUNK_3 = "And finally here is the third chunk";
+    private static final String SMALL_CHUNK = "12345678";
+
+    private FingerprintMixer mFingerprintMixer;
+    private RabinFingerprint64 mRabinFingerprint64;
+    private ContentDefinedChunker mChunker;
+
+    /** Set up a {@link ContentDefinedChunker} and dependencies for use in the tests. */
+    @Before
+    public void setUp() throws Exception {
+        SecretKey secretKey = generateAesKey();
+        byte[] salt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
+        Random random = new Random();
+        random.nextBytes(salt);
+        mFingerprintMixer = new FingerprintMixer(secretKey, salt);
+
+        mRabinFingerprint64 = new RabinFingerprint64();
+        long chunkBoundaryFingerprint = calculateFingerprint(CHUNK_BOUNDARY_BYTES);
+        mChunker =
+                new ContentDefinedChunker(
+                        MIN_SIZE_BYTES,
+                        MAX_SIZE_BYTES,
+                        mRabinFingerprint64,
+                        mFingerprintMixer,
+                        (fingerprint) -> fingerprint == chunkBoundaryFingerprint);
+    }
+
+    /**
+     * Creating a {@link ContentDefinedChunker} with a minimum chunk size that is smaller than the
+     * window size should throw an {@link IllegalArgumentException}.
+     */
+    @Test
+    public void create_withMinChunkSizeSmallerThanWindowSize_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        new ContentDefinedChunker(
+                                WINDOW_SIZE_BYTES - 1,
+                                MAX_SIZE_BYTES,
+                                mRabinFingerprint64,
+                                mFingerprintMixer,
+                                null));
+    }
+
+    /**
+     * Creating a {@link ContentDefinedChunker} with a maximum chunk size that is smaller than the
+     * minimum chunk size should throw an {@link IllegalArgumentException}.
+     */
+    @Test
+    public void create_withMaxChunkSizeSmallerThanMinChunkSize_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        new ContentDefinedChunker(
+                                MIN_SIZE_BYTES,
+                                MIN_SIZE_BYTES - 1,
+                                mRabinFingerprint64,
+                                mFingerprintMixer,
+                                null));
+    }
+
+    /**
+     * {@link ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should split the
+     * input stream across chunk boundaries by default.
+     */
+    @Test
+    public void chunkify_withLargeChunks_splitsIntoChunksAcrossBoundaries() throws Exception {
+        byte[] input =
+                (CHUNK_1 + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY + CHUNK_3).getBytes(UTF_8);
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
+        ArrayList<String> result = new ArrayList<>();
+
+        mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
+
+        assertThat(result)
+                .containsExactly(CHUNK_1 + CHUNK_BOUNDARY, CHUNK_2 + CHUNK_BOUNDARY, CHUNK_3)
+                .inOrder();
+    }
+
+    /** Chunks should be combined across boundaries until they reach the minimum chunk size. */
+    @Test
+    public void chunkify_withSmallChunks_combinesChunksUntilMinSize() throws Exception {
+        byte[] input =
+                (SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY + CHUNK_3).getBytes(UTF_8);
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
+        ArrayList<String> result = new ArrayList<>();
+
+        mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
+
+        assertThat(result)
+                .containsExactly(SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2 + CHUNK_BOUNDARY, CHUNK_3)
+                .inOrder();
+        assertThat(result.get(0).length()).isAtLeast(MIN_SIZE_BYTES);
+    }
+
+    /** Chunks can not be larger than the maximum chunk size. */
+    @Test
+    public void chunkify_doesNotProduceChunksLargerThanMaxSize() throws Exception {
+        byte[] largeInput = new byte[MAX_SIZE_BYTES * 10];
+        Arrays.fill(largeInput, "a".getBytes(UTF_8)[0]);
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(largeInput);
+        ArrayList<String> result = new ArrayList<>();
+
+        mChunker.chunkify(inputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
+
+        byte[] expectedChunkBytes = new byte[MAX_SIZE_BYTES];
+        Arrays.fill(expectedChunkBytes, "a".getBytes(UTF_8)[0]);
+        String expectedChunk = new String(expectedChunkBytes, UTF_8);
+        assertThat(result)
+                .containsExactly(
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk,
+                        expectedChunk)
+                .inOrder();
+    }
+
+    /**
+     * If the input stream signals zero availablility, {@link
+     * ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should still work.
+     */
+    @Test
+    public void chunkify_withInputStreamReturningZeroAvailability_returnsChunks() throws Exception {
+        byte[] input = (SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2).getBytes(UTF_8);
+        ZeroAvailabilityInputStream zeroAvailabilityInputStream =
+                new ZeroAvailabilityInputStream(input);
+        ArrayList<String> result = new ArrayList<>();
+
+        mChunker.chunkify(
+                zeroAvailabilityInputStream, (chunk) -> result.add(new String(chunk, UTF_8)));
+
+        assertThat(result).containsExactly(SMALL_CHUNK + CHUNK_BOUNDARY + CHUNK_2).inOrder();
+    }
+
+    /**
+     * {@link ContentDefinedChunker#chunkify(InputStream, Chunker.ChunkConsumer)} should rethrow any
+     * exception thrown by its consumer.
+     */
+    @Test
+    public void chunkify_whenConsumerThrowsException_rethrowsException() throws Exception {
+        ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[] {1});
+
+        assertThrows(
+                GeneralSecurityException.class,
+                () ->
+                        mChunker.chunkify(
+                                inputStream,
+                                (chunk) -> {
+                                    throw new GeneralSecurityException();
+                                }));
+    }
+
+    private long calculateFingerprint(byte[] bytes) {
+        long fingerprint = 0;
+        for (byte inByte : bytes) {
+            fingerprint =
+                    mRabinFingerprint64.computeFingerprint64(
+                            /*inChar=*/ inByte, /*outChar=*/ (byte) 0, fingerprint);
+        }
+        return mFingerprintMixer.mix(fingerprint);
+    }
+
+    private static class ZeroAvailabilityInputStream extends ByteArrayInputStream {
+        ZeroAvailabilityInputStream(byte[] wrapped) {
+            super(wrapped);
+        }
+
+        @Override
+        public synchronized int available() {
+            return 0;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java b/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java
new file mode 100644
index 0000000..e9f3050
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/FingerprintMixer.java
@@ -0,0 +1,95 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Helper for mixing fingerprint with key material.
+ *
+ * <p>We do this as otherwise the Rabin fingerprint leaks information about the plaintext. i.e., if
+ * two users have the same file, it will be partitioned by Rabin in the same way, allowing us to
+ * infer that it is the same as another user's file.
+ *
+ * <p>By mixing the fingerprint with the user's secret key, the chunking method is different on a
+ * per key basis. Each application has its own {@link SecretKey}, so we cannot infer that a file is
+ * the same even across multiple applications owned by the same user, never mind across multiple
+ * users.
+ *
+ * <p>Instead of directly mixing the fingerprint with the user's secret, we first securely and
+ * deterministically derive a secondary chunking key. As Rabin is not a cryptographically secure
+ * hash, it might otherwise leak information about the user's secret. This prevents that from
+ * happening.
+ */
+public class FingerprintMixer {
+    public static final int SALT_LENGTH_BYTES = 256 / Byte.SIZE;
+    private static final String DERIVED_KEY_NAME = "RabinFingerprint64Mixer";
+
+    private final long mAddend;
+    private final long mMultiplicand;
+
+    /**
+     * A new instance from a given secret key and salt. Salt must be the same across incremental
+     * backups, or a different chunking strategy will be used each time, defeating the dedup.
+     *
+     * @param secretKey The application-specific secret.
+     * @param salt The salt.
+     * @throws InvalidKeyException If the encoded form of {@code secretKey} is inaccessible.
+     */
+    public FingerprintMixer(SecretKey secretKey, byte[] salt) throws InvalidKeyException {
+        checkArgument(salt.length == SALT_LENGTH_BYTES, "Requires a 256-bit salt.");
+        byte[] keyBytes = secretKey.getEncoded();
+        if (keyBytes == null) {
+            throw new InvalidKeyException("SecretKey must support encoding for FingerprintMixer.");
+        }
+        byte[] derivedKey =
+                Hkdf.hkdf(keyBytes, salt, DERIVED_KEY_NAME.getBytes(StandardCharsets.UTF_8));
+        ByteBuffer buffer = ByteBuffer.wrap(derivedKey);
+        mAddend = buffer.getLong();
+        // Multiplicand must be odd - otherwise we lose some bits of the Rabin fingerprint when
+        // mixing
+        mMultiplicand = buffer.getLong() | 1;
+    }
+
+    /**
+     * Mixes the fingerprint with the derived key material. This is performed by adding part of the
+     * derived key and multiplying by another part of the derived key (which is forced to be odd, so
+     * that the operation is reversible).
+     *
+     * @param fingerprint A 64-bit Rabin fingerprint.
+     * @return The mixed fingerprint.
+     */
+    long mix(long fingerprint) {
+        return ((fingerprint + mAddend) * mMultiplicand);
+    }
+
+    /** The addend part of the derived key. */
+    long getAddend() {
+        return mAddend;
+    }
+
+    /** The multiplicand part of the derived key. */
+    long getMultiplicand() {
+        return mMultiplicand;
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java b/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java
new file mode 100644
index 0000000..936b5dc
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/FingerprintMixerTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.util.HashSet;
+import java.util.Random;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Tests for {@link FingerprintMixer}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class FingerprintMixerTest {
+    private static final String KEY_ALGORITHM = "AES";
+    private static final int SEED = 42;
+    private static final int SALT_LENGTH_BYTES = 256 / 8;
+    private static final int KEY_SIZE_BITS = 256;
+
+    private Random mSeededRandom;
+    private FingerprintMixer mFingerprintMixer;
+
+    /** Set up a {@link FingerprintMixer} with deterministic key and salt generation. */
+    @Before
+    public void setUp() throws Exception {
+        // Seed so that the tests are deterministic.
+        mSeededRandom = new Random(SEED);
+        mFingerprintMixer = new FingerprintMixer(randomKey(), randomSalt());
+    }
+
+    /**
+     * Construcing a {@link FingerprintMixer} with a salt that is too small should throw an {@link
+     * IllegalArgumentException}.
+     */
+    @Test
+    public void create_withIncorrectSaltSize_throwsIllegalArgumentException() {
+        byte[] tooSmallSalt = new byte[SALT_LENGTH_BYTES - 1];
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new FingerprintMixer(randomKey(), tooSmallSalt));
+    }
+
+    /**
+     * Constructing a {@link FingerprintMixer} with a secret key that can't be encoded should throw
+     * an {@link InvalidKeyException}.
+     */
+    @Test
+    public void create_withUnencodableSecretKey_throwsInvalidKeyException() {
+        byte[] keyBytes = new byte[KEY_SIZE_BITS / 8];
+        UnencodableSecretKeySpec keySpec =
+                new UnencodableSecretKeySpec(keyBytes, 0, keyBytes.length, KEY_ALGORITHM);
+
+        assertThrows(InvalidKeyException.class, () -> new FingerprintMixer(keySpec, randomSalt()));
+    }
+
+    /**
+     * {@link FingerprintMixer#getAddend()} should not return the same addend for two different
+     * keys.
+     */
+    @Test
+    public void getAddend_withDifferentKey_returnsDifferentResult() throws Exception {
+        int iterations = 100_000;
+        HashSet<Long> returnedAddends = new HashSet<>();
+        byte[] salt = randomSalt();
+
+        for (int i = 0; i < iterations; i++) {
+            FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), salt);
+            long addend = fingerprintMixer.getAddend();
+            returnedAddends.add(addend);
+        }
+
+        assertThat(returnedAddends).containsNoDuplicates();
+    }
+
+    /**
+     * {@link FingerprintMixer#getMultiplicand()} should not return the same multiplicand for two
+     * different keys.
+     */
+    @Test
+    public void getMultiplicand_withDifferentKey_returnsDifferentResult() throws Exception {
+        int iterations = 100_000;
+        HashSet<Long> returnedMultiplicands = new HashSet<>();
+        byte[] salt = randomSalt();
+
+        for (int i = 0; i < iterations; i++) {
+            FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), salt);
+            long multiplicand = fingerprintMixer.getMultiplicand();
+            returnedMultiplicands.add(multiplicand);
+        }
+
+        assertThat(returnedMultiplicands).containsNoDuplicates();
+    }
+
+    /** The multiplicant returned by {@link FingerprintMixer} should always be odd. */
+    @Test
+    public void getMultiplicand_isOdd() throws Exception {
+        int iterations = 100_000;
+
+        for (int i = 0; i < iterations; i++) {
+            FingerprintMixer fingerprintMixer = new FingerprintMixer(randomKey(), randomSalt());
+
+            long multiplicand = fingerprintMixer.getMultiplicand();
+
+            assertThat(isOdd(multiplicand)).isTrue();
+        }
+    }
+
+    /** {@link FingerprintMixer#mix(long)} should have a random distribution. */
+    @Test
+    public void mix_randomlyDistributesBits() throws Exception {
+        int iterations = 100_000;
+        float tolerance = 0.1f;
+        int[] totals = new int[64];
+
+        for (int i = 0; i < iterations; i++) {
+            long n = mFingerprintMixer.mix(mSeededRandom.nextLong());
+            for (int j = 0; j < 64; j++) {
+                int bit = (int) (n >> j & 1);
+                totals[j] += bit;
+            }
+        }
+
+        for (int i = 0; i < 64; i++) {
+            float mean = ((float) totals[i]) / iterations;
+            float diff = Math.abs(mean - 0.5f);
+            assertThat(diff).isLessThan(tolerance);
+        }
+    }
+
+    /**
+     * {@link FingerprintMixer#mix(long)} should always produce a number that's different from the
+     * input.
+     */
+    @Test
+    public void mix_doesNotProduceSameNumberAsInput() {
+        int iterations = 100_000;
+
+        for (int i = 0; i < iterations; i++) {
+            assertThat(mFingerprintMixer.mix(i)).isNotEqualTo(i);
+        }
+    }
+
+    private byte[] randomSalt() {
+        byte[] salt = new byte[SALT_LENGTH_BYTES];
+        mSeededRandom.nextBytes(salt);
+        return salt;
+    }
+
+    /**
+     * Not a secure way of generating keys. We want to deterministically generate the same keys for
+     * each test run, though, to ensure the test is deterministic.
+     */
+    private SecretKey randomKey() {
+        byte[] keyBytes = new byte[KEY_SIZE_BITS / 8];
+        mSeededRandom.nextBytes(keyBytes);
+        return new SecretKeySpec(keyBytes, 0, keyBytes.length, KEY_ALGORITHM);
+    }
+
+    private static boolean isOdd(long n) {
+        return Math.abs(n % 2) == 1;
+    }
+
+    /**
+     * Subclass of {@link SecretKeySpec} that does not provide an encoded version. As per its
+     * contract in {@link Key}, that means {@code getEncoded()} always returns null.
+     */
+    private class UnencodableSecretKeySpec extends SecretKeySpec {
+        UnencodableSecretKeySpec(byte[] key, int offset, int len, String algorithm) {
+            super(key, offset, len, algorithm);
+        }
+
+        @Override
+        public byte[] getEncoded() {
+            return null;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/Hkdf.java b/com/android/server/backup/encryption/chunking/cdc/Hkdf.java
new file mode 100644
index 0000000..c7af8c8
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/Hkdf.java
@@ -0,0 +1,114 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Secure HKDF utils. Allows client to deterministically derive additional key material from a base
+ * secret. If the derived key material is compromised, this does not in of itself compromise the
+ * root secret.
+ *
+ * <p>TODO(b/116575321): After all code is ported, rename this class to HkdfUtils.
+ */
+public final class Hkdf {
+    private static final byte[] CONSTANT_01 = {0x01};
+    private static final String HmacSHA256 = "HmacSHA256";
+    private static final String AES = "AES";
+
+    /**
+     * Implements HKDF (RFC 5869) with the SHA-256 hash and a 256-bit output key length.
+     *
+     * <p>IMPORTANT: The use or edit of this method requires a security review.
+     *
+     * @param masterKey Master key from which to derive sub-keys.
+     * @param salt A randomly generated 256-bit byte string.
+     * @param data Arbitrary information that is bound to the derived key (i.e., used in its
+     *     creation).
+     * @return Raw derived key bytes = HKDF-SHA256(masterKey, salt, data).
+     * @throws InvalidKeyException If the salt can not be used as a valid key.
+     */
+    static byte[] hkdf(byte[] masterKey, byte[] salt, byte[] data) throws InvalidKeyException {
+        Objects.requireNonNull(masterKey, "HKDF requires master key to be set.");
+        Objects.requireNonNull(salt, "HKDF requires a salt.");
+        Objects.requireNonNull(data, "No data provided to HKDF.");
+        return hkdfSha256Expand(hkdfSha256Extract(masterKey, salt), data);
+    }
+
+    private Hkdf() {}
+
+    /**
+     * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. This function is
+     * used to pre-process the {@code inputKeyMaterial} and mix it with the {@code salt}, producing
+     * output suitable for use with HKDF expansion function (which produces the actual derived key).
+     *
+     * <p>IMPORTANT: The use or edit of this method requires a security review.
+     *
+     * @see #hkdfSha256Expand(byte[], byte[])
+     * @return HMAC-SHA256(salt, inputKeyMaterial) (salt is the "key" for the HMAC)
+     * @throws InvalidKeyException If the salt can not be used as a valid key.
+     */
+    private static byte[] hkdfSha256Extract(byte[] inputKeyMaterial, byte[] salt)
+            throws InvalidKeyException {
+        // Note that the SecretKey encoding format is defined to be RAW, so the encoded form should
+        // be consistent across implementations.
+        Mac sha256;
+        try {
+            sha256 = Mac.getInstance(HmacSHA256);
+        } catch (NoSuchAlgorithmException e) {
+            // This can not happen - HmacSHA256 is supported by the platform.
+            throw new AssertionError(e);
+        }
+        sha256.init(new SecretKeySpec(salt, AES));
+
+        return sha256.doFinal(inputKeyMaterial);
+    }
+
+    /**
+     * Special case of HKDF (RFC 5869) expansion function, using the SHA-256 hash function and
+     * allowing for a maximum output length of 256 bits.
+     *
+     * <p>IMPORTANT: The use or edit of this method requires a security review.
+     *
+     * @param pseudoRandomKey Generated by {@link #hkdfSha256Extract(byte[], byte[])}.
+     * @param info Arbitrary information the derived key should be bound to.
+     * @return Raw derived key bytes = HMAC-SHA256(pseudoRandomKey, info | 0x01).
+     * @throws InvalidKeyException If the salt can not be used as a valid key.
+     */
+    private static byte[] hkdfSha256Expand(byte[] pseudoRandomKey, byte[] info)
+            throws InvalidKeyException {
+        // Note that RFC 5869 computes number of blocks N = ceil(hash length / output length), but
+        // here we only deal with a 256 bit hash up to a 256 bit output, yielding N=1.
+        Mac sha256;
+        try {
+            sha256 = Mac.getInstance(HmacSHA256);
+        } catch (NoSuchAlgorithmException e) {
+            // This can not happen - HmacSHA256 is supported by the platform.
+            throw new AssertionError(e);
+        }
+        sha256.init(new SecretKeySpec(pseudoRandomKey, AES));
+
+        sha256.update(info);
+        sha256.update(CONSTANT_01);
+        return sha256.doFinal();
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java b/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java
new file mode 100644
index 0000000..5494374
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/HkdfTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link Hkdf}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class HkdfTest {
+    /** HKDF Test Case 1 IKM from RFC 5869 */
+    private static final byte[] HKDF_CASE1_IKM = {
+        0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+        0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+        0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+        0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+        0x0b, 0x0b
+    };
+
+    /** HKDF Test Case 1 salt from RFC 5869 */
+    private static final byte[] HKDF_CASE1_SALT = {
+        0x00, 0x01, 0x02, 0x03, 0x04,
+        0x05, 0x06, 0x07, 0x08, 0x09,
+        0x0a, 0x0b, 0x0c
+    };
+
+    /** HKDF Test Case 1 info from RFC 5869 */
+    private static final byte[] HKDF_CASE1_INFO = {
+        (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, (byte) 0xf4,
+        (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, (byte) 0xf8, (byte) 0xf9
+    };
+
+    /** First 32 bytes of HKDF Test Case 1 OKM (output) from RFC 5869 */
+    private static final byte[] HKDF_CASE1_OKM = {
+        (byte) 0x3c, (byte) 0xb2, (byte) 0x5f, (byte) 0x25, (byte) 0xfa,
+        (byte) 0xac, (byte) 0xd5, (byte) 0x7a, (byte) 0x90, (byte) 0x43,
+        (byte) 0x4f, (byte) 0x64, (byte) 0xd0, (byte) 0x36, (byte) 0x2f,
+        (byte) 0x2a, (byte) 0x2d, (byte) 0x2d, (byte) 0x0a, (byte) 0x90,
+        (byte) 0xcf, (byte) 0x1a, (byte) 0x5a, (byte) 0x4c, (byte) 0x5d,
+        (byte) 0xb0, (byte) 0x2d, (byte) 0x56, (byte) 0xec, (byte) 0xc4,
+        (byte) 0xc5, (byte) 0xbf
+    };
+
+    /** Test the example from RFC 5869. */
+    @Test
+    public void hkdf_derivesKeyMaterial() throws Exception {
+        byte[] result = Hkdf.hkdf(HKDF_CASE1_IKM, HKDF_CASE1_SALT, HKDF_CASE1_INFO);
+
+        assertThat(result).isEqualTo(HKDF_CASE1_OKM);
+    }
+
+    /** Providing a key that is null should throw a {@link java.lang.NullPointerException}. */
+    @Test
+    public void hkdf_withNullKey_throwsNullPointerException() throws Exception {
+        assertThrows(
+                NullPointerException.class,
+                () -> Hkdf.hkdf(null, HKDF_CASE1_SALT, HKDF_CASE1_INFO));
+    }
+
+    /** Providing a salt that is null should throw a {@link java.lang.NullPointerException}. */
+    @Test
+    public void hkdf_withNullSalt_throwsNullPointerException() throws Exception {
+        assertThrows(
+                NullPointerException.class, () -> Hkdf.hkdf(HKDF_CASE1_IKM, null, HKDF_CASE1_INFO));
+    }
+
+    /** Providing data that is null should throw a {@link java.lang.NullPointerException}. */
+    @Test
+    public void hkdf_withNullData_throwsNullPointerException() throws Exception {
+        assertThrows(
+                NullPointerException.class, () -> Hkdf.hkdf(HKDF_CASE1_IKM, HKDF_CASE1_SALT, null));
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java b/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java
new file mode 100644
index 0000000..e867e7c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpoint.java
@@ -0,0 +1,78 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker.BreakpointPredicate;
+
+/**
+ * Function to determine whether a 64-bit fingerprint ought to be a chunk breakpoint.
+ *
+ * <p>This works by checking whether there are at least n leading zeros in the fingerprint. n is
+ * calculated to on average cause a breakpoint after a given number of trials (provided in the
+ * constructor). This allows us to choose a number of trials that gives a desired average chunk
+ * size. This works because the fingerprint is pseudo-randomly distributed.
+ */
+public class IsChunkBreakpoint implements BreakpointPredicate {
+    private final int mLeadingZeros;
+    private final long mBitmask;
+
+    /**
+     * A new instance that causes a breakpoint after a given number of trials on average.
+     *
+     * @param averageNumberOfTrialsUntilBreakpoint The number of trials after which on average to
+     *     create a new chunk. If this is not a power of 2, some precision is sacrificed (i.e., on
+     *     average, breaks will actually happen after the nearest power of 2 to the average number
+     *     of trials passed in).
+     */
+    public IsChunkBreakpoint(long averageNumberOfTrialsUntilBreakpoint) {
+        checkArgument(
+                averageNumberOfTrialsUntilBreakpoint >= 0,
+                "Average number of trials must be non-negative");
+
+        // Want n leading zeros after t trials.
+        // P(leading zeros = n) = 1/2^n
+        // Expected num trials to get n leading zeros = 1/2^-n
+        // t = 1/2^-n
+        // n = log2(t)
+        mLeadingZeros = (int) Math.round(log2(averageNumberOfTrialsUntilBreakpoint));
+        mBitmask = ~(~0L >>> mLeadingZeros);
+    }
+
+    /**
+     * Returns {@code true} if {@code fingerprint} indicates that there should be a chunk
+     * breakpoint.
+     */
+    @Override
+    public boolean isBreakpoint(long fingerprint) {
+        return (fingerprint & mBitmask) == 0;
+    }
+
+    /** Returns the number of leading zeros in the fingerprint that causes a breakpoint. */
+    public int getLeadingZeros() {
+        return mLeadingZeros;
+    }
+
+    /**
+     * Calculates log base 2 of x. Not the most efficient possible implementation, but it's simple,
+     * obviously correct, and is only invoked on object construction.
+     */
+    private static double log2(double x) {
+        return Math.log(x) / Math.log(2);
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java b/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java
new file mode 100644
index 0000000..277dc37
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/IsChunkBreakpointTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Random;
+
+/** Tests for {@link IsChunkBreakpoint}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class IsChunkBreakpointTest {
+    private static final int RANDOM_SEED = 42;
+    private static final double TOLERANCE = 0.01;
+    private static final int NUMBER_OF_TESTS = 10000;
+    private static final int BITS_PER_LONG = 64;
+
+    private Random mRandom;
+
+    /** Make sure that tests are deterministic. */
+    @Before
+    public void setUp() {
+        mRandom = new Random(RANDOM_SEED);
+    }
+
+    /**
+     * Providing a negative average number of trials should throw an {@link
+     * IllegalArgumentException}.
+     */
+    @Test
+    public void create_withNegativeAverageNumberOfTrials_throwsIllegalArgumentException() {
+        assertThrows(IllegalArgumentException.class, () -> new IsChunkBreakpoint(-1));
+    }
+
+    // Note: the following three tests are compute-intensive, so be cautious adding more.
+
+    /**
+     * If the provided average number of trials is zero, a breakpoint should be expected after one
+     * trial on average.
+     */
+    @Test
+    public void
+            isBreakpoint_withZeroAverageNumberOfTrials_isTrueOnAverageAfterOneTrial() {
+        assertExpectedTrials(new IsChunkBreakpoint(0), /*expectedTrials=*/ 1);
+    }
+
+    /**
+     * If the provided average number of trials is 512, a breakpoint should be expected after 512
+     * trials on average.
+     */
+    @Test
+    public void
+            isBreakpoint_with512AverageNumberOfTrials_isTrueOnAverageAfter512Trials() {
+        assertExpectedTrials(new IsChunkBreakpoint(512), /*expectedTrials=*/ 512);
+    }
+
+    /**
+     * If the provided average number of trials is 1024, a breakpoint should be expected after 1024
+     * trials on average.
+     */
+    @Test
+    public void
+            isBreakpoint_with1024AverageNumberOfTrials_isTrueOnAverageAfter1024Trials() {
+        assertExpectedTrials(new IsChunkBreakpoint(1024), /*expectedTrials=*/ 1024);
+    }
+
+    /** The number of leading zeros should be the logarithm of the average number of trials. */
+    @Test
+    public void getLeadingZeros_squaredIsAverageNumberOfTrials() {
+        for (int i = 0; i < BITS_PER_LONG; i++) {
+            long averageNumberOfTrials = (long) Math.pow(2, i);
+
+            int leadingZeros = new IsChunkBreakpoint(averageNumberOfTrials).getLeadingZeros();
+
+            assertThat(leadingZeros).isEqualTo(i);
+        }
+    }
+
+    private void assertExpectedTrials(IsChunkBreakpoint isChunkBreakpoint, long expectedTrials) {
+        long sum = 0;
+        for (int i = 0; i < NUMBER_OF_TESTS; i++) {
+            sum += numberOfTrialsTillBreakpoint(isChunkBreakpoint);
+        }
+        long averageTrials = sum / NUMBER_OF_TESTS;
+        assertThat((double) Math.abs(averageTrials - expectedTrials))
+                .isLessThan(TOLERANCE * expectedTrials);
+    }
+
+    private int numberOfTrialsTillBreakpoint(IsChunkBreakpoint isChunkBreakpoint) {
+        int trials = 0;
+
+        while (true) {
+            trials++;
+            if (isChunkBreakpoint.isBreakpoint(mRandom.nextLong())) {
+                return trials;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java b/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java
new file mode 100644
index 0000000..1e14ffa
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64.java
@@ -0,0 +1,113 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+/** Helper to calculate a 64-bit Rabin fingerprint over a 31-byte window. */
+public class RabinFingerprint64 {
+    private static final long DEFAULT_IRREDUCIBLE_POLYNOMIAL_64 = 0x000000000000001BL;
+    private static final int POLYNOMIAL_DEGREE = 64;
+    private static final int SLIDING_WINDOW_SIZE_BYTES = 31;
+
+    private final long mPoly64;
+    // Auxiliary tables to speed up the computation of Rabin fingerprints.
+    private final long[] mTableFP64 = new long[256];
+    private final long[] mTableOutByte = new long[256];
+
+    /**
+     * Constructs a new instance over the given irreducible 64-degree polynomial. It is up to the
+     * caller to determine that the polynomial is irreducible. If it is not the fingerprinting will
+     * not behave as expected.
+     *
+     * @param poly64 The polynomial.
+     */
+    public RabinFingerprint64(long poly64) {
+        mPoly64 = poly64;
+    }
+
+    /** Constructs a new instance using {@code x^64 + x^4 + x + 1} as the irreducible polynomial. */
+    public RabinFingerprint64() {
+        this(DEFAULT_IRREDUCIBLE_POLYNOMIAL_64);
+        computeFingerprintTables64();
+        computeFingerprintTables64Windowed();
+    }
+
+    /**
+     * Computes the fingerprint for the new sliding window given the fingerprint of the previous
+     * sliding window, the byte sliding in, and the byte sliding out.
+     *
+     * @param inChar The new char coming into the sliding window.
+     * @param outChar The left most char sliding out of the window.
+     * @param fingerPrint Fingerprint for previous window.
+     * @return New fingerprint for the new sliding window.
+     */
+    public long computeFingerprint64(byte inChar, byte outChar, long fingerPrint) {
+        return (fingerPrint << 8)
+                ^ (inChar & 0xFF)
+                ^ mTableFP64[(int) (fingerPrint >>> 56)]
+                ^ mTableOutByte[outChar & 0xFF];
+    }
+
+    /** Compute auxiliary tables to speed up the fingerprint computation. */
+    private void computeFingerprintTables64() {
+        long[] degreesRes64 = new long[POLYNOMIAL_DEGREE];
+        degreesRes64[0] = mPoly64;
+        for (int i = 1; i < POLYNOMIAL_DEGREE; i++) {
+            if ((degreesRes64[i - 1] & (1L << 63)) == 0) {
+                degreesRes64[i] = degreesRes64[i - 1] << 1;
+            } else {
+                degreesRes64[i] = (degreesRes64[i - 1] << 1) ^ mPoly64;
+            }
+        }
+        for (int i = 0; i < 256; i++) {
+            int currIndex = i;
+            for (int j = 0; (currIndex > 0) && (j < 8); j++) {
+                if ((currIndex & 0x1) == 1) {
+                    mTableFP64[i] ^= degreesRes64[j];
+                }
+                currIndex >>>= 1;
+            }
+        }
+    }
+
+    /**
+     * Compute auxiliary table {@code mTableOutByte} to facilitate the computing of fingerprints for
+     * sliding windows. This table is to take care of the effect on the fingerprint when the
+     * leftmost byte in the window slides out.
+     */
+    private void computeFingerprintTables64Windowed() {
+        // Auxiliary array degsRes64[8] defined by: <code>degsRes64[i] = x^(8 *
+        // SLIDING_WINDOW_SIZE_BYTES + i) mod this.mPoly64.</code>
+        long[] degsRes64 = new long[8];
+        degsRes64[0] = mPoly64;
+        for (int i = 65; i < 8 * (SLIDING_WINDOW_SIZE_BYTES + 1); i++) {
+            if ((degsRes64[(i - 1) % 8] & (1L << 63)) == 0) {
+                degsRes64[i % 8] = degsRes64[(i - 1) % 8] << 1;
+            } else {
+                degsRes64[i % 8] = (degsRes64[(i - 1) % 8] << 1) ^ mPoly64;
+            }
+        }
+        for (int i = 0; i < 256; i++) {
+            int currIndex = i;
+            for (int j = 0; (currIndex > 0) && (j < 8); j++) {
+                if ((currIndex & 0x1) == 1) {
+                    mTableOutByte[i] ^= degsRes64[j];
+                }
+                currIndex >>>= 1;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java b/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java
new file mode 100644
index 0000000..729580c
--- /dev/null
+++ b/com/android/server/backup/encryption/chunking/cdc/RabinFingerprint64Test.java
@@ -0,0 +1,132 @@
+/*
+ * 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.encryption.chunking.cdc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link RabinFingerprint64}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class RabinFingerprint64Test {
+    private static final int WINDOW_SIZE = 31;
+    private static final ImmutableList<String> TEST_STRINGS =
+            ImmutableList.of(
+                    "ervHTtChYXO6eXivYqThlyyzqkbRaOR",
+                    "IxaVunH9ZC3qneWfhj1GkBH4ys9CYqz",
+                    "wZRVjlE1p976icCFPX9pibk4PEBvjSH",
+                    "pHIVaT8x8If9D6s9croksgNmJpmGYWI");
+
+    private final RabinFingerprint64 mRabinFingerprint64 = new RabinFingerprint64();
+
+    /**
+     * No matter where in the input buffer a string occurs, {@link
+     * RabinFingerprint64#computeFingerprint64(byte, byte, long)} should return the same
+     * fingerprint.
+     */
+    @Test
+    public void computeFingerprint64_forSameWindow_returnsSameFingerprint() {
+        long fingerprint1 =
+                computeFingerprintAtPosition(getBytes(TEST_STRINGS.get(0)), WINDOW_SIZE - 1);
+        long fingerprint2 =
+                computeFingerprintAtPosition(
+                        getBytes(TEST_STRINGS.get(1), TEST_STRINGS.get(0)), WINDOW_SIZE * 2 - 1);
+        long fingerprint3 =
+                computeFingerprintAtPosition(
+                        getBytes(TEST_STRINGS.get(2), TEST_STRINGS.get(3), TEST_STRINGS.get(0)),
+                        WINDOW_SIZE * 3 - 1);
+        String stub = "abc";
+        long fingerprint4 =
+                computeFingerprintAtPosition(
+                        getBytes(stub, TEST_STRINGS.get(0)), WINDOW_SIZE + stub.length() - 1);
+
+        // Assert that all fingerprints are exactly the same
+        assertThat(ImmutableSet.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
+                .hasSize(1);
+    }
+
+    /** The computed fingerprint should be different for different inputs. */
+    @Test
+    public void computeFingerprint64_withDifferentInput_returnsDifferentFingerprint() {
+        long fingerprint1 = computeFingerprintOf(TEST_STRINGS.get(0));
+        long fingerprint2 = computeFingerprintOf(TEST_STRINGS.get(1));
+        long fingerprint3 = computeFingerprintOf(TEST_STRINGS.get(2));
+        long fingerprint4 = computeFingerprintOf(TEST_STRINGS.get(3));
+
+        assertThat(ImmutableList.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
+                .containsNoDuplicates();
+    }
+
+    /**
+     * An input with the same characters in a different order should return a different fingerprint.
+     */
+    @Test
+    public void computeFingerprint64_withSameInputInDifferentOrder_returnsDifferentFingerprint() {
+        long fingerprint1 = computeFingerprintOf("abcdefghijklmnopqrstuvwxyz12345");
+        long fingerprint2 = computeFingerprintOf("54321zyxwvutsrqponmlkjihgfedcba");
+        long fingerprint3 = computeFingerprintOf("4bcdefghijklmnopqrstuvwxyz123a5");
+        long fingerprint4 = computeFingerprintOf("bacdefghijklmnopqrstuvwxyz12345");
+
+        assertThat(ImmutableList.of(fingerprint1, fingerprint2, fingerprint3, fingerprint4))
+                .containsNoDuplicates();
+    }
+
+    /** UTF-8 bytes of all the given strings in order. */
+    private byte[] getBytes(String... strings) {
+        StringBuilder sb = new StringBuilder();
+        for (String s : strings) {
+            sb.append(s);
+        }
+        return sb.toString().getBytes(UTF_8);
+    }
+
+    /**
+     * The Rabin fingerprint of a window of bytes ending at {@code position} in the {@code bytes}
+     * array.
+     */
+    private long computeFingerprintAtPosition(byte[] bytes, int position) {
+        assertThat(position).isAtMost(bytes.length - 1);
+        long fingerprint = 0;
+        for (int i = 0; i <= position; i++) {
+            byte outChar;
+            if (i >= WINDOW_SIZE) {
+                outChar = bytes[i - WINDOW_SIZE];
+            } else {
+                outChar = (byte) 0;
+            }
+            fingerprint =
+                    mRabinFingerprint64.computeFingerprint64(
+                            /*inChar=*/ bytes[i], outChar, fingerprint);
+        }
+        return fingerprint;
+    }
+
+    private long computeFingerprintOf(String s) {
+        assertThat(s.length()).isEqualTo(WINDOW_SIZE);
+        return computeFingerprintAtPosition(s.getBytes(UTF_8), WINDOW_SIZE - 1);
+    }
+}
diff --git a/com/android/server/backup/encryption/client/CryptoBackupServer.java b/com/android/server/backup/encryption/client/CryptoBackupServer.java
new file mode 100644
index 0000000..d7f7dc7
--- /dev/null
+++ b/com/android/server/backup/encryption/client/CryptoBackupServer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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.encryption.client;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.util.Map;
+
+/**
+ * Contains methods for communicating with the parts of the backup server relevant to encryption.
+ */
+public interface CryptoBackupServer {
+    /**
+     * Uploads an incremental backup to the server.
+     *
+     * <p>Handles setting up and tearing down the connection.
+     *
+     * @param packageName the package to associate the data with
+     * @param oldDocId the id of the previous backup doc in Drive
+     * @param diffScript containing the actual backup data
+     * @param tertiaryKey the wrapped key used to encrypt this backup
+     * @return the id of the new backup doc in Drive.
+     */
+    String uploadIncrementalBackup(
+            String packageName,
+            String oldDocId,
+            byte[] diffScript,
+            WrappedKeyProto.WrappedKey tertiaryKey);
+
+    /**
+     * Uploads non-incremental backup to the server.
+     *
+     * <p>Handles setting up and tearing down the connection.
+     *
+     * @param packageName the package to associate the data with
+     * @param data the actual backup data
+     * @param tertiaryKey the wrapped key used to encrypt this backup
+     * @return the id of the new backup doc in Drive.
+     */
+    String uploadNonIncrementalBackup(
+            String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey);
+
+    /**
+     * Sets the alias of the active secondary key. This is the alias used to refer to the key in the
+     * {@link java.security.KeyStore}. It is also used to key storage for tertiary keys on the
+     * backup server. Also has to upload all existing tertiary keys, wrapped with the new key.
+     *
+     * @param keyAlias The ID of the secondary key.
+     * @param tertiaryKeys The tertiary keys, wrapped with the new secondary key.
+     */
+    void setActiveSecondaryKeyAlias(
+            String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys);
+}
diff --git a/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java b/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
new file mode 100644
index 0000000..9e31385
--- /dev/null
+++ b/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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.encryption.client;
+
+/**
+ * Error thrown when the user attempts to retrieve a key set from the server, but is asking for keys
+ * from an inactive secondary.
+ *
+ * <p>Although we could just return old keys, there is no good reason to do this. It almost
+ * certainly indicates a logic error on the client.
+ */
+public class UnexpectedActiveSecondaryOnServerException extends Exception {
+    public UnexpectedActiveSecondaryOnServerException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/KeyWrapUtils.java b/com/android/server/backup/encryption/keys/KeyWrapUtils.java
new file mode 100644
index 0000000..a043c1f
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/KeyWrapUtils.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+/** Utility functions for wrapping and unwrapping tertiary keys. */
+public class KeyWrapUtils {
+    private static final String AES_GCM_MODE = "AES/GCM/NoPadding";
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+    private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
+    private static final String KEY_ALGORITHM = "AES";
+
+    /**
+     * Uses the secondary key to unwrap the wrapped tertiary key.
+     *
+     * @param secondaryKey The secondary key used to wrap the tertiary key.
+     * @param wrappedKey The wrapped tertiary key.
+     * @return The unwrapped tertiary key.
+     * @throws InvalidKeyException if the provided secondary key cannot unwrap the tertiary key.
+     */
+    public static SecretKey unwrap(SecretKey secondaryKey, WrappedKeyProto.WrappedKey wrappedKey)
+            throws InvalidKeyException, NoSuchAlgorithmException,
+                    InvalidAlgorithmParameterException, NoSuchPaddingException {
+        if (wrappedKey.wrapAlgorithm != WrappedKeyProto.WrappedKey.AES_256_GCM) {
+            throw new InvalidKeyException(
+                    String.format(
+                            Locale.US,
+                            "Could not unwrap key wrapped with %s algorithm",
+                            wrappedKey.wrapAlgorithm));
+        }
+
+        if (wrappedKey.metadata == null) {
+            throw new InvalidKeyException("Metadata missing from wrapped tertiary key.");
+        }
+
+        if (wrappedKey.metadata.type != WrappedKeyProto.KeyMetadata.AES_256_GCM) {
+            throw new InvalidKeyException(
+                    String.format(
+                            Locale.US,
+                            "Wrapped key was unexpected %s algorithm. Only support"
+                                + " AES/GCM/NoPadding.",
+                            wrappedKey.metadata.type));
+        }
+
+        Cipher cipher = getCipher();
+
+        cipher.init(
+                Cipher.UNWRAP_MODE,
+                secondaryKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.nonce));
+
+        return (SecretKey) cipher.unwrap(wrappedKey.key, KEY_ALGORITHM, Cipher.SECRET_KEY);
+    }
+
+    /**
+     * Wraps the tertiary key with the secondary key.
+     *
+     * @param secondaryKey The secondary key to use for wrapping.
+     * @param tertiaryKey The key to wrap.
+     * @return The wrapped key.
+     * @throws InvalidKeyException if the key is not good for wrapping.
+     * @throws IllegalBlockSizeException if there is an issue wrapping.
+     */
+    public static WrappedKeyProto.WrappedKey wrap(SecretKey secondaryKey, SecretKey tertiaryKey)
+            throws InvalidKeyException, IllegalBlockSizeException, NoSuchAlgorithmException,
+                    NoSuchPaddingException {
+        Cipher cipher = getCipher();
+        cipher.init(Cipher.WRAP_MODE, secondaryKey);
+
+        WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
+        wrappedKey.key = cipher.wrap(tertiaryKey);
+        wrappedKey.nonce = cipher.getIV();
+        wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.AES_256_GCM;
+        wrappedKey.metadata = new WrappedKeyProto.KeyMetadata();
+        wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.AES_256_GCM;
+        return wrappedKey;
+    }
+
+    /**
+     * Rewraps a tertiary key with a new secondary key.
+     *
+     * @param oldSecondaryKey The old secondary key, used to unwrap the tertiary key.
+     * @param newSecondaryKey The new secondary key, used to rewrap the tertiary key.
+     * @param tertiaryKey The tertiary key, wrapped by {@code oldSecondaryKey}.
+     * @return The tertiary key, wrapped by {@code newSecondaryKey}.
+     * @throws InvalidKeyException if the key is not good for wrapping or unwrapping.
+     * @throws IllegalBlockSizeException if there is an issue wrapping.
+     */
+    public static WrappedKeyProto.WrappedKey rewrap(
+            SecretKey oldSecondaryKey,
+            SecretKey newSecondaryKey,
+            WrappedKeyProto.WrappedKey tertiaryKey)
+            throws InvalidKeyException, IllegalBlockSizeException,
+                    InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+                    NoSuchPaddingException {
+        return wrap(newSecondaryKey, unwrap(oldSecondaryKey, tertiaryKey));
+    }
+
+    private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
+        return Cipher.getInstance(AES_GCM_MODE);
+    }
+
+    // Statics only
+    private KeyWrapUtils() {}
+}
diff --git a/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java b/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
new file mode 100644
index 0000000..b607404
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/KeyWrapUtilsTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.InvalidKeyException;
+
+import javax.crypto.SecretKey;
+
+/** Key wrapping tests */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class KeyWrapUtilsTest {
+    private static final int KEY_SIZE_BITS = 256;
+    private static final int BITS_PER_BYTE = 8;
+    private static final int GCM_NONCE_LENGTH_BYTES = 16;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+
+    /** Test a wrapped key has metadata */
+    @Test
+    public void wrap_addsMetadata() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedKey =
+                KeyWrapUtils.wrap(
+                        /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+        assertThat(wrappedKey.metadata).isNotNull();
+        assertThat(wrappedKey.metadata.type).isEqualTo(WrappedKeyProto.KeyMetadata.AES_256_GCM);
+    }
+
+    /** Test a wrapped key has an algorithm specified */
+    @Test
+    public void wrap_addsWrapAlgorithm() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedKey =
+                KeyWrapUtils.wrap(
+                        /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+        assertThat(wrappedKey.wrapAlgorithm).isEqualTo(WrappedKeyProto.WrappedKey.AES_256_GCM);
+    }
+
+    /** Test a wrapped key haas an nonce of the right length */
+    @Test
+    public void wrap_addsNonceOfAppropriateLength() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedKey =
+                KeyWrapUtils.wrap(
+                        /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+        assertThat(wrappedKey.nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
+    }
+
+    /** Test a wrapped key has a key of the right length */
+    @Test
+    public void wrap_addsTagOfAppropriateLength() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedKey =
+                KeyWrapUtils.wrap(
+                        /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+        assertThat(wrappedKey.key).hasLength(KEY_SIZE_BITS / BITS_PER_BYTE + GCM_TAG_LENGTH_BYTES);
+    }
+
+    /** Ensure a key can be wrapped and unwrapped again */
+    @Test
+    public void unwrap_unwrapsEncryptedKey() throws Exception {
+        SecretKey secondaryKey = generateAesKey();
+        SecretKey tertiaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, tertiaryKey);
+        SecretKey unwrappedKey = KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+        assertThat(unwrappedKey).isEqualTo(tertiaryKey);
+    }
+
+    /** Ensure the unwrap method rejects keys with bad algorithms */
+    @Test(expected = InvalidKeyException.class)
+    public void unwrap_throwsForBadWrapAlgorithm() throws Exception {
+        SecretKey secondaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+        wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.UNKNOWN;
+
+        KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+    }
+
+    /** Ensure the unwrap method rejects metadata indicating the encryption type is unknown */
+    @Test(expected = InvalidKeyException.class)
+    public void unwrap_throwsForBadKeyAlgorithm() throws Exception {
+        SecretKey secondaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+        wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.UNKNOWN;
+
+        KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+    }
+
+    /** Ensure the unwrap method rejects wrapped keys missing the metadata */
+    @Test(expected = InvalidKeyException.class)
+    public void unwrap_throwsForMissingMetadata() throws Exception {
+        SecretKey secondaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(secondaryKey, generateAesKey());
+        wrappedKey.metadata = null;
+
+        KeyWrapUtils.unwrap(secondaryKey, wrappedKey);
+    }
+
+    /** Ensure unwrap rejects invalid secondary keys */
+    @Test(expected = InvalidKeyException.class)
+    public void unwrap_throwsForBadSecondaryKey() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedKey =
+                KeyWrapUtils.wrap(
+                        /*secondaryKey=*/ generateAesKey(), /*tertiaryKey=*/ generateAesKey());
+
+        KeyWrapUtils.unwrap(generateAesKey(), wrappedKey);
+    }
+
+    /** Ensure rewrap can rewrap keys */
+    @Test
+    public void rewrap_canBeUnwrappedWithNewSecondaryKey() throws Exception {
+        SecretKey tertiaryKey = generateAesKey();
+        SecretKey oldSecondaryKey = generateAesKey();
+        SecretKey newSecondaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
+
+        WrappedKeyProto.WrappedKey wrappedWithNew =
+                KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
+
+        assertThat(KeyWrapUtils.unwrap(newSecondaryKey, wrappedWithNew)).isEqualTo(tertiaryKey);
+    }
+
+    /** Ensure rewrap doesn't create something decryptable by an old key */
+    @Test(expected = InvalidKeyException.class)
+    public void rewrap_cannotBeUnwrappedWithOldSecondaryKey() throws Exception {
+        SecretKey tertiaryKey = generateAesKey();
+        SecretKey oldSecondaryKey = generateAesKey();
+        SecretKey newSecondaryKey = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedWithOld = KeyWrapUtils.wrap(oldSecondaryKey, tertiaryKey);
+
+        WrappedKeyProto.WrappedKey wrappedWithNew =
+                KeyWrapUtils.rewrap(oldSecondaryKey, newSecondaryKey, wrappedWithOld);
+
+        KeyWrapUtils.unwrap(oldSecondaryKey, wrappedWithNew);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java
new file mode 100644
index 0000000..436c6de
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKey.java
@@ -0,0 +1,117 @@
+/*
+ * 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.encryption.keys;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+import android.util.Slog;
+
+import java.util.Objects;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Wraps a {@link RecoveryController}'s {@link SecretKey}. These are kept in "AndroidKeyStore" (a
+ * provider for {@link java.security.KeyStore} and {@link javax.crypto.KeyGenerator}. They are also
+ * synced with the recoverable key store, wrapped by the primary key. This allows them to be
+ * recovered on a user's subsequent device through providing their lock screen secret.
+ */
+public class RecoverableKeyStoreSecondaryKey {
+    private static final String TAG = "RecoverableKeyStoreSecondaryKey";
+
+    private final String mAlias;
+    private final SecretKey mSecretKey;
+
+    /**
+     * A new instance.
+     *
+     * @param alias The alias. It is keyed with this in AndroidKeyStore and the recoverable key
+     *     store.
+     * @param secretKey The key.
+     */
+    public RecoverableKeyStoreSecondaryKey(String alias, SecretKey secretKey) {
+        mAlias = Objects.requireNonNull(alias);
+        mSecretKey = Objects.requireNonNull(secretKey);
+    }
+
+    /**
+     * The ID, as stored in the recoverable {@link java.security.KeyStore}, and as used to identify
+     * wrapped tertiary keys on the backup server.
+     */
+    public String getAlias() {
+        return mAlias;
+    }
+
+    /** The secret key, to be used to wrap tertiary keys. */
+    public SecretKey getSecretKey() {
+        return mSecretKey;
+    }
+
+    /**
+     * The status of the key. i.e., whether it's been synced to remote trusted hardware.
+     *
+     * @param context The application context.
+     * @return One of {@link Status#SYNCED}, {@link Status#NOT_SYNCED} or {@link Status#DESTROYED}.
+     */
+    public @Status int getStatus(Context context) {
+        try {
+            return getStatusInternal(context);
+        } catch (InternalRecoveryServiceException e) {
+            Slog.wtf(TAG, "Internal error getting recovery status", e);
+            // Return NOT_SYNCED by default, as we do not want the backups to fail or to repeatedly
+            // attempt to reinitialize.
+            return Status.NOT_SYNCED;
+        }
+    }
+
+    private @Status int getStatusInternal(Context context) throws InternalRecoveryServiceException {
+        int status = RecoveryController.getInstance(context).getRecoveryStatus(mAlias);
+        switch (status) {
+            case RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE:
+                return Status.DESTROYED;
+            case RecoveryController.RECOVERY_STATUS_SYNCED:
+                return Status.SYNCED;
+            case RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS:
+                return Status.NOT_SYNCED;
+            default:
+                // Throw an exception if we encounter a status that doesn't match any of the above.
+                throw new InternalRecoveryServiceException(
+                        "Unexpected status from getRecoveryStatus: " + status);
+        }
+    }
+
+    /** Status of a key in the recoverable key store. */
+    @IntDef({Status.NOT_SYNCED, Status.SYNCED, Status.DESTROYED})
+    public @interface Status {
+        /**
+         * The key has not yet been synced to remote trusted hardware. This may be because the user
+         * has not yet unlocked their device.
+         */
+        int NOT_SYNCED = 1;
+
+        /**
+         * The key has been synced with remote trusted hardware. It should now be recoverable on
+         * another device.
+         */
+        int SYNCED = 2;
+
+        /** The key has been lost forever. This can occur if the user disables their lock screen. */
+        int DESTROYED = 3;
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java
new file mode 100644
index 0000000..c89076b
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManager.java
@@ -0,0 +1,120 @@
+/*
+ * 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.encryption.keys;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.LockScreenRequiredException;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import libcore.util.HexEncoding;
+
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Manages generating, deleting, and retrieving secondary keys through {@link RecoveryController}.
+ *
+ * <p>The recoverable key store will be synced remotely via the {@link RecoveryController}, allowing
+ * recovery of keys on other devices owned by the user.
+ */
+public class RecoverableKeyStoreSecondaryKeyManager {
+    private static final String BACKUP_KEY_ALIAS_PREFIX =
+            "com.android.server.backup/recoverablekeystore/";
+    private static final int BACKUP_KEY_SUFFIX_LENGTH_BITS = 128;
+    private static final int BITS_PER_BYTE = 8;
+
+    /** A new instance. */
+    public static RecoverableKeyStoreSecondaryKeyManager getInstance(Context context) {
+        return new RecoverableKeyStoreSecondaryKeyManager(
+                RecoveryController.getInstance(context), new SecureRandom());
+    }
+
+    private final RecoveryController mRecoveryController;
+    private final SecureRandom mSecureRandom;
+
+    @VisibleForTesting
+    public RecoverableKeyStoreSecondaryKeyManager(
+            RecoveryController recoveryController, SecureRandom secureRandom) {
+        mRecoveryController = recoveryController;
+        mSecureRandom = secureRandom;
+    }
+
+    /**
+     * Generates a new recoverable key using the {@link RecoveryController}.
+     *
+     * @throws InternalRecoveryServiceException if an unexpected error occurred generating the key.
+     * @throws LockScreenRequiredException if the user does not have a lock screen. A lock screen is
+     *     required to generate a recoverable key.
+     */
+    public RecoverableKeyStoreSecondaryKey generate()
+            throws InternalRecoveryServiceException, LockScreenRequiredException,
+                    UnrecoverableKeyException {
+        String alias = generateId();
+        mRecoveryController.generateKey(alias);
+        SecretKey key = (SecretKey) mRecoveryController.getKey(alias);
+        if (key == null) {
+            throw new InternalRecoveryServiceException(
+                    String.format(
+                            "Generated key %s but could not get it back immediately afterwards.",
+                            alias));
+        }
+        return new RecoverableKeyStoreSecondaryKey(alias, key);
+    }
+
+    /**
+     * Removes the secondary key. This means the key will no longer be recoverable.
+     *
+     * @param alias The alias of the key.
+     * @throws InternalRecoveryServiceException if there was a {@link RecoveryController} error.
+     */
+    public void remove(String alias) throws InternalRecoveryServiceException {
+        mRecoveryController.removeKey(alias);
+    }
+
+    /**
+     * Returns the {@link RecoverableKeyStoreSecondaryKey} with {@code alias} if it is in the {@link
+     * RecoveryController}. Otherwise, {@link Optional#empty()}.
+     */
+    public Optional<RecoverableKeyStoreSecondaryKey> get(String alias)
+            throws InternalRecoveryServiceException, UnrecoverableKeyException {
+        SecretKey secretKey = (SecretKey) mRecoveryController.getKey(alias);
+        return Optional.ofNullable(secretKey)
+                .map(key -> new RecoverableKeyStoreSecondaryKey(alias, key));
+    }
+
+    /**
+     * Generates a new key alias. This has more entropy than a UUID - it can be considered
+     * universally unique.
+     */
+    private String generateId() {
+        byte[] id = new byte[BACKUP_KEY_SUFFIX_LENGTH_BITS / BITS_PER_BYTE];
+        mSecureRandom.nextBytes(id);
+        return BACKUP_KEY_ALIAS_PREFIX + HexEncoding.encodeToString(id);
+    }
+
+    /** Constructs a {@link RecoverableKeyStoreSecondaryKeyManager}. */
+    public interface RecoverableKeyStoreSecondaryKeyManagerProvider {
+        /** Returns a newly constructed {@link RecoverableKeyStoreSecondaryKeyManager}. */
+        RecoverableKeyStoreSecondaryKeyManager get();
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java
new file mode 100644
index 0000000..5342efa
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyManagerTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.testing.shadows.ShadowInternalRecoveryServiceException;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.security.SecureRandom;
+import java.util.Optional;
+
+/** Tests for {@link RecoverableKeyStoreSecondaryKeyManager}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowRecoveryController.class, ShadowInternalRecoveryServiceException.class})
+public class RecoverableKeyStoreSecondaryKeyManagerTest {
+    private static final String BACKUP_KEY_ALIAS_PREFIX =
+            "com.android.server.backup/recoverablekeystore/";
+    private static final int BITS_PER_BYTE = 8;
+    private static final int BACKUP_KEY_SUFFIX_LENGTH_BYTES = 128 / BITS_PER_BYTE;
+    private static final int HEX_PER_BYTE = 2;
+    private static final int BACKUP_KEY_ALIAS_LENGTH =
+            BACKUP_KEY_ALIAS_PREFIX.length() + BACKUP_KEY_SUFFIX_LENGTH_BYTES * HEX_PER_BYTE;
+    private static final String NONEXISTENT_KEY_ALIAS = "NONEXISTENT_KEY_ALIAS";
+
+    private RecoverableKeyStoreSecondaryKeyManager mRecoverableKeyStoreSecondaryKeyManager;
+    private Context mContext;
+
+    /** Create a new {@link RecoverableKeyStoreSecondaryKeyManager} to use in tests. */
+    @Before
+    public void setUp() throws Exception {
+        mContext = RuntimeEnvironment.application;
+
+        mRecoverableKeyStoreSecondaryKeyManager =
+                new RecoverableKeyStoreSecondaryKeyManager(
+                        RecoveryController.getInstance(mContext), new SecureRandom());
+    }
+
+    /** Reset the {@link ShadowRecoveryController}. */
+    @After
+    public void tearDown() throws Exception {
+        ShadowRecoveryController.reset();
+    }
+
+    /** The generated key should always have the prefix {@code BACKUP_KEY_ALIAS_PREFIX}. */
+    @Test
+    public void generate_generatesKeyWithExpectedPrefix() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
+
+        assertThat(key.getAlias()).startsWith(BACKUP_KEY_ALIAS_PREFIX);
+    }
+
+    /** The generated key should always have length {@code BACKUP_KEY_ALIAS_LENGTH}. */
+    @Test
+    public void generate_generatesKeyWithExpectedLength() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
+
+        assertThat(key.getAlias()).hasLength(BACKUP_KEY_ALIAS_LENGTH);
+    }
+
+    /** Ensure that hidden API exceptions are rethrown when generating keys. */
+    @Test
+    public void generate_encounteringHiddenApiException_rethrowsException() {
+        ShadowRecoveryController.setThrowsInternalError(true);
+
+        assertThrows(
+                InternalRecoveryServiceException.class,
+                mRecoverableKeyStoreSecondaryKeyManager::generate);
+    }
+
+    /** Ensure that retrieved keys correspond to those generated earlier. */
+    @Test
+    public void get_getsKeyGeneratedByController() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
+
+        Optional<RecoverableKeyStoreSecondaryKey> retrievedKey =
+                mRecoverableKeyStoreSecondaryKeyManager.get(key.getAlias());
+
+        assertThat(retrievedKey.isPresent()).isTrue();
+        assertThat(retrievedKey.get().getAlias()).isEqualTo(key.getAlias());
+        assertThat(retrievedKey.get().getSecretKey()).isEqualTo(key.getSecretKey());
+    }
+
+    /**
+     * Ensure that a call to {@link RecoverableKeyStoreSecondaryKeyManager#get(java.lang.String)}
+     * for nonexistent aliases returns an emtpy {@link Optional}.
+     */
+    @Test
+    public void get_forNonExistentKey_returnsEmptyOptional() throws Exception {
+        Optional<RecoverableKeyStoreSecondaryKey> retrievedKey =
+                mRecoverableKeyStoreSecondaryKeyManager.get(NONEXISTENT_KEY_ALIAS);
+
+        assertThat(retrievedKey.isPresent()).isFalse();
+    }
+
+    /**
+     * Ensure that exceptions occurring during {@link
+     * RecoverableKeyStoreSecondaryKeyManager#get(java.lang.String)} are not rethrown.
+     */
+    @Test
+    public void get_encounteringInternalException_doesNotPropagateException() throws Exception {
+        ShadowRecoveryController.setThrowsInternalError(true);
+
+        // Should not throw exception
+        mRecoverableKeyStoreSecondaryKeyManager.get(NONEXISTENT_KEY_ALIAS);
+    }
+
+    /** Ensure that keys are correctly removed from the store. */
+    @Test
+    public void remove_removesKeyFromRecoverableStore() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mRecoverableKeyStoreSecondaryKeyManager.generate();
+
+        mRecoverableKeyStoreSecondaryKeyManager.remove(key.getAlias());
+
+        assertThat(RecoveryController.getInstance(mContext).getAliases())
+                .doesNotContain(key.getAlias());
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java
new file mode 100644
index 0000000..89977f8
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RecoverableKeyStoreSecondaryKeyTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey.Status;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.testing.shadows.ShadowInternalRecoveryServiceException;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import javax.crypto.SecretKey;
+
+/** Tests for {@link RecoverableKeyStoreSecondaryKey}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowRecoveryController.class, ShadowInternalRecoveryServiceException.class})
+public class RecoverableKeyStoreSecondaryKeyTest {
+    private static final String TEST_ALIAS = "test";
+    private static final int NONEXISTENT_STATUS_CODE = 42;
+
+    private RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    private SecretKey mGeneratedSecretKey;
+    private Context mContext;
+
+    /** Instantiate a {@link RecoverableKeyStoreSecondaryKey} to use in tests. */
+    @Before
+    public void setUp() throws Exception {
+        mContext = RuntimeEnvironment.application;
+        mGeneratedSecretKey = CryptoTestUtils.generateAesKey();
+        mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_ALIAS, mGeneratedSecretKey);
+    }
+
+    /** Reset the {@link ShadowRecoveryController}. */
+    @After
+    public void tearDown() throws Exception {
+        ShadowRecoveryController.reset();
+    }
+
+    /**
+     * Checks that {@link RecoverableKeyStoreSecondaryKey#getAlias()} returns the value supplied in
+     * the constructor.
+     */
+    @Test
+    public void getAlias() {
+        String alias = mSecondaryKey.getAlias();
+
+        assertThat(alias).isEqualTo(TEST_ALIAS);
+    }
+
+    /**
+     * Checks that {@link RecoverableKeyStoreSecondaryKey#getSecretKey()} returns the value supplied
+     * in the constructor.
+     */
+    @Test
+    public void getSecretKey() {
+        SecretKey secretKey = mSecondaryKey.getSecretKey();
+
+        assertThat(secretKey).isEqualTo(mGeneratedSecretKey);
+    }
+
+    /**
+     * Checks that passing a secret key that is null to the constructor throws an exception.
+     */
+    @Test
+    public void constructor_withNullSecretKey_throwsNullPointerException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new RecoverableKeyStoreSecondaryKey(TEST_ALIAS, null));
+    }
+
+    /**
+     * Checks that passing an alias that is null to the constructor throws an exception.
+     */
+    @Test
+    public void constructor_withNullAlias_throwsNullPointerException() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new RecoverableKeyStoreSecondaryKey(null, mGeneratedSecretKey));
+    }
+
+    /** Checks that the synced status is returned correctly. */
+    @Test
+    public void getStatus_whenSynced_returnsSynced() throws Exception {
+        setStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+
+        int status = mSecondaryKey.getStatus(mContext);
+
+        assertThat(status).isEqualTo(Status.SYNCED);
+    }
+
+    /** Checks that the in progress sync status is returned correctly. */
+    @Test
+    public void getStatus_whenNotSynced_returnsNotSynced() throws Exception {
+        setStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+
+        int status = mSecondaryKey.getStatus(mContext);
+
+        assertThat(status).isEqualTo(Status.NOT_SYNCED);
+    }
+
+    /** Checks that the failure status is returned correctly. */
+    @Test
+    public void getStatus_onPermanentFailure_returnsDestroyed() throws Exception {
+        setStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
+
+        int status = mSecondaryKey.getStatus(mContext);
+
+        assertThat(status).isEqualTo(Status.DESTROYED);
+    }
+
+    /** Checks that an unknown status results in {@code NOT_SYNCED} being returned. */
+    @Test
+    public void getStatus_forUnknownStatusCode_returnsNotSynced() throws Exception {
+        setStatus(NONEXISTENT_STATUS_CODE);
+
+        int status = mSecondaryKey.getStatus(mContext);
+
+        assertThat(status).isEqualTo(Status.NOT_SYNCED);
+    }
+
+    /** Checks that an internal error results in {@code NOT_SYNCED} being returned. */
+    @Test
+    public void getStatus_onInternalError_returnsNotSynced() throws Exception {
+        ShadowRecoveryController.setThrowsInternalError(true);
+
+        int status = mSecondaryKey.getStatus(mContext);
+
+        assertThat(status).isEqualTo(Status.NOT_SYNCED);
+    }
+
+    private void setStatus(int status) throws Exception {
+        ShadowRecoveryController.setRecoveryStatus(TEST_ALIAS, status);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java b/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java
new file mode 100644
index 0000000..6fb958b
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RestoreKeyFetcher.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.util.Optional;
+
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/** Fetches the secondary key and uses it to unwrap the tertiary key during restore. */
+public class RestoreKeyFetcher {
+
+    /**
+     * Retrieves the secondary key with the given alias and uses it to unwrap the given wrapped
+     * tertiary key.
+     *
+     * @param secondaryKeyManagerProvider Provider which creates {@link
+     *     RecoverableKeyStoreSecondaryKeyManager}
+     * @param secondaryKeyAlias Alias of the secondary key used to wrap the tertiary key
+     * @param wrappedTertiaryKey Tertiary key wrapped with the secondary key above
+     * @return The unwrapped tertiary key
+     */
+    public static SecretKey unwrapTertiaryKey(
+            RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
+            String secondaryKeyAlias,
+            WrappedKeyProto.WrappedKey wrappedTertiaryKey)
+            throws KeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+                    NoSuchPaddingException {
+        Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
+                getSecondaryKey(secondaryKeyManagerProvider, secondaryKeyAlias);
+        if (!secondaryKey.isPresent()) {
+            throw new KeyException("No key:" + secondaryKeyAlias);
+        }
+
+        return KeyWrapUtils.unwrap(secondaryKey.get().getSecretKey(), wrappedTertiaryKey);
+    }
+
+    private static Optional<RecoverableKeyStoreSecondaryKey> getSecondaryKey(
+            RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
+            String secondaryKeyAlias)
+            throws KeyException {
+        try {
+            return secondaryKeyManagerProvider.get().get(secondaryKeyAlias);
+        } catch (InternalRecoveryServiceException | UnrecoverableKeyException e) {
+            throw new KeyException("Could not retrieve key:" + secondaryKeyAlias, e);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java b/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java
new file mode 100644
index 0000000..004f809
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/RestoreKeyFetcherTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.InvalidKeyException;
+import java.security.KeyException;
+import java.security.SecureRandom;
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Test the restore key fetcher */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class RestoreKeyFetcherTest {
+
+    private static final String KEY_GENERATOR_ALGORITHM = "AES";
+
+    private static final String TEST_SECONDARY_KEY_ALIAS = "test_2ndary_key";
+    private static final byte[] TEST_SECONDARY_KEY_BYTES = new byte[256 / Byte.SIZE];
+
+    @Mock private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+
+    /** Initialise the mocks **/
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /** Ensure the unwrap method works as expected */
+    @Test
+    public void unwrapTertiaryKey_returnsUnwrappedKey() throws Exception {
+        RecoverableKeyStoreSecondaryKey secondaryKey = createSecondaryKey();
+        SecretKey tertiaryKey = createTertiaryKey();
+        WrappedKeyProto.WrappedKey wrappedTertiaryKey =
+                KeyWrapUtils.wrap(secondaryKey.getSecretKey(), tertiaryKey);
+        when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS))
+                .thenReturn(Optional.of(secondaryKey));
+
+        SecretKey actualTertiaryKey =
+                RestoreKeyFetcher.unwrapTertiaryKey(
+                        () -> mSecondaryKeyManager,
+                        TEST_SECONDARY_KEY_ALIAS,
+                        wrappedTertiaryKey);
+
+        assertThat(actualTertiaryKey).isEqualTo(tertiaryKey);
+    }
+
+    /** Ensure that missing secondary keys are detected and an appropriate exception is thrown */
+    @Test
+    public void unwrapTertiaryKey_missingSecondaryKey_throwsSpecificException() throws Exception {
+        WrappedKeyProto.WrappedKey wrappedTertiaryKey =
+                KeyWrapUtils.wrap(createSecondaryKey().getSecretKey(), createTertiaryKey());
+        when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS)).thenReturn(Optional.empty());
+
+        assertThrows(
+                KeyException.class,
+                () ->
+                        RestoreKeyFetcher.unwrapTertiaryKey(
+                                () -> mSecondaryKeyManager,
+                                TEST_SECONDARY_KEY_ALIAS,
+                                wrappedTertiaryKey));
+    }
+
+    /** Ensure that invalid secondary keys are detected and an appropriate exception is thrown */
+    @Test
+    public void unwrapTertiaryKey_badSecondaryKey_throws() throws Exception {
+        RecoverableKeyStoreSecondaryKey badSecondaryKey =
+                new RecoverableKeyStoreSecondaryKey(
+                        TEST_SECONDARY_KEY_ALIAS,
+                        new SecretKeySpec(new byte[] {0, 1}, KEY_GENERATOR_ALGORITHM));
+
+        WrappedKeyProto.WrappedKey wrappedTertiaryKey =
+                KeyWrapUtils.wrap(createSecondaryKey().getSecretKey(), createTertiaryKey());
+        when(mSecondaryKeyManager.get(TEST_SECONDARY_KEY_ALIAS))
+                .thenReturn(Optional.of(badSecondaryKey));
+
+        assertThrows(
+                InvalidKeyException.class,
+                () ->
+                        RestoreKeyFetcher.unwrapTertiaryKey(
+                                () -> mSecondaryKeyManager,
+                                TEST_SECONDARY_KEY_ALIAS,
+                                wrappedTertiaryKey));
+    }
+
+    private static RecoverableKeyStoreSecondaryKey createSecondaryKey() {
+        return new RecoverableKeyStoreSecondaryKey(
+                TEST_SECONDARY_KEY_ALIAS,
+                new SecretKeySpec(TEST_SECONDARY_KEY_BYTES, KEY_GENERATOR_ALGORITHM));
+    }
+
+    private static SecretKey createTertiaryKey() {
+        return new TertiaryKeyGenerator(new SecureRandom(new byte[] {0})).generate();
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java b/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java
new file mode 100644
index 0000000..91b57cf
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;
+
+import java.io.File;
+import java.time.Clock;
+import java.util.Optional;
+
+/**
+ * Helps schedule rotations of secondary keys.
+ *
+ * <p>TODO(b/72028016) Replace with a job.
+ */
+public class SecondaryKeyRotationScheduler {
+
+    private static final String TAG = "SecondaryKeyRotationScheduler";
+    private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";
+
+    private final Context mContext;
+    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+    private final CryptoSettings mCryptoSettings;
+    private final Clock mClock;
+
+    public SecondaryKeyRotationScheduler(
+            Context context,
+            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
+            CryptoSettings cryptoSettings,
+            Clock clock) {
+        mContext = context;
+        mCryptoSettings = cryptoSettings;
+        mClock = clock;
+        mSecondaryKeyManager = secondaryKeyManager;
+    }
+
+    /**
+     * Returns {@code true} if a sentinel file for forcing secondary key rotation is present. This
+     * is only for testing purposes.
+     */
+    private boolean isForceRotationTestSentinelPresent() {
+        File file = new File(mContext.getFilesDir(), SENTINEL_FILE_PATH);
+        if (file.exists()) {
+            file.delete();
+            return true;
+        }
+        return false;
+    }
+
+    /** Start the key rotation task if it's time to do so */
+    public void startRotationIfScheduled() {
+        if (isForceRotationTestSentinelPresent()) {
+            Slog.i(TAG, "Found force flag for secondary rotation. Starting now.");
+            startRotation();
+            return;
+        }
+
+        Optional<Long> maybeLastRotated = mCryptoSettings.getSecondaryLastRotated();
+        if (!maybeLastRotated.isPresent()) {
+            Slog.v(TAG, "No previous rotation, scheduling from now.");
+            scheduleRotationFromNow();
+            return;
+        }
+
+        long lastRotated = maybeLastRotated.get();
+        long now = mClock.millis();
+
+        if (lastRotated > now) {
+            Slog.i(TAG, "Last rotation was in the future. Clock must have changed. Rotate now.");
+            startRotation();
+            return;
+        }
+
+        long millisSinceLastRotation = now - lastRotated;
+        long rotationInterval = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
+        if (millisSinceLastRotation >= rotationInterval) {
+            Slog.i(
+                    TAG,
+                    "Last rotation was more than "
+                            + rotationInterval
+                            + "ms ("
+                            + millisSinceLastRotation
+                            + "ms) in the past. Rotate now.");
+            startRotation();
+        }
+
+        Slog.v(TAG, "No rotation required, last " + lastRotated + ".");
+    }
+
+    private void startRotation() {
+        scheduleRotationFromNow();
+        new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager).run();
+    }
+
+    private void scheduleRotationFromNow() {
+        mCryptoSettings.setSecondaryLastRotated(mClock.millis());
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java b/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java
new file mode 100644
index 0000000..c31d19d
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+import java.io.File;
+import java.time.Clock;
+
+@Config(shadows = SecondaryKeyRotationSchedulerTest.ShadowStartSecondaryKeyRotationTask.class)
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class SecondaryKeyRotationSchedulerTest {
+    private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";
+
+    @Mock private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+    @Mock private Clock mClock;
+
+    private CryptoSettings mCryptoSettings;
+    private SecondaryKeyRotationScheduler mScheduler;
+    private long mRotationIntervalMillis;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context application = ApplicationProvider.getApplicationContext();
+
+        mCryptoSettings = CryptoSettings.getInstanceForTesting(application);
+        mRotationIntervalMillis = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
+
+        mScheduler =
+                new SecondaryKeyRotationScheduler(
+                        application, mSecondaryKeyManager, mCryptoSettings, mClock);
+        ShadowStartSecondaryKeyRotationTask.reset();
+    }
+
+    @Test
+    public void startRotationIfScheduled_rotatesIfRotationWasFarEnoughInThePast() {
+        long lastRotated = 100009;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(lastRotated + mRotationIntervalMillis);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
+    }
+
+    @Test
+    public void startRotationIfScheduled_setsNewRotationTimeIfRotationWasFarEnoughInThePast() {
+        long lastRotated = 100009;
+        long now = lastRotated + mRotationIntervalMillis;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(now);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
+    }
+
+    @Test
+    public void startRotationIfScheduled_rotatesIfClockHasChanged() {
+        long lastRotated = 100009;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(lastRotated - 1);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
+    }
+
+    @Test
+    public void startRotationIfScheduled_rotatesIfSentinelFileIsPresent() throws Exception {
+        File file = new File(RuntimeEnvironment.application.getFilesDir(), SENTINEL_FILE_PATH);
+        file.createNewFile();
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
+    }
+
+    @Test
+    public void startRotationIfScheduled_setsNextRotationIfClockHasChanged() {
+        long lastRotated = 100009;
+        long now = lastRotated - 1;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(now);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
+    }
+
+    @Test
+    public void startRotationIfScheduled_doesNothingIfRotationWasRecentEnough() {
+        long lastRotated = 100009;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(lastRotated + mRotationIntervalMillis - 1);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isFalse();
+    }
+
+    @Test
+    public void startRotationIfScheduled_doesNotSetRotationTimeIfRotationWasRecentEnough() {
+        long lastRotated = 100009;
+        mCryptoSettings.setSecondaryLastRotated(lastRotated);
+        setNow(lastRotated + mRotationIntervalMillis - 1);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(lastRotated);
+    }
+
+    @Test
+    public void startRotationIfScheduled_setsLastRotatedToNowIfNeverRotated() {
+        long now = 13295436;
+        setNow(now);
+
+        mScheduler.startRotationIfScheduled();
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
+    }
+
+    private void setNow(long timestamp) {
+        when(mClock.millis()).thenReturn(timestamp);
+    }
+
+    @Implements(StartSecondaryKeyRotationTask.class)
+    public static class ShadowStartSecondaryKeyRotationTask {
+        private static boolean sRan = false;
+
+        @Implementation
+        public void run() {
+            sRan = true;
+        }
+
+        @Resetter
+        public static void reset() {
+            sRan = false;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java b/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
new file mode 100644
index 0000000..a425c72
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
@@ -0,0 +1,47 @@
+/*
+ * 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.encryption.keys;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/** 256-bit AES key generator. Each app should have its own separate AES key. */
+public class TertiaryKeyGenerator {
+    private static final int KEY_SIZE_BITS = 256;
+    private static final String KEY_ALGORITHM = "AES";
+
+    private final KeyGenerator mKeyGenerator;
+
+    /** New instance generating keys using {@code secureRandom}. */
+    public TertiaryKeyGenerator(SecureRandom secureRandom) {
+        try {
+            mKeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+            mKeyGenerator.init(KEY_SIZE_BITS, secureRandom);
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError(
+                    "Impossible condition: JCE thinks it does not support AES.", e);
+        }
+    }
+
+    /** Generates a new random AES key. */
+    public SecretKey generate() {
+        return mKeyGenerator.generateKey();
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java
new file mode 100644
index 0000000..48216f8
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyGeneratorTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.SecureRandom;
+
+import javax.crypto.SecretKey;
+
+/** Tests for {@link TertiaryKeyGenerator}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TertiaryKeyGeneratorTest {
+    private static final String KEY_ALGORITHM = "AES";
+    private static final int KEY_SIZE_BITS = 256;
+
+    private TertiaryKeyGenerator mTertiaryKeyGenerator;
+
+    /** Instantiate a new {@link TertiaryKeyGenerator} for use in tests. */
+    @Before
+    public void setUp() {
+        mTertiaryKeyGenerator = new TertiaryKeyGenerator(new SecureRandom());
+    }
+
+    /** Generated keys should be AES keys. */
+    @Test
+    public void generate_generatesAESKeys() {
+        SecretKey secretKey = mTertiaryKeyGenerator.generate();
+
+        assertThat(secretKey.getAlgorithm()).isEqualTo(KEY_ALGORITHM);
+    }
+
+    /** Generated keys should be 256 bits in size. */
+    @Test
+    public void generate_generates256BitKeys() {
+        SecretKey secretKey = mTertiaryKeyGenerator.generate();
+
+        assertThat(secretKey.getEncoded()).hasLength(KEY_SIZE_BITS / 8);
+    }
+
+    /**
+     * Subsequent calls to {@link TertiaryKeyGenerator#generate()} should generate different keys.
+     */
+    @Test
+    public void generate_generatesNewKeys() {
+        SecretKey key1 = mTertiaryKeyGenerator.generate();
+        SecretKey key2 = mTertiaryKeyGenerator.generate();
+
+        assertThat(key1).isNotEqualTo(key2);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyManager.java b/com/android/server/backup/encryption/keys/TertiaryKeyManager.java
new file mode 100644
index 0000000..a783579
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Optional;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Gets the correct tertiary key to use during a backup, rotating it if required.
+ *
+ * <p>Calling any method on this class will count a incremental backup against the app, and the key
+ * will be rotated if required.
+ */
+public class TertiaryKeyManager {
+
+    private static final String TAG = "TertiaryKeyMgr";
+
+    private final TertiaryKeyStore mKeyStore;
+    private final TertiaryKeyGenerator mKeyGenerator;
+    private final TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler;
+    private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    private final String mPackageName;
+
+    private boolean mKeyRotated;
+    @Nullable private SecretKey mTertiaryKey;
+
+    public TertiaryKeyManager(
+            Context context,
+            SecureRandom secureRandom,
+            TertiaryKeyRotationScheduler tertiaryKeyRotationScheduler,
+            RecoverableKeyStoreSecondaryKey secondaryKey,
+            String packageName) {
+        mSecondaryKey = secondaryKey;
+        mPackageName = packageName;
+        mKeyGenerator = new TertiaryKeyGenerator(secureRandom);
+        mKeyStore = TertiaryKeyStore.newInstance(context, secondaryKey);
+        mTertiaryKeyRotationScheduler = tertiaryKeyRotationScheduler;
+    }
+
+    /**
+     * Returns either the previously used tertiary key, or a new tertiary key if there was no
+     * previous key or it needed to be rotated.
+     */
+    public SecretKey getKey()
+            throws InvalidKeyException, IOException, IllegalBlockSizeException,
+                    NoSuchPaddingException, NoSuchAlgorithmException,
+                    InvalidAlgorithmParameterException {
+        init();
+        return mTertiaryKey;
+    }
+
+    /** Returns the key given by {@link #getKey()} wrapped by the secondary key. */
+    public WrappedKeyProto.WrappedKey getWrappedKey()
+            throws InvalidKeyException, IOException, IllegalBlockSizeException,
+                    NoSuchPaddingException, NoSuchAlgorithmException,
+                    InvalidAlgorithmParameterException {
+        init();
+        return KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), mTertiaryKey);
+    }
+
+    /**
+     * Returns {@code true} if a new tertiary key was generated at the start of this session,
+     * otherwise {@code false}.
+     */
+    public boolean wasKeyRotated()
+            throws InvalidKeyException, IllegalBlockSizeException, IOException,
+                    NoSuchPaddingException, NoSuchAlgorithmException,
+                    InvalidAlgorithmParameterException {
+        init();
+        return mKeyRotated;
+    }
+
+    private void init()
+            throws IllegalBlockSizeException, InvalidKeyException, IOException,
+                    NoSuchAlgorithmException, NoSuchPaddingException,
+                    InvalidAlgorithmParameterException {
+        if (mTertiaryKey != null) {
+            return;
+        }
+
+        Optional<SecretKey> key = getExistingKeyIfNotRotated();
+
+        if (!key.isPresent()) {
+            Slog.d(TAG, "Generating new tertiary key for " + mPackageName);
+
+            key = Optional.of(mKeyGenerator.generate());
+            mKeyRotated = true;
+            mTertiaryKeyRotationScheduler.recordKeyRotation(mPackageName);
+            mKeyStore.save(mPackageName, key.get());
+        }
+
+        mTertiaryKey = key.get();
+
+        mTertiaryKeyRotationScheduler.recordBackup(mPackageName);
+    }
+
+    private Optional<SecretKey> getExistingKeyIfNotRotated()
+            throws InvalidKeyException, IOException, InvalidAlgorithmParameterException,
+                    NoSuchAlgorithmException, NoSuchPaddingException {
+        if (mTertiaryKeyRotationScheduler.isKeyRotationDue(mPackageName)) {
+            Slog.i(TAG, "Tertiary key rotation was required for " + mPackageName);
+            return Optional.empty();
+        } else {
+            Slog.i(TAG, "Tertiary key rotation was not required");
+            return mKeyStore.load(mPackageName);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java
new file mode 100644
index 0000000..1ed8309
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.security.SecureRandom;
+
+import javax.crypto.SecretKey;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowRecoveryController.class)
+public class TertiaryKeyManagerTest {
+
+    private static final String TEST_PACKAGE_1 = "com.example.app1";
+    private static final String TEST_PACKAGE_2 = "com.example.app2";
+
+    private SecureRandom mSecureRandom;
+    private RecoverableKeyStoreSecondaryKey mSecondaryKey;
+
+    @Mock private TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mSecureRandom = new SecureRandom();
+        mSecondaryKey =
+                new RecoverableKeyStoreSecondaryKeyManager(
+                                RecoveryController.getInstance(application), mSecureRandom)
+                        .generate();
+        ShadowRecoveryController.reset();
+    }
+
+    private TertiaryKeyManager createNewManager(String packageName) {
+        return new TertiaryKeyManager(
+                application,
+                mSecureRandom,
+                mTertiaryKeyRotationScheduler,
+                mSecondaryKey,
+                packageName);
+    }
+
+    @Test
+    public void getKey_noExistingKey_returnsNewKey() throws Exception {
+        assertThat(createNewManager(TEST_PACKAGE_1).getKey()).isNotNull();
+    }
+
+    @Test
+    public void getKey_noExistingKey_recordsIncrementalBackup() throws Exception {
+        createNewManager(TEST_PACKAGE_1).getKey();
+        verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
+    }
+
+    @Test
+    public void getKey_existingKey_returnsExistingKey() throws Exception {
+        TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
+        SecretKey existingKey = manager.getKey();
+
+        assertThat(manager.getKey()).isEqualTo(existingKey);
+    }
+
+    @Test
+    public void getKey_existingKey_recordsBackupButNotRotation() throws Exception {
+        createNewManager(TEST_PACKAGE_1).getKey();
+        reset(mTertiaryKeyRotationScheduler);
+
+        createNewManager(TEST_PACKAGE_1).getKey();
+
+        verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
+        verify(mTertiaryKeyRotationScheduler, never()).recordKeyRotation(any());
+    }
+
+    @Test
+    public void getKey_existingKeyButRotationRequired_returnsNewKey() throws Exception {
+        SecretKey firstKey = createNewManager(TEST_PACKAGE_1).getKey();
+        when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true);
+
+        SecretKey secondKey = createNewManager(TEST_PACKAGE_1).getKey();
+
+        assertThat(secondKey).isNotEqualTo(firstKey);
+    }
+
+    @Test
+    public void getKey_existingKeyButRotationRequired_recordsKeyRotationAndBackup()
+            throws Exception {
+        when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true);
+        createNewManager(TEST_PACKAGE_1).getKey();
+
+        InOrder inOrder = inOrder(mTertiaryKeyRotationScheduler);
+        inOrder.verify(mTertiaryKeyRotationScheduler).recordKeyRotation(TEST_PACKAGE_1);
+        inOrder.verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1);
+    }
+
+    @Test
+    public void getKey_twoApps_returnsDifferentKeys() throws Exception {
+        TertiaryKeyManager firstManager = createNewManager(TEST_PACKAGE_1);
+        TertiaryKeyManager secondManager = createNewManager(TEST_PACKAGE_2);
+        SecretKey firstKey = firstManager.getKey();
+
+        assertThat(secondManager.getKey()).isNotEqualTo(firstKey);
+    }
+
+    @Test
+    public void getWrappedKey_noExistingKey_returnsWrappedNewKey() throws Exception {
+        TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
+        SecretKey unwrappedKey = manager.getKey();
+        WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey();
+
+        SecretKey expectedUnwrappedKey =
+                KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey);
+        assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey);
+    }
+
+    @Test
+    public void getWrappedKey_existingKey_returnsWrappedExistingKey() throws Exception {
+        TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
+        WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey();
+        SecretKey unwrappedKey = manager.getKey();
+
+        SecretKey expectedUnwrappedKey =
+                KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey);
+        assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey);
+    }
+
+    @Test
+    public void wasKeyRotated_noExistingKey_returnsTrue() throws Exception {
+        TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1);
+        assertThat(manager.wasKeyRotated()).isTrue();
+    }
+
+    @Test
+    public void wasKeyRotated_existingKey_returnsFalse() throws Exception {
+        createNewManager(TEST_PACKAGE_1).getKey();
+        assertThat(createNewManager(TEST_PACKAGE_1).wasKeyRotated()).isFalse();
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
new file mode 100644
index 0000000..f16a68d
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Schedules tertiary key rotations in a staggered fashion.
+ *
+ * <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered
+ * over a period of time, through restricting the number of rotations allowed in a 24-hour window.
+ * This will causes the apps to enter a staggered cycle of regular rotations.
+ *
+ * <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to
+ * ensure that scheduler information is committed to disk, so that it is available after the user
+ * turns their device off and on. This ought to be fine as
+ *
+ * <ul>
+ *   <li>It will be invoked before a backup, so should never be invoked on the UI thread
+ *   <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not
+ *       writing tiny amounts of data to disk.
+ * </ul>
+ */
+public class TertiaryKeyRotationScheduler {
+    /** Default number of key rotations allowed within 24 hours. */
+    private static final int KEY_ROTATION_LIMIT = 2;
+
+    /** A new instance, using {@code context} to determine where to store state. */
+    public static TertiaryKeyRotationScheduler getInstance(Context context) {
+        TertiaryKeyRotationWindowedCount windowedCount =
+                TertiaryKeyRotationWindowedCount.getInstance(context);
+        TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context);
+        return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT);
+    }
+
+    private final TertiaryKeyRotationTracker mTracker;
+    private final TertiaryKeyRotationWindowedCount mWindowedCount;
+    private final int mMaximumRotationsPerWindow;
+
+    /**
+     * A new instance.
+     *
+     * @param tracker Tracks how many times each application has backed up.
+     * @param windowedCount Tracks how many rotations have happened in the last 24 hours.
+     * @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours.
+     */
+    @VisibleForTesting
+    TertiaryKeyRotationScheduler(
+            TertiaryKeyRotationTracker tracker,
+            TertiaryKeyRotationWindowedCount windowedCount,
+            int maximumRotationsPerWindow) {
+        mTracker = tracker;
+        mWindowedCount = windowedCount;
+        mMaximumRotationsPerWindow = maximumRotationsPerWindow;
+    }
+
+    /**
+     * Returns {@code true} if the app with {@code packageName} is due having its key rotated.
+     *
+     * <p>This ought to be queried before backing up an app, to determine whether to do an
+     * incremental backup or a full backup. (A full backup forces key rotation.)
+     */
+    public boolean isKeyRotationDue(String packageName) {
+        if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) {
+            return false;
+        }
+        return mTracker.isKeyRotationDue(packageName);
+    }
+
+    /**
+     * Records that a backup happened for the app with the given {@code packageName}.
+     *
+     * <p>Each backup brings the app closer to the point at which a key rotation is due.
+     */
+    public void recordBackup(String packageName) {
+        mTracker.recordBackup(packageName);
+    }
+
+    /**
+     * Records a key rotation happened for the app with the given {@code packageName}.
+     *
+     * <p>This resets the countdown until the next key rotation is due.
+     */
+    public void recordKeyRotation(String packageName) {
+        mTracker.resetCountdown(packageName);
+        mWindowedCount.record();
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
new file mode 100644
index 0000000..dfc7e2b
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for the tertiary key rotation scheduler */
+@RunWith(RobolectricTestRunner.class)
+public final class TertiaryKeyRotationSchedulerTest {
+
+    private static final int MAXIMUM_ROTATIONS_PER_WINDOW = 2;
+    private static final int MAX_BACKUPS_TILL_ROTATION = 31;
+    private static final String SHARED_PREFS_NAME = "tertiary_key_rotation_tracker";
+    private static final String PACKAGE_1 = "com.android.example1";
+    private static final String PACKAGE_2 = "com.android.example2";
+
+    @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Mock private Clock mClock;
+
+    private File mFile;
+    private TertiaryKeyRotationScheduler mScheduler;
+
+    /** Setup the scheduler for test */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mFile = temporaryFolder.newFile();
+        mScheduler =
+                new TertiaryKeyRotationScheduler(
+                        new TertiaryKeyRotationTracker(
+                                application.getSharedPreferences(
+                                        SHARED_PREFS_NAME, Context.MODE_PRIVATE),
+                                MAX_BACKUPS_TILL_ROTATION),
+                        new TertiaryKeyRotationWindowedCount(mFile, mClock),
+                        MAXIMUM_ROTATIONS_PER_WINDOW);
+    }
+
+    /** Test we don't trigger a rotation straight off */
+    @Test
+    public void isKeyRotationDue_isFalseInitially() {
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    /** Test we don't prematurely trigger a rotation */
+    @Test
+    public void isKeyRotationDue_isFalseAfterInsufficientBackups() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION - 1);
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    /** Test we do trigger a backup */
+    @Test
+    public void isKeyRotationDue_isTrueAfterEnoughBackups() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+    }
+
+    /** Test rotation will occur if the quota allows */
+    @Test
+    public void isKeyRotationDue_isTrueIfRotationQuotaRemainsInWindow() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+        mScheduler.recordKeyRotation(PACKAGE_2);
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+    }
+
+    /** Test rotation is blocked if the quota has been exhausted */
+    @Test
+    public void isKeyRotationDue_isFalseIfEnoughRotationsHaveHappenedInWindow() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+        mScheduler.recordKeyRotation(PACKAGE_2);
+        mScheduler.recordKeyRotation(PACKAGE_2);
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    /** Test rotation is due after one window has passed */
+    @Test
+    public void isKeyRotationDue_isTrueAfterAWholeWindowHasPassed() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+        mScheduler.recordKeyRotation(PACKAGE_2);
+        mScheduler.recordKeyRotation(PACKAGE_2);
+        setTimeMillis(TimeUnit.HOURS.toMillis(24));
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+    }
+
+    /** Test the rotation state changes after a rotation */
+    @Test
+    public void isKeyRotationDue_isFalseAfterRotation() {
+        simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+        mScheduler.recordKeyRotation(PACKAGE_1);
+        assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    /** Test the rate limiting for a given window */
+    @Test
+    public void isKeyRotationDue_neverAllowsMoreThanInWindow() {
+        List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
+
+        // simulate backups of all apps each night
+        for (int i = 0; i < 300; i++) {
+            setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
+            int rotationsThisNight = 0;
+            for (String app : apps) {
+                if (mScheduler.isKeyRotationDue(app)) {
+                    rotationsThisNight++;
+                    mScheduler.recordKeyRotation(app);
+                } else {
+                    mScheduler.recordBackup(app);
+                }
+            }
+            assertThat(rotationsThisNight).isAtMost(MAXIMUM_ROTATIONS_PER_WINDOW);
+        }
+    }
+
+    /** Test that backups are staggered over the window */
+    @Test
+    public void isKeyRotationDue_naturallyStaggersBackupsOverTime() {
+        List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
+
+        HashMap<String, ArrayList<Integer>> rotationDays = new HashMap<>();
+        for (String app : apps) {
+            rotationDays.put(app, new ArrayList<>());
+        }
+
+        // simulate backups of all apps each night
+        for (int i = 0; i < 300; i++) {
+            setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
+            for (String app : apps) {
+                if (mScheduler.isKeyRotationDue(app)) {
+                    rotationDays.get(app).add(i);
+                    mScheduler.recordKeyRotation(app);
+                } else {
+                    mScheduler.recordBackup(app);
+                }
+            }
+        }
+
+        for (String app : apps) {
+            List<Integer> days = rotationDays.get(app);
+            for (int i = 1; i < days.size(); i++) {
+                assertThat(days.get(i) - days.get(i - 1)).isEqualTo(MAX_BACKUPS_TILL_ROTATION + 1);
+            }
+        }
+    }
+
+    private ArrayList<String> makeTestApps(int n) {
+        ArrayList<String> apps = new ArrayList<>();
+        for (int i = 0; i < n; i++) {
+            apps.add(String.format(Locale.US, "com.android.app%d", i));
+        }
+        return apps;
+    }
+
+    private void simulateBackups(int numberOfBackups) {
+        while (numberOfBackups > 0) {
+            mScheduler.recordBackup(PACKAGE_1);
+            numberOfBackups--;
+        }
+    }
+
+    private void setTimeMillis(long timeMillis) {
+        when(mClock.millis()).thenReturn(timeMillis);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
new file mode 100644
index 0000000..1a281e7
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+/**
+ * Tracks when a tertiary key rotation is due.
+ *
+ * <p>After a certain number of incremental backups, the device schedules a full backup, which will
+ * generate a new encryption key, effecting a key rotation. We should do this on a regular basis so
+ * that if a key does become compromised it has limited value to the attacker.
+ *
+ * <p>No additional synchronization of this class is provided. Only one instance should be used at
+ * any time. This should be fine as there should be no parallelism in backups.
+ */
+public class TertiaryKeyRotationTracker {
+    private static final int MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION = 31;
+    private static final String SHARED_PREFERENCES_NAME = "tertiary_key_rotation_tracker";
+
+    private static final String TAG = "TertiaryKeyRotationTracker";
+    private static final boolean DEBUG = false;
+
+    /**
+     * A new instance, using {@code context} to commit data to disk via {@link SharedPreferences}.
+     */
+    public static TertiaryKeyRotationTracker getInstance(Context context) {
+        return new TertiaryKeyRotationTracker(
+                context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE),
+                MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION);
+    }
+
+    private final SharedPreferences mSharedPreferences;
+    private final int mMaxBackupsTillRotation;
+
+    /**
+     * New instance, storing data in {@code sharedPreferences} and initializing backup countdown to
+     * {@code maxBackupsTillRotation}.
+     */
+    @VisibleForTesting
+    TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) {
+        checkArgument(
+                maxBackupsTillRotation >= 0,
+                String.format(
+                        Locale.US,
+                        "maxBackupsTillRotation should be non-negative but was %d",
+                        maxBackupsTillRotation));
+        mSharedPreferences = sharedPreferences;
+        mMaxBackupsTillRotation = maxBackupsTillRotation;
+    }
+
+    /**
+     * Returns {@code true} if the given app is due having its key rotated.
+     *
+     * @param packageName The package name of the app.
+     */
+    public boolean isKeyRotationDue(String packageName) {
+        return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation;
+    }
+
+    /**
+     * Records that an incremental backup has occurred. Each incremental backup brings the app
+     * closer to the time when its key should be rotated.
+     *
+     * @param packageName The package name of the app for which the backup occurred.
+     */
+    public void recordBackup(String packageName) {
+        int backupsSinceRotation = getBackupsSinceRotation(packageName) + 1;
+        mSharedPreferences.edit().putInt(packageName, backupsSinceRotation).apply();
+        if (DEBUG) {
+            Slog.d(
+                    TAG,
+                    String.format(
+                            Locale.US,
+                            "Incremental backup for %s. %d backups until key rotation.",
+                            packageName,
+                            Math.max(
+                                    0,
+                                    mMaxBackupsTillRotation
+                                            - backupsSinceRotation)));
+        }
+    }
+
+    /**
+     * Resets the rotation delay for the given app. Should be invoked after a key rotation.
+     *
+     * @param packageName Package name of the app whose key has rotated.
+     */
+    public void resetCountdown(String packageName) {
+        mSharedPreferences.edit().putInt(packageName, 0).apply();
+    }
+
+    /** Marks all enrolled packages for key rotation. */
+    public void markAllForRotation() {
+        SharedPreferences.Editor editor = mSharedPreferences.edit();
+        for (String packageName : mSharedPreferences.getAll().keySet()) {
+            editor.putInt(packageName, mMaxBackupsTillRotation);
+        }
+        editor.apply();
+    }
+
+    private int getBackupsSinceRotation(String packageName) {
+        return mSharedPreferences.getInt(packageName, 0);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java
new file mode 100644
index 0000000..49bb410
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationTrackerTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Tests for {@link TertiaryKeyRotationTracker}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TertiaryKeyRotationTrackerTest {
+    private static final String PACKAGE_1 = "com.package.one";
+    private static final int NUMBER_OF_BACKUPS_BEFORE_ROTATION = 31;
+
+    private TertiaryKeyRotationTracker mTertiaryKeyRotationTracker;
+
+    /** Instantiate a {@link TertiaryKeyRotationTracker} for use in tests. */
+    @Before
+    public void setUp() {
+        mTertiaryKeyRotationTracker = newInstance();
+    }
+
+    /** New packages should not be due for key rotation. */
+    @Test
+    public void isKeyRotationDue_forNewPackage_isFalse() {
+        // Simulate a new package by not calling simulateBackups(). As a result, PACKAGE_1 hasn't
+        // been seen by mTertiaryKeyRotationTracker before.
+        boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
+
+        assertThat(keyRotationDue).isFalse();
+    }
+
+    /**
+     * Key rotation should not be due after less than {@code NUMBER_OF_BACKUPS_BEFORE_ROTATION}
+     * backups.
+     */
+    @Test
+    public void isKeyRotationDue_afterLessThanRotationAmountBackups_isFalse() {
+        simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION - 1);
+
+        boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
+
+        assertThat(keyRotationDue).isFalse();
+    }
+
+    /** Key rotation should be due after {@code NUMBER_OF_BACKUPS_BEFORE_ROTATION} backups. */
+    @Test
+    public void isKeyRotationDue_afterRotationAmountBackups_isTrue() {
+        simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
+
+        boolean keyRotationDue = mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1);
+
+        assertThat(keyRotationDue).isTrue();
+    }
+
+    /**
+     * A call to {@link TertiaryKeyRotationTracker#resetCountdown(String)} should make sure no key
+     * rotation is due.
+     */
+    @Test
+    public void resetCountdown_makesKeyRotationNotDue() {
+        simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
+
+        mTertiaryKeyRotationTracker.resetCountdown(PACKAGE_1);
+
+        assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    /**
+     * New instances of {@link TertiaryKeyRotationTracker} should read state about the number of
+     * backups from disk.
+     */
+    @Test
+    public void isKeyRotationDue_forNewInstance_readsStateFromDisk() {
+        simulateBackups(PACKAGE_1, NUMBER_OF_BACKUPS_BEFORE_ROTATION);
+
+        boolean keyRotationDueForNewInstance = newInstance().isKeyRotationDue(PACKAGE_1);
+
+        assertThat(keyRotationDueForNewInstance).isTrue();
+    }
+
+    /**
+     * A call to {@link TertiaryKeyRotationTracker#markAllForRotation()} should mark all previously
+     * seen packages for rotation.
+     */
+    @Test
+    public void markAllForRotation_marksSeenPackagesForKeyRotation() {
+        simulateBackups(PACKAGE_1, /*numberOfBackups=*/ 1);
+
+        mTertiaryKeyRotationTracker.markAllForRotation();
+
+        assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isTrue();
+    }
+
+    /**
+     * A call to {@link TertiaryKeyRotationTracker#markAllForRotation()} should not mark any new
+     * packages for rotation.
+     */
+    @Test
+    public void markAllForRotation_doesNotMarkUnseenPackages() {
+        mTertiaryKeyRotationTracker.markAllForRotation();
+
+        assertThat(mTertiaryKeyRotationTracker.isKeyRotationDue(PACKAGE_1)).isFalse();
+    }
+
+    private void simulateBackups(String packageName, int numberOfBackups) {
+        while (numberOfBackups > 0) {
+            mTertiaryKeyRotationTracker.recordBackup(packageName);
+            numberOfBackups--;
+        }
+    }
+
+    private static TertiaryKeyRotationTracker newInstance() {
+        return TertiaryKeyRotationTracker.getInstance(RuntimeEnvironment.application);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
new file mode 100644
index 0000000..b90343a
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This
+ * allows us to limit (and therefore stagger) the number of key rotations in a given period of time.
+ *
+ * <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures:
+ * we expect the total size of this count at any time to be below however many rotations we allow in
+ * the window, which is going to be in single digits. Any changes that mean we write to disk more
+ * frequently, that the code is no longer resistant to clock changes, or that the code is more
+ * difficult to understand are almost certainly not worthwhile.
+ */
+public class TertiaryKeyRotationWindowedCount {
+    private static final String TAG = "TertiaryKeyRotCount";
+
+    private static final int WINDOW_IN_HOURS = 24;
+    private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count";
+
+    private final Clock mClock;
+    private final File mFile;
+    private ArrayList<Long> mEvents;
+
+    /** Returns a new instance, persisting state to the files dir of {@code context}. */
+    public static TertiaryKeyRotationWindowedCount getInstance(Context context) {
+        File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
+        return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone());
+    }
+
+    /** A new instance, committing state to {@code file}, and reading time from {@code clock}. */
+    @VisibleForTesting
+    TertiaryKeyRotationWindowedCount(File file, Clock clock) {
+        mFile = file;
+        mClock = clock;
+        mEvents = new ArrayList<>();
+        try {
+            loadFromFile();
+        } catch (IOException e) {
+            Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e);
+        }
+    }
+
+    /** Records a key rotation at the current time. */
+    public void record() {
+        mEvents.add(mClock.millis());
+        compact();
+        try {
+            saveToFile();
+        } catch (IOException e) {
+            Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e);
+        }
+    }
+
+    /** Returns the number of key rotation that have been recorded in the window. */
+    public int getCount() {
+        compact();
+        return mEvents.size();
+    }
+
+    private void compact() {
+        long minimumTimestamp = getMinimumTimestamp();
+        long now = mClock.millis();
+        ArrayList<Long> compacted = new ArrayList<>();
+        for (long event : mEvents) {
+            if (event >= minimumTimestamp && event <= now) {
+                compacted.add(event);
+            }
+        }
+        mEvents = compacted;
+    }
+
+    private long getMinimumTimestamp() {
+        return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1;
+    }
+
+    private void loadFromFile() throws IOException {
+        if (!mFile.exists()) {
+            return;
+        }
+        try (FileInputStream fis = new FileInputStream(mFile);
+                DataInputStream dis = new DataInputStream(fis)) {
+            while (true) {
+                mEvents.add(dis.readLong());
+            }
+        } catch (EOFException eof) {
+            // expected
+        }
+    }
+
+    private void saveToFile() throws IOException {
+        // File size is maximum number of key rotations in window multiplied by 8 bytes, which is
+        // why
+        // we just overwrite it each time. We expect it will always be less than 100 bytes in size.
+        try (FileOutputStream fos = new FileOutputStream(mFile);
+                DataOutputStream dos = new DataOutputStream(fos)) {
+            for (long event : mEvents) {
+                dos.writeLong(event);
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
new file mode 100644
index 0000000..bd30977
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link TertiaryKeyRotationWindowedCount}. */
+@RunWith(RobolectricTestRunner.class)
+public class TertiaryKeyRotationWindowedCountTest {
+    private static final int TIMESTAMP_SIZE_IN_BYTES = 8;
+
+    @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    @Mock private Clock mClock;
+
+    private File mFile;
+    private TertiaryKeyRotationWindowedCount mWindowedcount;
+
+    /** Setup the windowed counter for testing */
+    @Before
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+        mFile = mTemporaryFolder.newFile();
+        mWindowedcount = new TertiaryKeyRotationWindowedCount(mFile, mClock);
+    }
+
+    /** Test handling bad files */
+    @Test
+    public void constructor_doesNotFailForBadFile() throws IOException {
+        new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock);
+    }
+
+    /** Test the count is 0 to start */
+    @Test
+    public void getCount_isZeroInitially() {
+        assertThat(mWindowedcount.getCount()).isEqualTo(0);
+    }
+
+    /** Test the count is correct for a time window */
+    @Test
+    public void getCount_includesResultsInLastTwentyFourHours() {
+        setTimeMillis(0);
+        mWindowedcount.record();
+        setTimeMillis(TimeUnit.HOURS.toMillis(4));
+        mWindowedcount.record();
+        setTimeMillis(TimeUnit.HOURS.toMillis(23));
+        mWindowedcount.record();
+        mWindowedcount.record();
+        assertThat(mWindowedcount.getCount()).isEqualTo(4);
+    }
+
+    /** Test old results are ignored */
+    @Test
+    public void getCount_ignoresResultsOlderThanTwentyFourHours() {
+        setTimeMillis(0);
+        mWindowedcount.record();
+        setTimeMillis(TimeUnit.HOURS.toMillis(24));
+        assertThat(mWindowedcount.getCount()).isEqualTo(0);
+    }
+
+    /** Test future events are removed if the clock moves backways (e.g. DST, TZ change) */
+    @Test
+    public void getCount_removesFutureEventsIfClockHasChanged() {
+        setTimeMillis(1000);
+        mWindowedcount.record();
+        setTimeMillis(0);
+        assertThat(mWindowedcount.getCount()).isEqualTo(0);
+    }
+
+    /** Check recording doesn't fail for a bad file */
+    @Test
+    public void record_doesNotFailForBadFile() throws Exception {
+        new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock).record();
+    }
+
+    /** Checks the state is persisted */
+    @Test
+    public void record_persistsStateToDisk() {
+        setTimeMillis(0);
+        mWindowedcount.record();
+        assertThat(new TertiaryKeyRotationWindowedCount(mFile, mClock).getCount()).isEqualTo(1);
+    }
+
+    /** Test the file doesn't contain unnecessary data */
+    @Test
+    public void record_compactsFileToLast24Hours() {
+        setTimeMillis(0);
+        mWindowedcount.record();
+        assertThat(mFile.length()).isEqualTo(TIMESTAMP_SIZE_IN_BYTES);
+        setTimeMillis(1);
+        mWindowedcount.record();
+        assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
+        setTimeMillis(TimeUnit.HOURS.toMillis(24));
+        mWindowedcount.record();
+        assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
+    }
+
+    private void setTimeMillis(long timeMillis) {
+        when(mClock.millis()).thenReturn(timeMillis);
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyStore.java b/com/android/server/backup/encryption/keys/TertiaryKeyStore.java
new file mode 100644
index 0000000..01444bf
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyStore.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.util.ArrayMap;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.encryption.storage.BackupEncryptionDb;
+import com.android.server.backup.encryption.storage.TertiaryKey;
+import com.android.server.backup.encryption.storage.TertiaryKeysTable;
+
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Stores backup package keys. Each application package has its own {@link SecretKey}, which is used
+ * to encrypt the backup data. These keys are then wrapped by a master backup key, and stored in
+ * their wrapped form on disk and on the backup server.
+ *
+ * <p>For now this code only implements writing to disk. Once the backup server is ready, it will be
+ * extended to sync the keys there, also.
+ */
+public class TertiaryKeyStore {
+
+    private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    private final BackupEncryptionDb mDatabase;
+
+    /**
+     * Creates an instance, using {@code secondaryKey} to wrap tertiary keys, and storing them in
+     * the database.
+     */
+    public static TertiaryKeyStore newInstance(
+            Context context, RecoverableKeyStoreSecondaryKey secondaryKey) {
+        return new TertiaryKeyStore(secondaryKey, BackupEncryptionDb.newInstance(context));
+    }
+
+    private TertiaryKeyStore(
+            RecoverableKeyStoreSecondaryKey secondaryKey, BackupEncryptionDb database) {
+        mSecondaryKey = secondaryKey;
+        mDatabase = database;
+    }
+
+    /**
+     * Saves the given key.
+     *
+     * @param applicationName The package name of the application for which this key will be used to
+     *     encrypt data. e.g., "com.example.app".
+     * @param key The key.
+     * @throws InvalidKeyException if the backup key is not capable of wrapping.
+     * @throws IOException if there is an issue writing to the database.
+     */
+    public void save(String applicationName, SecretKey key)
+            throws IOException, InvalidKeyException, IllegalBlockSizeException,
+                    NoSuchPaddingException, NoSuchAlgorithmException {
+        checkApplicationName(applicationName);
+
+        byte[] keyBytes = getEncodedKey(KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), key));
+
+        long pk;
+        try {
+            pk =
+                    mDatabase
+                            .getTertiaryKeysTable()
+                            .addKey(
+                                    new TertiaryKey(
+                                            mSecondaryKey.getAlias(), applicationName, keyBytes));
+        } finally {
+            mDatabase.close();
+        }
+
+        if (pk == -1) {
+            throw new IOException("Failed to commit to db");
+        }
+    }
+
+    /**
+     * Tries to load a key for the given application.
+     *
+     * @param applicationName The package name of the application, e.g. "com.example.app".
+     * @return The key if it is exists, {@link Optional#empty()} ()} otherwise.
+     * @throws InvalidKeyException if the backup key is not good for unwrapping.
+     * @throws IOException if there is a problem loading the key from the database.
+     */
+    public Optional<SecretKey> load(String applicationName)
+            throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
+                    NoSuchAlgorithmException, NoSuchPaddingException {
+        checkApplicationName(applicationName);
+
+        Optional<TertiaryKey> keyFromDb;
+        try {
+            keyFromDb =
+                    mDatabase
+                            .getTertiaryKeysTable()
+                            .getKey(mSecondaryKey.getAlias(), applicationName);
+        } finally {
+            mDatabase.close();
+        }
+
+        if (!keyFromDb.isPresent()) {
+            return Optional.empty();
+        }
+
+        WrappedKeyProto.WrappedKey wrappedKey =
+                WrappedKeyProto.WrappedKey.parseFrom(keyFromDb.get().getWrappedKeyBytes());
+        return Optional.of(KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
+    }
+
+    /**
+     * Loads keys for all applications.
+     *
+     * @return All of the keys in a map keyed by package name.
+     * @throws IOException if there is an issue loading from the database.
+     * @throws InvalidKeyException if the backup key is not an appropriate key for unwrapping.
+     */
+    public Map<String, SecretKey> getAll()
+            throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
+                    NoSuchAlgorithmException, NoSuchPaddingException {
+        Map<String, TertiaryKey> tertiaries;
+        try {
+            tertiaries = mDatabase.getTertiaryKeysTable().getAllKeys(mSecondaryKey.getAlias());
+        } finally {
+            mDatabase.close();
+        }
+
+        Map<String, SecretKey> unwrappedKeys = new ArrayMap<>();
+        for (String applicationName : tertiaries.keySet()) {
+            WrappedKeyProto.WrappedKey wrappedKey =
+                    WrappedKeyProto.WrappedKey.parseFrom(
+                            tertiaries.get(applicationName).getWrappedKeyBytes());
+            unwrappedKeys.put(
+                    applicationName, KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
+        }
+
+        return unwrappedKeys;
+    }
+
+    /**
+     * Adds all wrapped keys to the database.
+     *
+     * @throws IOException if an error occurred adding a wrapped key.
+     */
+    public void putAll(Map<String, WrappedKeyProto.WrappedKey> wrappedKeysByApplicationName)
+            throws IOException {
+        TertiaryKeysTable tertiaryKeysTable = mDatabase.getTertiaryKeysTable();
+        try {
+
+            for (String applicationName : wrappedKeysByApplicationName.keySet()) {
+                byte[] keyBytes = getEncodedKey(wrappedKeysByApplicationName.get(applicationName));
+                long primaryKey =
+                        tertiaryKeysTable.addKey(
+                                new TertiaryKey(
+                                        mSecondaryKey.getAlias(), applicationName, keyBytes));
+
+                if (primaryKey == -1) {
+                    throw new IOException("Failed to commit to db");
+                }
+            }
+
+        } finally {
+            mDatabase.close();
+        }
+    }
+
+    private static void checkApplicationName(String applicationName) {
+        checkArgument(!applicationName.isEmpty(), "applicationName must not be empty string.");
+        checkArgument(!applicationName.contains("/"), "applicationName must not contain slash.");
+    }
+
+    private byte[] getEncodedKey(WrappedKeyProto.WrappedKey key) throws IOException {
+        byte[] buffer = new byte[key.getSerializedSize()];
+        CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(buffer);
+        key.writeTo(out);
+        return buffer;
+    }
+}
diff --git a/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java b/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java
new file mode 100644
index 0000000..ccc5f32
--- /dev/null
+++ b/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2019 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.encryption.keys;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+
+import android.content.Context;
+
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.security.InvalidKeyException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+
+/** Tests for the tertiary key store */
+@RunWith(RobolectricTestRunner.class)
+public class TertiaryKeyStoreTest {
+
+    private static final String SECONDARY_KEY_ALIAS = "Robbo/Ranx";
+
+    private Context mApplication;
+    private TertiaryKeyStore mTertiaryKeyStore;
+    private SecretKey mSecretKey;
+
+    /** Initialise the keystore for testing */
+    @Before
+    public void setUp() throws Exception {
+        mApplication = RuntimeEnvironment.application;
+        mSecretKey = generateAesKey();
+        mTertiaryKeyStore =
+                TertiaryKeyStore.newInstance(
+                        mApplication,
+                        new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, mSecretKey));
+    }
+
+    /** Test a reound trip for a key */
+    @Test
+    public void load_loadsAKeyThatWasSaved() throws Exception {
+        String packageName = "com.android.example";
+        SecretKey packageKey = generateAesKey();
+        mTertiaryKeyStore.save(packageName, packageKey);
+
+        Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
+
+        assertTrue(maybeLoadedKey.isPresent());
+        assertEquals(packageKey, maybeLoadedKey.get());
+    }
+
+    /** Test isolation between packages */
+    @Test
+    public void load_doesNotLoadAKeyForAnotherSecondary() throws Exception {
+        String packageName = "com.android.example";
+        SecretKey packageKey = generateAesKey();
+        mTertiaryKeyStore.save(packageName, packageKey);
+        TertiaryKeyStore managerWithOtherSecondaryKey =
+                TertiaryKeyStore.newInstance(
+                        mApplication,
+                        new RecoverableKeyStoreSecondaryKey(
+                                "myNewSecondaryKeyAlias", generateAesKey()));
+
+        assertFalse(managerWithOtherSecondaryKey.load(packageName).isPresent());
+    }
+
+    /** Test non-existent key handling */
+    @Test
+    public void load_returnsAbsentForANonExistentKey() throws Exception {
+        assertFalse(mTertiaryKeyStore.load("mystery.package").isPresent());
+    }
+
+    /** Test handling incorrect keys */
+    @Test
+    public void load_throwsIfHasWrongBackupKey() throws Exception {
+        String packageName = "com.android.example";
+        SecretKey packageKey = generateAesKey();
+        mTertiaryKeyStore.save(packageName, packageKey);
+        TertiaryKeyStore managerWithBadKey =
+                TertiaryKeyStore.newInstance(
+                        mApplication,
+                        new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, generateAesKey()));
+
+        assertThrows(InvalidKeyException.class, () -> managerWithBadKey.load(packageName));
+    }
+
+    /** Test handling of empty app name */
+    @Test
+    public void load_throwsForEmptyApplicationName() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mTertiaryKeyStore.load(""));
+    }
+
+    /** Test handling of an invalid app name */
+    @Test
+    public void load_throwsForBadApplicationName() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mTertiaryKeyStore.load("com/android/example"));
+    }
+
+    /** Test key replacement */
+    @Test
+    public void save_overwritesPreviousKey() throws Exception {
+        String packageName = "com.android.example";
+        SecretKey oldKey = generateAesKey();
+        mTertiaryKeyStore.save(packageName, oldKey);
+        SecretKey newKey = generateAesKey();
+
+        mTertiaryKeyStore.save(packageName, newKey);
+
+        Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
+        assertTrue(maybeLoadedKey.isPresent());
+        SecretKey loadedKey = maybeLoadedKey.get();
+        assertThat(loadedKey).isNotEqualTo(oldKey);
+        assertThat(loadedKey).isEqualTo(newKey);
+    }
+
+    /** Test saving with an empty application name fails */
+    @Test
+    public void save_throwsForEmptyApplicationName() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class, () -> mTertiaryKeyStore.save("", generateAesKey()));
+    }
+
+    /** Test saving an invalid application name fails */
+    @Test
+    public void save_throwsForBadApplicationName() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mTertiaryKeyStore.save("com/android/example", generateAesKey()));
+    }
+
+    /** Test handling an empty database */
+    @Test
+    public void getAll_returnsEmptyMapForEmptyDb() throws Exception {
+        assertThat(mTertiaryKeyStore.getAll()).isEmpty();
+    }
+
+    /** Test loading all available keys works as expected */
+    @Test
+    public void getAll_returnsAllKeysSaved() throws Exception {
+        String package1 = "com.android.example";
+        SecretKey key1 = generateAesKey();
+        String package2 = "com.anndroid.example1";
+        SecretKey key2 = generateAesKey();
+        String package3 = "com.android.example2";
+        SecretKey key3 = generateAesKey();
+        mTertiaryKeyStore.save(package1, key1);
+        mTertiaryKeyStore.save(package2, key2);
+        mTertiaryKeyStore.save(package3, key3);
+
+        Map<String, SecretKey> keys = mTertiaryKeyStore.getAll();
+
+        assertThat(keys).containsExactly(package1, key1, package2, key2, package3, key3);
+    }
+
+    /** Test cross-secondary isolation */
+    @Test
+    public void getAll_doesNotReturnKeysForOtherSecondary() throws Exception {
+        String packageName = "com.android.example";
+        TertiaryKeyStore managerWithOtherSecondaryKey =
+                TertiaryKeyStore.newInstance(
+                        mApplication,
+                        new RecoverableKeyStoreSecondaryKey(
+                                "myNewSecondaryKeyAlias", generateAesKey()));
+        managerWithOtherSecondaryKey.save(packageName, generateAesKey());
+
+        assertThat(mTertiaryKeyStore.getAll()).isEmpty();
+    }
+
+    /** Test mass put into the keystore */
+    @Test
+    public void putAll_putsAllWrappedKeysInTheStore() throws Exception {
+        String packageName = "com.android.example";
+        SecretKey key = generateAesKey();
+        WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(mSecretKey, key);
+
+        Map<String, WrappedKeyProto.WrappedKey> testElements = new HashMap<>();
+        testElements.put(packageName, wrappedKey);
+        mTertiaryKeyStore.putAll(testElements);
+
+        assertThat(mTertiaryKeyStore.getAll()).containsKey(packageName);
+        assertThat(mTertiaryKeyStore.getAll().get(packageName).getEncoded())
+                .isEqualTo(key.getEncoded());
+    }
+}
diff --git a/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java b/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
new file mode 100644
index 0000000..56e1c05
--- /dev/null
+++ b/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 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.encryption.kv;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Builds a key value backup set from plaintext chunks. Computes a digest over the sorted SHA-256
+ * hashes of the chunks.
+ */
+public class DecryptedChunkKvOutput implements DecryptedChunkOutput {
+    @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
+
+    private final ChunkHasher mChunkHasher;
+    private final List<KeyValuePairProto.KeyValuePair> mUnsortedPairs = new ArrayList<>();
+    private final List<ChunkHash> mUnsortedHashes = new ArrayList<>();
+    private boolean mClosed;
+
+    /** Constructs a new instance which computers the digest using the given hasher. */
+    public DecryptedChunkKvOutput(ChunkHasher chunkHasher) {
+        mChunkHasher = chunkHasher;
+    }
+
+    @Override
+    public DecryptedChunkOutput open() {
+        // As we don't have any resources there is nothing to open.
+        return this;
+    }
+
+    @Override
+    public void processChunk(byte[] plaintextBuffer, int length)
+            throws IOException, InvalidKeyException {
+        checkState(!mClosed, "Cannot process chunk after close()");
+        KeyValuePairProto.KeyValuePair kvPair = new KeyValuePairProto.KeyValuePair();
+        KeyValuePairProto.KeyValuePair.mergeFrom(kvPair, plaintextBuffer, 0, length);
+        mUnsortedPairs.add(kvPair);
+        // TODO(b/71492289): Update ChunkHasher to accept offset and length so we don't have to copy
+        // the buffer into a smaller array.
+        mUnsortedHashes.add(mChunkHasher.computeHash(Arrays.copyOf(plaintextBuffer, length)));
+    }
+
+    @Override
+    public void close() {
+        // As we don't have any resources there is nothing to close.
+        mClosed = true;
+    }
+
+    @Override
+    public byte[] getDigest() throws NoSuchAlgorithmException {
+        checkState(mClosed, "Must close() before getDigest()");
+        MessageDigest digest = getMessageDigest();
+        Collections.sort(mUnsortedHashes);
+        for (ChunkHash hash : mUnsortedHashes) {
+            digest.update(hash.getHash());
+        }
+        return digest.digest();
+    }
+
+    private static MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(DIGEST_ALGORITHM);
+    }
+
+    /**
+     * Returns the key value pairs from the backup, sorted lexicographically by key.
+     *
+     * <p>You must call {@link #close} first.
+     */
+    public List<KeyValuePairProto.KeyValuePair> getPairs() {
+        checkState(mClosed, "Must close() before getPairs()");
+        Collections.sort(
+                mUnsortedPairs,
+                new Comparator<KeyValuePairProto.KeyValuePair>() {
+                    @Override
+                    public int compare(
+                            KeyValuePairProto.KeyValuePair o1, KeyValuePairProto.KeyValuePair o2) {
+                        return o1.key.compareTo(o2.key);
+                    }
+                });
+        return mUnsortedPairs;
+    }
+}
diff --git a/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java b/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
new file mode 100644
index 0000000..215e1cb
--- /dev/null
+++ b/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 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.encryption.kv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Debug;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class DecryptedChunkKvOutputTest {
+    private static final String TEST_KEY_1 = "key_1";
+    private static final String TEST_KEY_2 = "key_2";
+    private static final byte[] TEST_VALUE_1 = {1, 2, 3};
+    private static final byte[] TEST_VALUE_2 = {10, 11, 12, 13};
+    private static final byte[] TEST_PAIR_1 = toByteArray(createPair(TEST_KEY_1, TEST_VALUE_1));
+    private static final byte[] TEST_PAIR_2 = toByteArray(createPair(TEST_KEY_2, TEST_VALUE_2));
+    private static final int TEST_BUFFER_SIZE = Math.max(TEST_PAIR_1.length, TEST_PAIR_2.length);
+
+    @Mock private ChunkHasher mChunkHasher;
+    private DecryptedChunkKvOutput mOutput;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mChunkHasher.computeHash(any()))
+                .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
+        mOutput = new DecryptedChunkKvOutput(mChunkHasher);
+    }
+
+    @Test
+    public void open_returnsInstance() throws Exception {
+        assertThat(mOutput.open()).isEqualTo(mOutput);
+    }
+
+    @Test
+    public void processChunk_alreadyClosed_throws() throws Exception {
+        mOutput.open();
+        mOutput.close();
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mOutput.processChunk(TEST_PAIR_1, TEST_PAIR_1.length));
+    }
+
+    @Test
+    public void getDigest_beforeClose_throws() throws Exception {
+        // TODO: b/141356823 We should add a test which calls .open() here
+        assertThrows(IllegalStateException.class, () -> mOutput.getDigest());
+    }
+
+    @Test
+    public void getDigest_returnsDigestOfSortedHashes() throws Exception {
+        mOutput.open();
+        Debug.waitForDebugger();
+        mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
+        mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
+        mOutput.close();
+
+        byte[] actualDigest = mOutput.getDigest();
+
+        MessageDigest digest = MessageDigest.getInstance(DecryptedChunkKvOutput.DIGEST_ALGORITHM);
+        Stream.of(TEST_PAIR_1, TEST_PAIR_2)
+                .map(DecryptedChunkKvOutputTest::fakeHash)
+                .sorted(Comparator.naturalOrder())
+                .forEachOrdered(hash -> digest.update(hash.getHash()));
+        assertThat(actualDigest).isEqualTo(digest.digest());
+    }
+
+    @Test
+    public void getPairs_beforeClose_throws() throws Exception {
+        // TODO: b/141356823 We should add a test which calls .open() here
+        assertThrows(IllegalStateException.class, () -> mOutput.getPairs());
+    }
+
+    @Test
+    public void getPairs_returnsPairsSortedByKey() throws Exception {
+        mOutput.open();
+        // Write out of order to check that it sorts the chunks.
+        mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
+        mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
+        mOutput.close();
+
+        List<KeyValuePairProto.KeyValuePair> pairs = mOutput.getPairs();
+
+        assertThat(
+                        isInOrder(
+                                pairs,
+                                Comparator.comparing(
+                                        (KeyValuePairProto.KeyValuePair pair) -> pair.key)))
+                .isTrue();
+        assertThat(pairs).hasSize(2);
+        assertThat(pairs.get(0).key).isEqualTo(TEST_KEY_1);
+        assertThat(pairs.get(0).value).isEqualTo(TEST_VALUE_1);
+        assertThat(pairs.get(1).key).isEqualTo(TEST_KEY_2);
+        assertThat(pairs.get(1).value).isEqualTo(TEST_VALUE_2);
+    }
+
+    private static KeyValuePairProto.KeyValuePair createPair(String key, byte[] value) {
+        KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
+        pair.key = key;
+        pair.value = value;
+        return pair;
+    }
+
+    private boolean isInOrder(
+            List<KeyValuePairProto.KeyValuePair> list,
+            Comparator<KeyValuePairProto.KeyValuePair> comparator) {
+        if (list.size() < 2) {
+            return true;
+        }
+
+        List<KeyValuePairProto.KeyValuePair> sortedList = new ArrayList<>(list);
+        Collections.sort(sortedList, comparator);
+        return list.equals(sortedList);
+    }
+
+    private static byte[] toByteArray(KeyValuePairProto.KeyValuePair nano) {
+        return KeyValuePairProto.KeyValuePair.toByteArray(nano);
+    }
+
+    private static ChunkHash fakeHash(byte[] data) {
+        return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
+    }
+}
diff --git a/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java b/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
new file mode 100644
index 0000000..217304c
--- /dev/null
+++ b/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.encryption.kv;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+/**
+ * Builds a {@link KeyValueListingProto.KeyValueListing}, which is a nano proto and so has no
+ * builder.
+ */
+public class KeyValueListingBuilder {
+    private final List<KeyValueListingProto.KeyValueEntry> mEntries = new ArrayList<>();
+
+    /** Adds a new pair entry to the listing. */
+    public KeyValueListingBuilder addPair(String key, ChunkHash hash) {
+        checkArgument(key.length() != 0, "Key must have non-zero length");
+        Objects.requireNonNull(hash, "Hash must not be null");
+
+        KeyValueListingProto.KeyValueEntry entry = new KeyValueListingProto.KeyValueEntry();
+        entry.key = key;
+        entry.hash = hash.getHash();
+        mEntries.add(entry);
+
+        return this;
+    }
+
+    /** Adds all pairs contained in a map, where the map is from key to hash. */
+    public KeyValueListingBuilder addAll(Map<String, ChunkHash> map) {
+        for (Entry<String, ChunkHash> entry : map.entrySet()) {
+            addPair(entry.getKey(), entry.getValue());
+        }
+
+        return this;
+    }
+
+    /** Returns a new listing containing all the pairs added so far. */
+    public KeyValueListingProto.KeyValueListing build() {
+        if (mEntries.size() == 0) {
+            return emptyListing();
+        }
+
+        KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
+        listing.entries = new KeyValueListingProto.KeyValueEntry[mEntries.size()];
+        mEntries.toArray(listing.entries);
+        return listing;
+    }
+
+    /** Returns a new listing which does not contain any pairs. */
+    public static KeyValueListingProto.KeyValueListing emptyListing() {
+        KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
+        listing.entries = KeyValueListingProto.KeyValueEntry.emptyArray();
+        return listing;
+    }
+}
diff --git a/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java b/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
new file mode 100644
index 0000000..acc6628
--- /dev/null
+++ b/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2019 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.encryption.kv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class KeyValueListingBuilderTest {
+    private static final String TEST_KEY_1 = "test_key_1";
+    private static final String TEST_KEY_2 = "test_key_2";
+    private static final ChunkHash TEST_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1, 2}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {5, 6}, ChunkHash.HASH_LENGTH_BYTES));
+
+    private KeyValueListingBuilder mBuilder;
+
+    @Before
+    public void setUp() {
+        mBuilder = new KeyValueListingBuilder();
+    }
+
+    @Test
+    public void addPair_nullKey_throws() {
+        assertThrows(NullPointerException.class, () -> mBuilder.addPair(null, TEST_HASH_1));
+    }
+
+    @Test
+    public void addPair_emptyKey_throws() {
+        assertThrows(IllegalArgumentException.class, () -> mBuilder.addPair("", TEST_HASH_1));
+    }
+
+    @Test
+    public void addPair_nullHash_throws() {
+        assertThrows(NullPointerException.class, () -> mBuilder.addPair(TEST_KEY_1, null));
+    }
+
+    @Test
+    public void build_noPairs_buildsEmptyListing() {
+        KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+        assertThat(listing.entries).isEmpty();
+    }
+
+    @Test
+    public void build_returnsCorrectListing() {
+        mBuilder.addPair(TEST_KEY_1, TEST_HASH_1);
+
+        KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+        assertThat(listing.entries.length).isEqualTo(1);
+        assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
+        assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+    }
+
+    @Test
+    public void addAll_addsAllPairsInMap() {
+        ImmutableMap<String, ChunkHash> pairs =
+                new ImmutableMap.Builder<String, ChunkHash>()
+                        .put(TEST_KEY_1, TEST_HASH_1)
+                        .put(TEST_KEY_2, TEST_HASH_2)
+                        .build();
+
+        mBuilder.addAll(pairs);
+        KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+        assertThat(listing.entries.length).isEqualTo(2);
+        assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
+        assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+        assertThat(listing.entries[1].key).isEqualTo(TEST_KEY_2);
+        assertThat(listing.entries[1].hash).isEqualTo(TEST_HASH_2.getHash());
+    }
+
+    @Test
+    public void emptyListing_returnsListingWithoutAnyPairs() {
+        KeyValueListingProto.KeyValueListing emptyListing = KeyValueListingBuilder.emptyListing();
+        assertThat(emptyListing.entries).isEmpty();
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/BackupEncryptionDb.java b/com/android/server/backup/encryption/storage/BackupEncryptionDb.java
new file mode 100644
index 0000000..9f6c03a
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/BackupEncryptionDb.java
@@ -0,0 +1,59 @@
+/*
+ * 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.encryption.storage;
+
+import android.content.Context;
+
+/**
+ * Backup encryption SQLite database. All instances are threadsafe.
+ *
+ * <p>The database is automatically opened when accessing one of the tables. After the caller is
+ * done they must call {@link #close()}.
+ */
+public class BackupEncryptionDb {
+    private final BackupEncryptionDbHelper mHelper;
+
+    /** A new instance, using the storage defined by {@code context}. */
+    public static BackupEncryptionDb newInstance(Context context) {
+        BackupEncryptionDbHelper helper = new BackupEncryptionDbHelper(context);
+        helper.setWriteAheadLoggingEnabled(true);
+        return new BackupEncryptionDb(helper);
+    }
+
+    private BackupEncryptionDb(BackupEncryptionDbHelper helper) {
+        mHelper = helper;
+    }
+
+    public TertiaryKeysTable getTertiaryKeysTable() {
+        return new TertiaryKeysTable(mHelper);
+    }
+
+    /** Deletes the database. */
+    public void clear() throws EncryptionDbException {
+        mHelper.resetDatabase();
+    }
+
+    /**
+     * Closes the database if it is open.
+     *
+     * <p>After calling this, the caller may access one of the tables again which will automatically
+     * reopen the database.
+     */
+    public void close() {
+        mHelper.close();
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java b/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java
new file mode 100644
index 0000000..5e8a8d9
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/BackupEncryptionDbContract.java
@@ -0,0 +1,41 @@
+/*
+ * 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.encryption.storage;
+
+import android.provider.BaseColumns;
+
+/** Contract for the backup encryption database. Describes tables present. */
+class BackupEncryptionDbContract {
+    /**
+     * Table containing tertiary keys belonging to the user. Tertiary keys are wrapped by a
+     * secondary key, which never leaves {@code AndroidKeyStore} (a provider for {@link
+     * java.security.KeyStore}). Each application has a tertiary key, which is used to encrypt the
+     * backup data.
+     */
+    static class TertiaryKeysEntry implements BaseColumns {
+        static final String TABLE_NAME = "tertiary_keys";
+
+        /** Alias of the secondary key used to wrap the tertiary key. */
+        static final String COLUMN_NAME_SECONDARY_KEY_ALIAS = "secondary_key_alias";
+
+        /** Name of the package to which the tertiary key belongs. */
+        static final String COLUMN_NAME_PACKAGE_NAME = "package_name";
+
+        /** Encrypted bytes of the tertiary key. */
+        static final String COLUMN_NAME_WRAPPED_KEY_BYTES = "wrapped_key_bytes";
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java b/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java
new file mode 100644
index 0000000..c706342
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/BackupEncryptionDbHelper.java
@@ -0,0 +1,102 @@
+/*
+ * 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.encryption.storage;
+
+import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/** Helper for creating an instance of the backup encryption database. */
+class BackupEncryptionDbHelper extends SQLiteOpenHelper {
+    private static final int DATABASE_VERSION = 1;
+    static final String DATABASE_NAME = "backupencryption.db";
+
+    private static final String SQL_CREATE_TERTIARY_KEYS_ENTRY =
+            "CREATE TABLE "
+                    + TertiaryKeysEntry.TABLE_NAME
+                    + " ( "
+                    + TertiaryKeysEntry._ID
+                    + " INTEGER PRIMARY KEY,"
+                    + TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+                    + " TEXT,"
+                    + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+                    + " TEXT,"
+                    + TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
+                    + " BLOB,"
+                    + "UNIQUE("
+                    + TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+                    + ","
+                    + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+                    + "))";
+
+    private static final String SQL_DROP_TERTIARY_KEYS_ENTRY =
+            "DROP TABLE IF EXISTS " + TertiaryKeysEntry.TABLE_NAME;
+
+    BackupEncryptionDbHelper(Context context) {
+        super(context, DATABASE_NAME, /*factory=*/ null, DATABASE_VERSION);
+    }
+
+    public void resetDatabase() throws EncryptionDbException {
+        SQLiteDatabase db = getWritableDatabaseSafe();
+        db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
+        onCreate(db);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(SQL_CREATE_TERTIARY_KEYS_ENTRY);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
+        onCreate(db);
+    }
+
+    @Override
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
+        onCreate(db);
+    }
+
+    /**
+     * Calls {@link #getWritableDatabase()}, but catches the unchecked {@link SQLiteException} and
+     * rethrows {@link EncryptionDbException}.
+     */
+    public SQLiteDatabase getWritableDatabaseSafe() throws EncryptionDbException {
+        try {
+            return super.getWritableDatabase();
+        } catch (SQLiteException e) {
+            throw new EncryptionDbException(e);
+        }
+    }
+
+    /**
+     * Calls {@link #getReadableDatabase()}, but catches the unchecked {@link SQLiteException} and
+     * rethrows {@link EncryptionDbException}.
+     */
+    public SQLiteDatabase getReadableDatabaseSafe() throws EncryptionDbException {
+        try {
+            return super.getReadableDatabase();
+        } catch (SQLiteException e) {
+            throw new EncryptionDbException(e);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java b/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java
new file mode 100644
index 0000000..87f21bf
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/BackupEncryptionDbTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.encryption.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Tests for {@link BackupEncryptionDb}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupEncryptionDbTest {
+    private BackupEncryptionDb mBackupEncryptionDb;
+
+    /** Creates an empty {@link BackupEncryptionDb} */
+    @Before
+    public void setUp() {
+        mBackupEncryptionDb = BackupEncryptionDb.newInstance(RuntimeEnvironment.application);
+    }
+
+    /**
+     * Tests that the tertiary keys table gets cleared when calling {@link
+     * BackupEncryptionDb#clear()}.
+     */
+    @Test
+    public void clear_withNonEmptyTertiaryKeysTable_clearsTertiaryKeysTable() throws Exception {
+        String secondaryKeyAlias = "secondaryKeyAlias";
+        TertiaryKeysTable tertiaryKeysTable = mBackupEncryptionDb.getTertiaryKeysTable();
+        tertiaryKeysTable.addKey(new TertiaryKey(secondaryKeyAlias, "packageName", new byte[0]));
+
+        mBackupEncryptionDb.clear();
+
+        assertThat(tertiaryKeysTable.getAllKeys(secondaryKeyAlias)).isEmpty();
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/EncryptionDbException.java b/com/android/server/backup/encryption/storage/EncryptionDbException.java
new file mode 100644
index 0000000..82f7dea
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/EncryptionDbException.java
@@ -0,0 +1,26 @@
+/*
+ * 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.encryption.storage;
+
+import java.io.IOException;
+
+/** Thrown when there is a problem reading or writing the encryption database. */
+public class EncryptionDbException extends IOException {
+    public EncryptionDbException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/TertiaryKey.java b/com/android/server/backup/encryption/storage/TertiaryKey.java
new file mode 100644
index 0000000..39a2c6e
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/TertiaryKey.java
@@ -0,0 +1,52 @@
+/*
+ * 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.encryption.storage;
+
+/** Wrapped bytes of a tertiary key. */
+public class TertiaryKey {
+    private final String mSecondaryKeyAlias;
+    private final String mPackageName;
+    private final byte[] mWrappedKeyBytes;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param secondaryKeyAlias Alias of the secondary used to wrap the key.
+     * @param packageName The package name of the app to which the key belongs.
+     * @param wrappedKeyBytes The wrapped key bytes.
+     */
+    public TertiaryKey(String secondaryKeyAlias, String packageName, byte[] wrappedKeyBytes) {
+        mSecondaryKeyAlias = secondaryKeyAlias;
+        mPackageName = packageName;
+        mWrappedKeyBytes = wrappedKeyBytes;
+    }
+
+    /** Returns the alias of the secondary key used to wrap this tertiary key. */
+    public String getSecondaryKeyAlias() {
+        return mSecondaryKeyAlias;
+    }
+
+    /** Returns the package name of the application this key relates to. */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns the wrapped bytes of the key. */
+    public byte[] getWrappedKeyBytes() {
+        return mWrappedKeyBytes;
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/TertiaryKeysTable.java b/com/android/server/backup/encryption/storage/TertiaryKeysTable.java
new file mode 100644
index 0000000..d8d40c4
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/TertiaryKeysTable.java
@@ -0,0 +1,134 @@
+/*
+ * 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.encryption.storage;
+
+import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.ArrayMap;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+/** Database table for storing and retrieving tertiary keys. */
+public class TertiaryKeysTable {
+    private final BackupEncryptionDbHelper mHelper;
+
+    TertiaryKeysTable(BackupEncryptionDbHelper helper) {
+        mHelper = helper;
+    }
+
+    /**
+     * Adds the {@code tertiaryKey} to the database.
+     *
+     * @return The primary key of the inserted row if successful, -1 otherwise.
+     */
+    public long addKey(TertiaryKey tertiaryKey) throws EncryptionDbException {
+        SQLiteDatabase db = mHelper.getWritableDatabaseSafe();
+        ContentValues values = new ContentValues();
+        values.put(
+                TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
+                tertiaryKey.getSecondaryKeyAlias());
+        values.put(TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME, tertiaryKey.getPackageName());
+        values.put(
+                TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES, tertiaryKey.getWrappedKeyBytes());
+        return db.replace(TertiaryKeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+    }
+
+    /** Gets the key wrapped by {@code secondaryKeyAlias} for app with {@code packageName}. */
+    public Optional<TertiaryKey> getKey(String secondaryKeyAlias, String packageName)
+            throws EncryptionDbException {
+        SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
+        String[] projection = {
+            TertiaryKeysEntry._ID,
+            TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
+            TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
+            TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
+        };
+        String selection =
+                TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+                        + " = ? AND "
+                        + TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+                        + " = ?";
+        String[] selectionArguments = {secondaryKeyAlias, packageName};
+
+        try (Cursor cursor =
+                db.query(
+                        TertiaryKeysEntry.TABLE_NAME,
+                        projection,
+                        selection,
+                        selectionArguments,
+                        /*groupBy=*/ null,
+                        /*having=*/ null,
+                        /*orderBy=*/ null)) {
+            int count = cursor.getCount();
+            if (count == 0) {
+                return Optional.empty();
+            }
+
+            cursor.moveToFirst();
+            byte[] wrappedKeyBytes =
+                    cursor.getBlob(
+                            cursor.getColumnIndexOrThrow(
+                                    TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
+            return Optional.of(new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
+        }
+    }
+
+    /** Returns all keys wrapped with {@code tertiaryKeyAlias} as an unmodifiable map. */
+    public Map<String, TertiaryKey> getAllKeys(String secondaryKeyAlias)
+            throws EncryptionDbException {
+        SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
+        String[] projection = {
+            TertiaryKeysEntry._ID,
+            TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
+            TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
+            TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
+        };
+        String selection = TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS + " = ?";
+        String[] selectionArguments = {secondaryKeyAlias};
+
+        Map<String, TertiaryKey> keysByPackageName = new ArrayMap<>();
+        try (Cursor cursor =
+                db.query(
+                        TertiaryKeysEntry.TABLE_NAME,
+                        projection,
+                        selection,
+                        selectionArguments,
+                        /*groupBy=*/ null,
+                        /*having=*/ null,
+                        /*orderBy=*/ null)) {
+            while (cursor.moveToNext()) {
+                String packageName =
+                        cursor.getString(
+                                cursor.getColumnIndexOrThrow(
+                                        TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME));
+                byte[] wrappedKeyBytes =
+                        cursor.getBlob(
+                                cursor.getColumnIndexOrThrow(
+                                        TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
+                keysByPackageName.put(
+                        packageName,
+                        new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
+            }
+        }
+        return Collections.unmodifiableMap(keysByPackageName);
+    }
+}
diff --git a/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java b/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java
new file mode 100644
index 0000000..319ec89
--- /dev/null
+++ b/com/android/server/backup/encryption/storage/TertiaryKeysTableTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.encryption.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.testing.CryptoTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Map;
+import java.util.Optional;
+
+/** Tests for {@link TertiaryKeysTable}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TertiaryKeysTableTest {
+    private static final int KEY_SIZE_BYTES = 32;
+    private static final String SECONDARY_ALIAS = "phoebe";
+    private static final String PACKAGE_NAME = "generic.package.name";
+
+    private TertiaryKeysTable mTertiaryKeysTable;
+
+    /** Creates an empty {@link BackupEncryptionDb}. */
+    @Before
+    public void setUp() {
+        mTertiaryKeysTable =
+                BackupEncryptionDb.newInstance(RuntimeEnvironment.application)
+                        .getTertiaryKeysTable();
+    }
+
+    /** Tests that new {@link TertiaryKey}s get successfully added to the database. */
+    @Test
+    public void addKey_onEmptyDatabase_putsKeyInDb() throws Exception {
+        byte[] key = generateRandomKey();
+        TertiaryKey keyToInsert = new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, key);
+
+        long result = mTertiaryKeysTable.addKey(keyToInsert);
+
+        assertThat(result).isNotEqualTo(-1);
+        Optional<TertiaryKey> maybeKeyInDb =
+                mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME);
+        assertThat(maybeKeyInDb.isPresent()).isTrue();
+        TertiaryKey keyInDb = maybeKeyInDb.get();
+        assertTertiaryKeysEqual(keyInDb, keyToInsert);
+    }
+
+    /** Tests that keys replace older keys with the same secondary alias and package name. */
+    @Test
+    public void addKey_havingSameSecondaryAliasAndPackageName_replacesOldKey() throws Exception {
+        mTertiaryKeysTable.addKey(
+                new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, generateRandomKey()));
+        byte[] newKey = generateRandomKey();
+
+        long result =
+                mTertiaryKeysTable.addKey(new TertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME, newKey));
+
+        assertThat(result).isNotEqualTo(-1);
+        TertiaryKey keyInDb = mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).get();
+        assertThat(keyInDb.getWrappedKeyBytes()).isEqualTo(newKey);
+    }
+
+    /**
+     * Tests that keys do not replace older keys with the same package name but a different alias.
+     */
+    @Test
+    public void addKey_havingSamePackageNameButDifferentAlias_doesNotReplaceOldKey()
+            throws Exception {
+        String alias2 = "karl";
+        TertiaryKey key1 = generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME);
+        TertiaryKey key2 = generateTertiaryKey(alias2, PACKAGE_NAME);
+
+        long primaryKey1 = mTertiaryKeysTable.addKey(key1);
+        long primaryKey2 = mTertiaryKeysTable.addKey(key2);
+
+        assertThat(primaryKey1).isNotEqualTo(primaryKey2);
+        assertThat(mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).isPresent()).isTrue();
+        assertTertiaryKeysEqual(
+                mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME).get(), key1);
+        assertThat(mTertiaryKeysTable.getKey(alias2, PACKAGE_NAME).isPresent()).isTrue();
+        assertTertiaryKeysEqual(mTertiaryKeysTable.getKey(alias2, PACKAGE_NAME).get(), key2);
+    }
+
+    /**
+     * Tests that {@link TertiaryKeysTable#getKey(String, String)} returns an empty {@link Optional}
+     * for a missing key.
+     */
+    @Test
+    public void getKey_forMissingKey_returnsEmptyOptional() throws Exception {
+        Optional<TertiaryKey> key = mTertiaryKeysTable.getKey(SECONDARY_ALIAS, PACKAGE_NAME);
+
+        assertThat(key.isPresent()).isFalse();
+    }
+
+    /**
+     * Tests that {@link TertiaryKeysTable#getAllKeys(String)} returns an empty map when no keys
+     * with the secondary alias exist.
+     */
+    @Test
+    public void getAllKeys_withNoKeysForAlias_returnsEmptyMap() throws Exception {
+        assertThat(mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS)).isEmpty();
+    }
+
+    /**
+     * Tests that {@link TertiaryKeysTable#getAllKeys(String)} returns all keys corresponding to the
+     * provided secondary alias.
+     */
+    @Test
+    public void getAllKeys_withMatchingKeys_returnsAllKeysWrappedWithSecondary() throws Exception {
+        TertiaryKey key1 = generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME);
+        mTertiaryKeysTable.addKey(key1);
+        String package2 = "generic.package.two";
+        TertiaryKey key2 = generateTertiaryKey(SECONDARY_ALIAS, package2);
+        mTertiaryKeysTable.addKey(key2);
+        String package3 = "generic.package.three";
+        TertiaryKey key3 = generateTertiaryKey(SECONDARY_ALIAS, package3);
+        mTertiaryKeysTable.addKey(key3);
+
+        Map<String, TertiaryKey> keysByPackageName = mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS);
+
+        assertThat(keysByPackageName).hasSize(3);
+        assertThat(keysByPackageName).containsKey(PACKAGE_NAME);
+        assertTertiaryKeysEqual(keysByPackageName.get(PACKAGE_NAME), key1);
+        assertThat(keysByPackageName).containsKey(package2);
+        assertTertiaryKeysEqual(keysByPackageName.get(package2), key2);
+        assertThat(keysByPackageName).containsKey(package3);
+        assertTertiaryKeysEqual(keysByPackageName.get(package3), key3);
+    }
+
+    /**
+     * Tests that {@link TertiaryKeysTable#getAllKeys(String)} does not return any keys wrapped with
+     * another alias.
+     */
+    @Test
+    public void getAllKeys_withMatchingKeys_doesNotReturnKeysWrappedWithOtherAlias()
+            throws Exception {
+        mTertiaryKeysTable.addKey(generateTertiaryKey(SECONDARY_ALIAS, PACKAGE_NAME));
+        mTertiaryKeysTable.addKey(generateTertiaryKey("somekey", "generic.package.two"));
+
+        Map<String, TertiaryKey> keysByPackageName = mTertiaryKeysTable.getAllKeys(SECONDARY_ALIAS);
+
+        assertThat(keysByPackageName).hasSize(1);
+        assertThat(keysByPackageName).containsKey(PACKAGE_NAME);
+    }
+
+    private void assertTertiaryKeysEqual(TertiaryKey a, TertiaryKey b) {
+        assertThat(a.getSecondaryKeyAlias()).isEqualTo(b.getSecondaryKeyAlias());
+        assertThat(a.getPackageName()).isEqualTo(b.getPackageName());
+        assertThat(a.getWrappedKeyBytes()).isEqualTo(b.getWrappedKeyBytes());
+    }
+
+    private TertiaryKey generateTertiaryKey(String alias, String packageName) {
+        return new TertiaryKey(alias, packageName, generateRandomKey());
+    }
+
+    private byte[] generateRandomKey() {
+        return CryptoTestUtils.generateRandomBytes(KEY_SIZE_BYTES);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java b/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
new file mode 100644
index 0000000..2e8a61f
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/**
+ * Error thrown when the server's active secondary key does not exist in the user's recoverable
+ * keychain. This means the backup data cannot be decrypted, and should be wiped.
+ */
+public class ActiveSecondaryNotInKeychainException extends Exception {
+    public ActiveSecondaryNotInKeychainException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/BackupEncrypter.java b/com/android/server/backup/encryption/tasks/BackupEncrypter.java
new file mode 100644
index 0000000..95d0d97
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/BackupEncrypter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static java.util.Collections.unmodifiableList;
+
+import android.annotation.Nullable;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.crypto.SecretKey;
+
+/** Task which reads data from some source, splits it into chunks and encrypts new chunks. */
+public interface BackupEncrypter {
+    /** The algorithm which we use to compute the digest of the backup file plaintext. */
+    String MESSAGE_DIGEST_ALGORITHM = "SHA-256";
+
+    /**
+     * Splits the backup input into encrypted chunks and encrypts new chunks.
+     *
+     * @param secretKey Key used to encrypt backup.
+     * @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
+     *     full backup. Should be {@code null} for a key-value backup.
+     * @param existingChunks Set of the SHA-256 Macs of chunks the server already has.
+     * @return a result containing an array of new encrypted chunks to upload, and an ordered
+     *     listing of the chunks in the backup file.
+     * @throws IOException if a problem occurs reading from the backup data.
+     * @throws GeneralSecurityException if there is a problem encrypting the data.
+     */
+    Result backup(
+            SecretKey secretKey,
+            @Nullable byte[] fingerprintMixerSalt,
+            Set<ChunkHash> existingChunks)
+            throws IOException, GeneralSecurityException;
+
+    /**
+     * The result of an incremental backup. Contains new encrypted chunks to upload, and an ordered
+     * list of the chunks in the backup file.
+     */
+    class Result {
+        private final List<ChunkHash> mAllChunks;
+        private final List<EncryptedChunk> mNewChunks;
+        private final byte[] mDigest;
+
+        public Result(List<ChunkHash> allChunks, List<EncryptedChunk> newChunks, byte[] digest) {
+            mAllChunks = unmodifiableList(new ArrayList<>(allChunks));
+            mDigest = digest;
+            mNewChunks = unmodifiableList(new ArrayList<>(newChunks));
+        }
+
+        /**
+         * Returns an unmodifiable list of the hashes of all the chunks in the backup, in the order
+         * they appear in the plaintext.
+         */
+        public List<ChunkHash> getAllChunks() {
+            return mAllChunks;
+        }
+
+        /** Returns an unmodifiable list of the new chunks in the backup. */
+        public List<EncryptedChunk> getNewChunks() {
+            return mNewChunks;
+        }
+
+        /** Returns the message digest of the backup. */
+        public byte[] getDigest() {
+            return mDigest;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java b/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
new file mode 100644
index 0000000..9bf148d
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
+
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Locale;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+
+/**
+ * A backup file consists of, in order:
+ *
+ * <ul>
+ *   <li>A randomly ordered sequence of encrypted chunks
+ *   <li>A plaintext {@link ChunksMetadata} proto, containing the bytes of an encrypted {@link
+ *       ChunkOrdering} proto.
+ *   <li>A 64-bit long denoting the offset of the file at which the ChunkOrdering proto starts.
+ * </ul>
+ *
+ * <p>This task decrypts such a blob and writes the plaintext to another file.
+ *
+ * <p>The backup file has two formats to indicate the boundaries of the chunks in the encrypted
+ * file. In {@link ChunksMetadataProto#EXPLICIT_STARTS} mode the chunk ordering contains the start
+ * positions of each chunk and the decryptor outputs the chunks in the order they appeared in the
+ * plaintext file. In {@link ChunksMetadataProto#INLINE_LENGTHS} mode the length of each encrypted
+ * chunk is prepended to the chunk in the file and the decryptor outputs the chunks in no specific
+ * order.
+ *
+ * <p>{@link ChunksMetadataProto#EXPLICIT_STARTS} is for use with full backup (Currently used for
+ * all backups as b/77188289 is not implemented yet), {@link ChunksMetadataProto#INLINE_LENGTHS}
+ * will be used for kv backup (once b/77188289 is implemented) to avoid re-uploading the chunk
+ * ordering (see b/70782620).
+ */
+public class BackupFileDecryptorTask {
+    private static final String TAG = "BackupFileDecryptorTask";
+
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+    private static final String READ_MODE = "r";
+    private static final int BYTES_PER_LONG = 64 / BITS_PER_BYTE;
+
+    private final Cipher mCipher;
+    private final SecretKey mSecretKey;
+
+    /**
+     * A new instance.
+     *
+     * @param secretKey The tertiary key used to encrypt the backup blob.
+     */
+    public BackupFileDecryptorTask(SecretKey secretKey)
+            throws NoSuchPaddingException, NoSuchAlgorithmException {
+        this.mCipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        this.mSecretKey = secretKey;
+    }
+
+    /**
+     * Runs the task, reading the encrypted data from {@code input} and writing the plaintext data
+     * to {@code output}.
+     *
+     * @param inputFile The encrypted backup file.
+     * @param decryptedChunkOutput Unopened output to write the plaintext to, which this class will
+     *     open and close during decryption.
+     * @throws IOException if an error occurred reading the encrypted file or writing the plaintext,
+     *     or if one of the protos could not be deserialized.
+     */
+    public void decryptFile(File inputFile, DecryptedChunkOutput decryptedChunkOutput)
+            throws IOException, EncryptedRestoreException, IllegalBlockSizeException,
+                    BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException,
+                    ShortBufferException, NoSuchAlgorithmException {
+        RandomAccessFile input = new RandomAccessFile(inputFile, READ_MODE);
+
+        long metadataOffset = getChunksMetadataOffset(input);
+        ChunksMetadataProto.ChunksMetadata chunksMetadata =
+                getChunksMetadata(input, metadataOffset);
+        ChunkOrdering chunkOrdering = decryptChunkOrdering(chunksMetadata);
+
+        if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED
+                || chunksMetadata.chunkOrderingType == ChunksMetadataProto.EXPLICIT_STARTS) {
+            Slog.d(TAG, "Using explicit starts");
+            decryptFileWithExplicitStarts(
+                    input, decryptedChunkOutput, chunkOrdering, metadataOffset);
+
+        } else if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.INLINE_LENGTHS) {
+            Slog.d(TAG, "Using inline lengths");
+            decryptFileWithInlineLengths(input, decryptedChunkOutput, metadataOffset);
+
+        } else {
+            throw new UnsupportedEncryptedFileException(
+                    "Unknown chunk ordering type:" + chunksMetadata.chunkOrderingType);
+        }
+
+        if (!Arrays.equals(decryptedChunkOutput.getDigest(), chunkOrdering.checksum)) {
+            throw new MessageDigestMismatchException("Checksums did not match");
+        }
+    }
+
+    private void decryptFileWithExplicitStarts(
+            RandomAccessFile input,
+            DecryptedChunkOutput decryptedChunkOutput,
+            ChunkOrdering chunkOrdering,
+            long metadataOffset)
+            throws IOException, InvalidKeyException, IllegalBlockSizeException,
+                    InvalidAlgorithmParameterException, ShortBufferException, BadPaddingException,
+                    NoSuchAlgorithmException {
+        SparseIntArray chunkLengthsByPosition =
+                getChunkLengths(chunkOrdering.starts, (int) metadataOffset);
+        int largestChunkLength = getLargestChunkLength(chunkLengthsByPosition);
+        byte[] encryptedChunkBuffer = new byte[largestChunkLength];
+        // largestChunkLength is 0 if the backup file contains zero chunks e.g. 0 kv pairs.
+        int plaintextBufferLength =
+                Math.max(0, largestChunkLength - GCM_NONCE_LENGTH_BYTES - GCM_TAG_LENGTH_BYTES);
+        byte[] plaintextChunkBuffer = new byte[plaintextBufferLength];
+
+        try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+            for (int start : chunkOrdering.starts) {
+                int length = chunkLengthsByPosition.get(start);
+
+                input.seek(start);
+                input.readFully(encryptedChunkBuffer, 0, length);
+                int plaintextLength =
+                        decryptChunk(encryptedChunkBuffer, length, plaintextChunkBuffer);
+                outputChunk(output, plaintextChunkBuffer, plaintextLength);
+            }
+        }
+    }
+
+    private void decryptFileWithInlineLengths(
+            RandomAccessFile input, DecryptedChunkOutput decryptedChunkOutput, long metadataOffset)
+            throws MalformedEncryptedFileException, IOException, IllegalBlockSizeException,
+                    BadPaddingException, InvalidAlgorithmParameterException, ShortBufferException,
+                    InvalidKeyException, NoSuchAlgorithmException {
+        input.seek(0);
+        try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+            while (input.getFilePointer() < metadataOffset) {
+                long start = input.getFilePointer();
+                int encryptedChunkLength = input.readInt();
+
+                if (encryptedChunkLength <= 0) {
+                    // If the length of the encrypted chunk is not positive we will not make
+                    // progress reading the file and so will loop forever.
+                    throw new MalformedEncryptedFileException(
+                            "Encrypted chunk length not positive:" + encryptedChunkLength);
+                }
+
+                if (start + encryptedChunkLength > metadataOffset) {
+                    throw new MalformedEncryptedFileException(
+                            String.format(
+                                    Locale.US,
+                                    "Encrypted chunk longer (%d) than file (%d)",
+                                    encryptedChunkLength,
+                                    metadataOffset));
+                }
+
+                byte[] plaintextChunk = new byte[encryptedChunkLength];
+                byte[] plaintext =
+                        new byte
+                                [encryptedChunkLength
+                                        - GCM_NONCE_LENGTH_BYTES
+                                        - GCM_TAG_LENGTH_BYTES];
+
+                input.readFully(plaintextChunk);
+
+                int plaintextChunkLength =
+                        decryptChunk(plaintextChunk, encryptedChunkLength, plaintext);
+                outputChunk(output, plaintext, plaintextChunkLength);
+            }
+        }
+    }
+
+    private void outputChunk(
+            DecryptedChunkOutput output, byte[] plaintextChunkBuffer, int plaintextLength)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+        output.processChunk(plaintextChunkBuffer, plaintextLength);
+    }
+
+    /**
+     * Decrypts chunk and returns the length of the plaintext.
+     *
+     * @param encryptedChunkBuffer The encrypted data, prefixed by the nonce.
+     * @param encryptedChunkBufferLength The length of the encrypted chunk (including nonce).
+     * @param plaintextChunkBuffer The buffer into which to write the plaintext chunk.
+     * @return The length of the plaintext chunk.
+     */
+    private int decryptChunk(
+            byte[] encryptedChunkBuffer,
+            int encryptedChunkBufferLength,
+            byte[] plaintextChunkBuffer)
+            throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
+                    ShortBufferException, IllegalBlockSizeException {
+
+        mCipher.init(
+                Cipher.DECRYPT_MODE,
+                mSecretKey,
+                new GCMParameterSpec(
+                        GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
+                        encryptedChunkBuffer,
+                        0,
+                        GCM_NONCE_LENGTH_BYTES));
+
+        return mCipher.doFinal(
+                encryptedChunkBuffer,
+                GCM_NONCE_LENGTH_BYTES,
+                encryptedChunkBufferLength - GCM_NONCE_LENGTH_BYTES,
+                plaintextChunkBuffer);
+    }
+
+    /** Given all the lengths, returns the largest length. */
+    private int getLargestChunkLength(SparseIntArray lengths) {
+        int maxSeen = 0;
+        for (int i = 0; i < lengths.size(); i++) {
+            maxSeen = Math.max(maxSeen, lengths.valueAt(i));
+        }
+        return maxSeen;
+    }
+
+    /**
+     * From a list of the starting position of each chunk in the correct order of the backup data,
+     * calculates a mapping from start position to length of that chunk.
+     *
+     * @param starts The start positions of chunks, in order.
+     * @param chunkOrderingPosition Where the {@link ChunkOrdering} proto starts, used to calculate
+     *     the length of the last chunk.
+     * @return The mapping.
+     */
+    private SparseIntArray getChunkLengths(int[] starts, int chunkOrderingPosition) {
+        int[] boundaries = Arrays.copyOf(starts, starts.length + 1);
+        boundaries[boundaries.length - 1] = chunkOrderingPosition;
+        Arrays.sort(boundaries);
+
+        SparseIntArray lengths = new SparseIntArray();
+        for (int i = 0; i < boundaries.length - 1; i++) {
+            lengths.put(boundaries[i], boundaries[i + 1] - boundaries[i]);
+        }
+        return lengths;
+    }
+
+    /**
+     * Reads and decrypts the {@link ChunkOrdering} from the {@link ChunksMetadata}.
+     *
+     * @param metadata The metadata.
+     * @return The ordering.
+     * @throws InvalidProtocolBufferNanoException if there is an issue deserializing the proto.
+     */
+    private ChunkOrdering decryptChunkOrdering(ChunksMetadata metadata)
+            throws InvalidProtocolBufferNanoException, InvalidAlgorithmParameterException,
+                    InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
+                    UnsupportedEncryptedFileException {
+        assertCryptoSupported(metadata);
+
+        mCipher.init(
+                Cipher.DECRYPT_MODE,
+                mSecretKey,
+                new GCMParameterSpec(
+                        GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
+                        metadata.chunkOrdering,
+                        0,
+                        GCM_NONCE_LENGTH_BYTES));
+
+        byte[] decrypted =
+                mCipher.doFinal(
+                        metadata.chunkOrdering,
+                        GCM_NONCE_LENGTH_BYTES,
+                        metadata.chunkOrdering.length - GCM_NONCE_LENGTH_BYTES);
+
+        return ChunkOrdering.parseFrom(decrypted);
+    }
+
+    /**
+     * Asserts that the Cipher and MessageDigest algorithms in the backup metadata are supported.
+     * For now we only support SHA-256 for checksum and 256-bit AES/GCM/NoPadding for the Cipher.
+     *
+     * @param chunksMetadata The file metadata.
+     * @throws UnsupportedEncryptedFileException if any algorithm is unsupported.
+     */
+    private void assertCryptoSupported(ChunksMetadata chunksMetadata)
+            throws UnsupportedEncryptedFileException {
+        if (chunksMetadata.checksumType != ChunksMetadataProto.SHA_256) {
+            // For now we only support SHA-256.
+            throw new UnsupportedEncryptedFileException(
+                    "Unrecognized checksum type for backup (this version of backup only supports"
+                        + " SHA-256): "
+                            + chunksMetadata.checksumType);
+        }
+
+        if (chunksMetadata.cipherType != ChunksMetadataProto.AES_256_GCM) {
+            throw new UnsupportedEncryptedFileException(
+                    "Unrecognized cipher type for backup (this version of backup only supports"
+                        + " AES-256-GCM: "
+                            + chunksMetadata.cipherType);
+        }
+    }
+
+    /**
+     * Reads the offset of the {@link ChunksMetadata} proto from the end of the file.
+     *
+     * @return The offset.
+     * @throws IOException if there is an error reading.
+     */
+    private long getChunksMetadataOffset(RandomAccessFile input) throws IOException {
+        input.seek(input.length() - BYTES_PER_LONG);
+        return input.readLong();
+    }
+
+    /**
+     * Reads the {@link ChunksMetadata} proto from the given position in the file.
+     *
+     * @param input The encrypted file.
+     * @param position The position where the proto starts.
+     * @return The proto.
+     * @throws IOException if there is an issue reading the file or deserializing the proto.
+     */
+    private ChunksMetadata getChunksMetadata(RandomAccessFile input, long position)
+            throws IOException, MalformedEncryptedFileException {
+        long length = input.length();
+        if (position >= length || position < 0) {
+            throw new MalformedEncryptedFileException(
+                    String.format(
+                            Locale.US,
+                            "%d is not valid position for chunks metadata in file of %d bytes",
+                            position,
+                            length));
+        }
+
+        // Read chunk ordering bytes
+        input.seek(position);
+        long chunksMetadataLength = input.length() - BYTES_PER_LONG - position;
+        byte[] chunksMetadataBytes = new byte[(int) chunksMetadataLength];
+        input.readFully(chunksMetadataBytes);
+
+        try {
+            return ChunksMetadata.parseFrom(chunksMetadataBytes);
+        } catch (InvalidProtocolBufferNanoException e) {
+            throw new MalformedEncryptedFileException(
+                    String.format(
+                            Locale.US,
+                            "Could not read chunks metadata at position %d of file of %d bytes",
+                            position,
+                            length));
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java b/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
new file mode 100644
index 0000000..07a6fd2
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+import static com.android.server.backup.testing.CryptoTestUtils.newChunkOrdering;
+import static com.android.server.backup.testing.CryptoTestUtils.newChunksMetadata;
+import static com.android.server.backup.testing.CryptoTestUtils.newPair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupDataInput;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
+import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto.KeyValuePair;
+import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.nano.MessageNano;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.RandomAccessFile;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+@Config(shadows = {ShadowBackupDataInput.class})
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupFileDecryptorTaskTest {
+    private static final String READ_WRITE_MODE = "rw";
+    private static final int BYTES_PER_KILOBYTE = 1024;
+    private static final int MIN_CHUNK_SIZE_BYTES = 2 * BYTES_PER_KILOBYTE;
+    private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * BYTES_PER_KILOBYTE;
+    private static final int MAX_CHUNK_SIZE_BYTES = 64 * BYTES_PER_KILOBYTE;
+    private static final int BACKUP_DATA_SIZE_BYTES = 60 * BYTES_PER_KILOBYTE;
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+    private static final int CHECKSUM_LENGTH_BYTES = 256 / BITS_PER_BYTE;
+    @Nullable private static final FileDescriptor NULL_FILE_DESCRIPTOR = null;
+
+    private static final Set<KeyValuePair> TEST_KV_DATA = new HashSet<>();
+
+    static {
+        TEST_KV_DATA.add(newPair("key1", "value1"));
+        TEST_KV_DATA.add(newPair("key2", "value2"));
+    }
+
+    @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private SecretKey mTertiaryKey;
+    private SecretKey mChunkEncryptionKey;
+    private File mInputFile;
+    private File mOutputFile;
+    private DecryptedChunkOutput mFileOutput;
+    private DecryptedChunkKvOutput mKvOutput;
+    private Random mRandom;
+    private BackupFileDecryptorTask mTask;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mRandom = new Random();
+        mTertiaryKey = generateAesKey();
+        // In good situations it's always the same. We allow changing it for testing when somehow it
+        // has become mismatched that we throw an error.
+        mChunkEncryptionKey = mTertiaryKey;
+        mInputFile = mTemporaryFolder.newFile();
+        mOutputFile = mTemporaryFolder.newFile();
+        mFileOutput = new DecryptedChunkFileOutput(mOutputFile);
+        mKvOutput = new DecryptedChunkKvOutput(new ChunkHasher(mTertiaryKey));
+        mTask = new BackupFileDecryptorTask(mTertiaryKey);
+    }
+
+    @Test
+    public void decryptFile_throwsForNonExistentInput() throws Exception {
+        assertThrows(
+                FileNotFoundException.class,
+                () ->
+                        mTask.decryptFile(
+                                new File(mTemporaryFolder.newFolder(), "nonexistent"),
+                                mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForDirectoryInputFile() throws Exception {
+        assertThrows(
+                FileNotFoundException.class,
+                () -> mTask.decryptFile(mTemporaryFolder.newFolder(), mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_withExplicitStarts_decryptsEncryptedData() throws Exception {
+        byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
+        createEncryptedFileUsingExplicitStarts(backupData);
+
+        mTask.decryptFile(mInputFile, mFileOutput);
+
+        assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+    }
+
+    @Test
+    public void decryptFile_withInlineLengths_decryptsEncryptedData() throws Exception {
+        createEncryptedFileUsingInlineLengths(
+                TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+        mTask.decryptFile(mInputFile, mKvOutput);
+        assertThat(asMap(mKvOutput.getPairs())).containsExactlyEntriesIn(asMap(TEST_KV_DATA));
+    }
+
+    @Test
+    public void decryptFile_withNoChunkOrderingType_decryptsUsingExplicitStarts() throws Exception {
+        byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
+        createEncryptedFileUsingExplicitStarts(
+                backupData,
+                chunkOrdering -> chunkOrdering,
+                chunksMetadata -> {
+                    ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+                    metadata.chunkOrderingType =
+                            ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+                    return metadata;
+                });
+
+        mTask.decryptFile(mInputFile, mFileOutput);
+
+        assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+    }
+
+    @Test
+    public void decryptFile_withInlineLengths_throwsForZeroLengths() throws Exception {
+        createEncryptedFileUsingInlineLengths(
+                TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+
+        // Set the length of the first chunk to zero.
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(0);
+        raf.writeInt(0);
+
+        assertThrows(
+                MalformedEncryptedFileException.class,
+                () -> mTask.decryptFile(mInputFile, mKvOutput));
+    }
+
+    @Test
+    public void decryptFile_withInlineLengths_throwsForLongLengths() throws Exception {
+        createEncryptedFileUsingInlineLengths(
+                TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
+
+        // Set the length of the first chunk to zero.
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(0);
+        raf.writeInt((int) mInputFile.length());
+
+        assertThrows(
+                MalformedEncryptedFileException.class,
+                () -> mTask.decryptFile(mInputFile, mKvOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForBadKey() throws Exception {
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        assertThrows(
+                AEADBadTagException.class,
+                () ->
+                        new BackupFileDecryptorTask(generateAesKey())
+                                .decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_withExplicitStarts_throwsForMangledOrdering() throws Exception {
+        createEncryptedFileUsingExplicitStarts(
+                randomData(BACKUP_DATA_SIZE_BYTES),
+                chunkOrdering -> {
+                    ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+                    Arrays.sort(ordering.starts);
+                    return ordering;
+                });
+
+        assertThrows(
+                MessageDigestMismatchException.class,
+                () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_withExplicitStarts_noChunks_returnsNoData() throws Exception {
+        byte[] backupData = randomData(/*length=*/ 0);
+        createEncryptedFileUsingExplicitStarts(
+                backupData,
+                chunkOrdering -> {
+                    ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+                    ordering.starts = new int[0];
+                    return ordering;
+                });
+
+        mTask.decryptFile(mInputFile, mFileOutput);
+
+        assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
+    }
+
+    @Test
+    public void decryptFile_throwsForMismatchedChecksum() throws Exception {
+        createEncryptedFileUsingExplicitStarts(
+                randomData(BACKUP_DATA_SIZE_BYTES),
+                chunkOrdering -> {
+                    ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
+                    ordering.checksum =
+                            Arrays.copyOf(randomData(CHECKSUM_LENGTH_BYTES), CHECKSUM_LENGTH_BYTES);
+                    return ordering;
+                });
+
+        assertThrows(
+                MessageDigestMismatchException.class,
+                () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForBadChunksMetadataOffset() throws Exception {
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        // Replace the metadata with all 1s.
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(raf.length() - Long.BYTES);
+        int metadataOffset = (int) raf.readLong();
+        int metadataLength = (int) raf.length() - metadataOffset - Long.BYTES;
+
+        byte[] allOnes = new byte[metadataLength];
+        Arrays.fill(allOnes, (byte) 1);
+
+        raf.seek(metadataOffset);
+        raf.write(allOnes, /*off=*/ 0, metadataLength);
+
+        MalformedEncryptedFileException thrown =
+                expectThrows(
+                        MalformedEncryptedFileException.class,
+                        () -> mTask.decryptFile(mInputFile, mFileOutput));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo(
+                        "Could not read chunks metadata at position "
+                                + metadataOffset
+                                + " of file of "
+                                + raf.length()
+                                + " bytes");
+    }
+
+    @Test
+    public void decryptFile_throwsForChunksMetadataOffsetBeyondEndOfFile() throws Exception {
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(raf.length() - Long.BYTES);
+        raf.writeLong(raf.length());
+
+        MalformedEncryptedFileException thrown =
+                expectThrows(
+                        MalformedEncryptedFileException.class,
+                        () -> mTask.decryptFile(mInputFile, mFileOutput));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo(
+                        raf.length()
+                                + " is not valid position for chunks metadata in file of "
+                                + raf.length()
+                                + " bytes");
+    }
+
+    @Test
+    public void decryptFile_throwsForChunksMetadataOffsetBeforeBeginningOfFile() throws Exception {
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(raf.length() - Long.BYTES);
+        raf.writeLong(-1);
+
+        MalformedEncryptedFileException thrown =
+                expectThrows(
+                        MalformedEncryptedFileException.class,
+                        () -> mTask.decryptFile(mInputFile, mFileOutput));
+        assertThat(thrown)
+                .hasMessageThat()
+                .isEqualTo(
+                        "-1 is not valid position for chunks metadata in file of "
+                                + raf.length()
+                                + " bytes");
+    }
+
+    @Test
+    public void decryptFile_throwsForMangledChunks() throws Exception {
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        // Mess up some bits in a random byte
+        RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
+        raf.seek(50);
+        byte fiftiethByte = raf.readByte();
+        raf.seek(50);
+        raf.write(~fiftiethByte);
+
+        assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForBadChunkEncryptionKey() throws Exception {
+        mChunkEncryptionKey = generateAesKey();
+
+        createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
+
+        assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForUnsupportedCipherType() throws Exception {
+        createEncryptedFileUsingExplicitStarts(
+                randomData(BACKUP_DATA_SIZE_BYTES),
+                chunkOrdering -> chunkOrdering,
+                chunksMetadata -> {
+                    ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+                    metadata.cipherType = ChunksMetadataProto.UNKNOWN_CIPHER_TYPE;
+                    return metadata;
+                });
+
+        assertThrows(
+                UnsupportedEncryptedFileException.class,
+                () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    @Test
+    public void decryptFile_throwsForUnsupportedMessageDigestType() throws Exception {
+        createEncryptedFileUsingExplicitStarts(
+                randomData(BACKUP_DATA_SIZE_BYTES),
+                chunkOrdering -> chunkOrdering,
+                chunksMetadata -> {
+                    ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
+                    metadata.checksumType = ChunksMetadataProto.UNKNOWN_CHECKSUM_TYPE;
+                    return metadata;
+                });
+
+        assertThrows(
+                UnsupportedEncryptedFileException.class,
+                () -> mTask.decryptFile(mInputFile, mFileOutput));
+    }
+
+    /**
+     * Creates an encrypted backup file from the given data.
+     *
+     * @param data The plaintext content.
+     */
+    private void createEncryptedFileUsingExplicitStarts(byte[] data) throws Exception {
+        createEncryptedFileUsingExplicitStarts(data, chunkOrdering -> chunkOrdering);
+    }
+
+    /**
+     * Creates an encrypted backup file from the given data.
+     *
+     * @param data The plaintext content.
+     * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+     */
+    private void createEncryptedFileUsingExplicitStarts(
+            byte[] data, Transformer<ChunkOrdering> chunkOrderingTransformer) throws Exception {
+        createEncryptedFileUsingExplicitStarts(
+                data, chunkOrderingTransformer, chunksMetadata -> chunksMetadata);
+    }
+
+    /**
+     * Creates an encrypted backup file from the given data in mode {@link
+     * ChunksMetadataProto#EXPLICIT_STARTS}.
+     *
+     * @param data The plaintext content.
+     * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+     * @param chunksMetadataTransformer Transforms the metadata before it's written.
+     */
+    private void createEncryptedFileUsingExplicitStarts(
+            byte[] data,
+            Transformer<ChunkOrdering> chunkOrderingTransformer,
+            Transformer<ChunksMetadata> chunksMetadataTransformer)
+            throws Exception {
+        Result result = backupFullData(data);
+
+        ArrayList<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
+        Collections.shuffle(chunks);
+        HashMap<ChunkHash, Integer> startPositions = new HashMap<>();
+
+        try (FileOutputStream fos = new FileOutputStream(mInputFile);
+                DataOutputStream dos = new DataOutputStream(fos)) {
+            int position = 0;
+
+            for (EncryptedChunk chunk : chunks) {
+                startPositions.put(chunk.key(), position);
+                dos.write(chunk.nonce());
+                dos.write(chunk.encryptedBytes());
+                position += chunk.nonce().length + chunk.encryptedBytes().length;
+            }
+
+            int[] starts = new int[chunks.size()];
+            List<ChunkHash> chunkListing = result.getAllChunks();
+
+            for (int i = 0; i < chunks.size(); i++) {
+                starts[i] = startPositions.get(chunkListing.get(i));
+            }
+
+            ChunkOrdering chunkOrdering = newChunkOrdering(starts, result.getDigest());
+            chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
+
+            ChunksMetadata metadata =
+                    newChunksMetadata(
+                            ChunksMetadataProto.AES_256_GCM,
+                            ChunksMetadataProto.SHA_256,
+                            ChunksMetadataProto.EXPLICIT_STARTS,
+                            encrypt(chunkOrdering));
+            metadata = chunksMetadataTransformer.accept(metadata);
+
+            dos.write(MessageNano.toByteArray(metadata));
+            dos.writeLong(position);
+        }
+    }
+
+    /**
+     * Creates an encrypted backup file from the given data in mode {@link
+     * ChunksMetadataProto#INLINE_LENGTHS}.
+     *
+     * @param data The plaintext key value pairs to back up.
+     * @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
+     * @param chunksMetadataTransformer Transforms the metadata before it's written.
+     */
+    private void createEncryptedFileUsingInlineLengths(
+            Set<KeyValuePair> data,
+            Transformer<ChunkOrdering> chunkOrderingTransformer,
+            Transformer<ChunksMetadata> chunksMetadataTransformer)
+            throws Exception {
+        Result result = backupKvData(data);
+
+        List<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
+        System.out.println("we have chunk count " + chunks.size());
+        Collections.shuffle(chunks);
+
+        try (FileOutputStream fos = new FileOutputStream(mInputFile);
+                DataOutputStream dos = new DataOutputStream(fos)) {
+            for (EncryptedChunk chunk : chunks) {
+                dos.writeInt(chunk.nonce().length + chunk.encryptedBytes().length);
+                dos.write(chunk.nonce());
+                dos.write(chunk.encryptedBytes());
+            }
+
+            ChunkOrdering chunkOrdering = newChunkOrdering(null, result.getDigest());
+            chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
+
+            ChunksMetadata metadata =
+                    newChunksMetadata(
+                            ChunksMetadataProto.AES_256_GCM,
+                            ChunksMetadataProto.SHA_256,
+                            ChunksMetadataProto.INLINE_LENGTHS,
+                            encrypt(chunkOrdering));
+            metadata = chunksMetadataTransformer.accept(metadata);
+
+            int metadataStart = dos.size();
+            dos.write(MessageNano.toByteArray(metadata));
+            dos.writeLong(metadataStart);
+        }
+    }
+
+    /** Performs a full backup of the given data, and returns the chunks. */
+    private BackupEncrypter.Result backupFullData(byte[] data) throws Exception {
+        BackupStreamEncrypter encrypter =
+                new BackupStreamEncrypter(
+                        new ByteArrayInputStream(data),
+                        MIN_CHUNK_SIZE_BYTES,
+                        MAX_CHUNK_SIZE_BYTES,
+                        AVERAGE_CHUNK_SIZE_BYTES);
+        return encrypter.backup(
+                mChunkEncryptionKey,
+                randomData(FingerprintMixer.SALT_LENGTH_BYTES),
+                new HashSet<>());
+    }
+
+    private Result backupKvData(Set<KeyValuePair> data) throws Exception {
+        ShadowBackupDataInput.reset();
+        for (KeyValuePair pair : data) {
+            ShadowBackupDataInput.addEntity(pair.key, pair.value);
+        }
+        KvBackupEncrypter encrypter =
+                new KvBackupEncrypter(new BackupDataInput(NULL_FILE_DESCRIPTOR));
+        return encrypter.backup(
+                mChunkEncryptionKey,
+                randomData(FingerprintMixer.SALT_LENGTH_BYTES),
+                Collections.EMPTY_SET);
+    }
+
+    /** Encrypts {@code chunkOrdering} using {@link #mTertiaryKey}. */
+    private byte[] encrypt(ChunkOrdering chunkOrdering) throws Exception {
+        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+        byte[] nonce = randomData(GCM_NONCE_LENGTH_BYTES);
+        cipher.init(
+                Cipher.ENCRYPT_MODE,
+                mTertiaryKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
+        byte[] nanoBytes = MessageNano.toByteArray(chunkOrdering);
+        byte[] encryptedBytes = cipher.doFinal(nanoBytes);
+
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            out.write(nonce);
+            out.write(encryptedBytes);
+            return out.toByteArray();
+        }
+    }
+
+    /** Returns {@code length} random bytes. */
+    private byte[] randomData(int length) {
+        byte[] data = new byte[length];
+        mRandom.nextBytes(data);
+        return data;
+    }
+
+    private static ImmutableMap<String, String> asMap(Collection<KeyValuePair> pairs) {
+        ImmutableMap.Builder<String, String> map = ImmutableMap.builder();
+        for (KeyValuePair pair : pairs) {
+            map.put(pair.key, new String(pair.value, Charset.forName("UTF-8")));
+        }
+        return map.build();
+    }
+
+    private interface Transformer<T> {
+        T accept(T t);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/BackupStreamEncrypter.java b/com/android/server/backup/encryption/tasks/BackupStreamEncrypter.java
new file mode 100644
index 0000000..45798d3
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/BackupStreamEncrypter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.util.Slog;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkEncryptor;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker;
+import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
+import com.android.server.backup.encryption.chunking.cdc.IsChunkBreakpoint;
+import com.android.server.backup.encryption.chunking.cdc.RabinFingerprint64;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Splits backup data into variable-sized chunks using content-defined chunking, then encrypts the
+ * chunks. Given a hash of the SHA-256s of existing chunks, performs an incremental backup (i.e.,
+ * only encrypts new chunks).
+ */
+public class BackupStreamEncrypter implements BackupEncrypter {
+    private static final String TAG = "BackupStreamEncryptor";
+
+    private final InputStream mData;
+    private final int mMinChunkSizeBytes;
+    private final int mMaxChunkSizeBytes;
+    private final int mAverageChunkSizeBytes;
+
+    /**
+     * A new instance over the given distribution of chunk sizes.
+     *
+     * @param data The data to be backed up.
+     * @param minChunkSizeBytes The minimum chunk size. No chunk will be smaller than this.
+     * @param maxChunkSizeBytes The maximum chunk size. No chunk will be larger than this.
+     * @param averageChunkSizeBytes The average chunk size. The mean size of chunks will be roughly
+     *     this (with a few tens of bytes of overhead for the initialization vector and message
+     *     authentication code).
+     */
+    public BackupStreamEncrypter(
+            InputStream data,
+            int minChunkSizeBytes,
+            int maxChunkSizeBytes,
+            int averageChunkSizeBytes) {
+        this.mData = data;
+        this.mMinChunkSizeBytes = minChunkSizeBytes;
+        this.mMaxChunkSizeBytes = maxChunkSizeBytes;
+        this.mAverageChunkSizeBytes = averageChunkSizeBytes;
+    }
+
+    @Override
+    public Result backup(
+            SecretKey secretKey, byte[] fingerprintMixerSalt, Set<ChunkHash> existingChunks)
+            throws IOException, GeneralSecurityException {
+        MessageDigest messageDigest =
+                MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
+        RabinFingerprint64 rabinFingerprint64 = new RabinFingerprint64();
+        FingerprintMixer fingerprintMixer = new FingerprintMixer(secretKey, fingerprintMixerSalt);
+        IsChunkBreakpoint isChunkBreakpoint =
+                new IsChunkBreakpoint(mAverageChunkSizeBytes - mMinChunkSizeBytes);
+        ContentDefinedChunker chunker =
+                new ContentDefinedChunker(
+                        mMinChunkSizeBytes,
+                        mMaxChunkSizeBytes,
+                        rabinFingerprint64,
+                        fingerprintMixer,
+                        isChunkBreakpoint);
+        ChunkHasher chunkHasher = new ChunkHasher(secretKey);
+        ChunkEncryptor encryptor = new ChunkEncryptor(secretKey, new SecureRandom());
+        Set<ChunkHash> includedChunks = new HashSet<>();
+        // New chunks will be added only once to this list, even if they occur multiple times.
+        List<EncryptedChunk> newChunks = new ArrayList<>();
+        // All chunks (including multiple occurrences) will be added to the chunkListing.
+        List<ChunkHash> chunkListing = new ArrayList<>();
+
+        includedChunks.addAll(existingChunks);
+
+        chunker.chunkify(
+                mData,
+                chunk -> {
+                    messageDigest.update(chunk);
+                    ChunkHash key = chunkHasher.computeHash(chunk);
+
+                    if (!includedChunks.contains(key)) {
+                        newChunks.add(encryptor.encrypt(key, chunk));
+                        includedChunks.add(key);
+                    }
+                    chunkListing.add(key);
+                });
+
+        Slog.i(
+                TAG,
+                String.format(
+                        "Chunks: %d total, %d unique, %d new",
+                        chunkListing.size(), new HashSet<>(chunkListing).size(), newChunks.size()));
+        return new Result(
+                Collections.unmodifiableList(chunkListing),
+                Collections.unmodifiableList(newChunks),
+                messageDigest.digest());
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/BackupStreamEncrypterTest.java b/com/android/server/backup/encryption/tasks/BackupStreamEncrypterTest.java
new file mode 100644
index 0000000..21c4e07
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/BackupStreamEncrypterTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.backup.testing.RandomInputStream;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+
+import javax.crypto.SecretKey;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupStreamEncrypterTest {
+    private static final int SALT_LENGTH = 32;
+    private static final int BITS_PER_BYTE = 8;
+    private static final int BYTES_PER_KILOBYTE = 1024;
+    private static final int BYTES_PER_MEGABYTE = 1024 * 1024;
+    private static final int MIN_CHUNK_SIZE = 2 * BYTES_PER_KILOBYTE;
+    private static final int AVERAGE_CHUNK_SIZE = 4 * BYTES_PER_KILOBYTE;
+    private static final int MAX_CHUNK_SIZE = 64 * BYTES_PER_KILOBYTE;
+    private static final int BACKUP_SIZE = 2 * BYTES_PER_MEGABYTE;
+    private static final int SMALL_BACKUP_SIZE = BYTES_PER_KILOBYTE;
+    // 16 bytes for the mac. iv is encoded in a separate field.
+    private static final int BYTES_OVERHEAD_PER_CHUNK = 16;
+    private static final int MESSAGE_DIGEST_SIZE_IN_BYTES = 256 / BITS_PER_BYTE;
+    private static final int RANDOM_SEED = 42;
+    private static final double TOLERANCE = 0.1;
+
+    private Random mRandom;
+    private SecretKey mSecretKey;
+    private byte[] mSalt;
+
+    @Before
+    public void setUp() throws Exception {
+        mSecretKey = CryptoTestUtils.generateAesKey();
+
+        mSalt = new byte[SALT_LENGTH];
+        // Make these tests deterministic
+        mRandom = new Random(RANDOM_SEED);
+        mRandom.nextBytes(mSalt);
+    }
+
+    @Test
+    public void testBackup_producesChunksOfTheGivenAverageSize() throws Exception {
+        BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
+
+        long totalSize = 0;
+        for (EncryptedChunk chunk : result.getNewChunks()) {
+            totalSize += chunk.encryptedBytes().length;
+        }
+
+        double meanSize = totalSize / result.getNewChunks().size();
+        double expectedChunkSize = AVERAGE_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK;
+        assertThat(Math.abs(meanSize - expectedChunkSize) / expectedChunkSize)
+                .isLessThan(TOLERANCE);
+    }
+
+    @Test
+    public void testBackup_producesNoChunksSmallerThanMinSize() throws Exception {
+        BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
+        List<EncryptedChunk> chunks = result.getNewChunks();
+
+        // Last chunk could be smaller, depending on the file size and how it is chunked
+        for (EncryptedChunk chunk : chunks.subList(0, chunks.size() - 2)) {
+            assertThat(chunk.encryptedBytes().length)
+                    .isAtLeast(MIN_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
+        }
+    }
+
+    @Test
+    public void testBackup_producesNoChunksLargerThanMaxSize() throws Exception {
+        BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
+        List<EncryptedChunk> chunks = result.getNewChunks();
+
+        for (EncryptedChunk chunk : chunks) {
+            assertThat(chunk.encryptedBytes().length)
+                    .isAtMost(MAX_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
+        }
+    }
+
+    @Test
+    public void testBackup_producesAFileOfTheExpectedSize() throws Exception {
+        BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
+        HashMap<ChunkHash, EncryptedChunk> chunksBySha256 =
+                chunksIndexedByKey(result.getNewChunks());
+
+        int expectedSize = BACKUP_SIZE + result.getAllChunks().size() * BYTES_OVERHEAD_PER_CHUNK;
+        int size = 0;
+        for (ChunkHash byteString : result.getAllChunks()) {
+            size += chunksBySha256.get(byteString).encryptedBytes().length;
+        }
+        assertThat(size).isEqualTo(expectedSize);
+    }
+
+    @Test
+    public void testBackup_forSameFile_producesNoNewChunks() throws Exception {
+        byte[] backupData = getRandomData(BACKUP_SIZE);
+        BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
+
+        BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
+
+        assertThat(incrementalResult.getNewChunks()).isEmpty();
+    }
+
+    @Test
+    public void testBackup_onlyUpdatesChangedChunks() throws Exception {
+        byte[] backupData = getRandomData(BACKUP_SIZE);
+        BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
+
+        // Let's update the 2nd and 5th chunk
+        backupData[positionOfChunk(result, 1)]++;
+        backupData[positionOfChunk(result, 4)]++;
+        BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
+
+        assertThat(incrementalResult.getNewChunks()).hasSize(2);
+    }
+
+    @Test
+    public void testBackup_doesNotIncludeUpdatedChunksInNewListing() throws Exception {
+        byte[] backupData = getRandomData(BACKUP_SIZE);
+        BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
+
+        // Let's update the 2nd and 5th chunk
+        backupData[positionOfChunk(result, 1)]++;
+        backupData[positionOfChunk(result, 4)]++;
+        BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
+
+        List<EncryptedChunk> newChunks = incrementalResult.getNewChunks();
+        List<ChunkHash> chunkListing = result.getAllChunks();
+        assertThat(newChunks).doesNotContain(chunkListing.get(1));
+        assertThat(newChunks).doesNotContain(chunkListing.get(4));
+    }
+
+    @Test
+    public void testBackup_includesUnchangedChunksInNewListing() throws Exception {
+        byte[] backupData = getRandomData(BACKUP_SIZE);
+        BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
+
+        // Let's update the 2nd and 5th chunk
+        backupData[positionOfChunk(result, 1)]++;
+        backupData[positionOfChunk(result, 4)]++;
+        BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
+
+        HashSet<ChunkHash> chunksPresentInIncremental =
+                new HashSet<>(incrementalResult.getAllChunks());
+        chunksPresentInIncremental.removeAll(result.getAllChunks());
+
+        assertThat(chunksPresentInIncremental).hasSize(2);
+    }
+
+    @Test
+    public void testBackup_forSameData_createsSameDigest() throws Exception {
+        byte[] backupData = getRandomData(SMALL_BACKUP_SIZE);
+
+        BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
+        BackupEncrypter.Result result2 = runBackup(backupData, ImmutableList.of());
+        assertThat(result.getDigest()).isEqualTo(result2.getDigest());
+    }
+
+    @Test
+    public void testBackup_forDifferentData_createsDifferentDigest() throws Exception {
+        byte[] backup1Data = getRandomData(SMALL_BACKUP_SIZE);
+        byte[] backup2Data = getRandomData(SMALL_BACKUP_SIZE);
+
+        BackupEncrypter.Result result = runBackup(backup1Data, ImmutableList.of());
+        BackupEncrypter.Result result2 = runBackup(backup2Data, ImmutableList.of());
+        assertThat(result.getDigest()).isNotEqualTo(result2.getDigest());
+    }
+
+    @Test
+    public void testBackup_createsDigestOf32Bytes() throws Exception {
+        assertThat(runBackup(getRandomData(SMALL_BACKUP_SIZE), ImmutableList.of()).getDigest())
+                .hasLength(MESSAGE_DIGEST_SIZE_IN_BYTES);
+    }
+
+    private byte[] getRandomData(int size) throws Exception {
+        RandomInputStream randomInputStream = new RandomInputStream(mRandom, size);
+        byte[] backupData = new byte[size];
+        randomInputStream.read(backupData);
+        return backupData;
+    }
+
+    private BackupEncrypter.Result runBackup(int backupSize) throws Exception {
+        RandomInputStream dataStream = new RandomInputStream(mRandom, backupSize);
+        BackupStreamEncrypter task =
+                new BackupStreamEncrypter(
+                        dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
+        return task.backup(mSecretKey, mSalt, ImmutableSet.of());
+    }
+
+    private BackupEncrypter.Result runBackup(byte[] data, List<ChunkHash> existingChunks)
+            throws Exception {
+        ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
+        BackupStreamEncrypter task =
+                new BackupStreamEncrypter(
+                        dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
+        return task.backup(mSecretKey, mSalt, ImmutableSet.copyOf(existingChunks));
+    }
+
+    /** Returns a {@link HashMap} of the chunks, indexed by the SHA-256 Mac key. */
+    private static HashMap<ChunkHash, EncryptedChunk> chunksIndexedByKey(
+            List<EncryptedChunk> chunks) {
+        HashMap<ChunkHash, EncryptedChunk> chunksByKey = new HashMap<>();
+        for (EncryptedChunk chunk : chunks) {
+            chunksByKey.put(chunk.key(), chunk);
+        }
+        return chunksByKey;
+    }
+
+    /**
+     * Returns the start position of the chunk in the plaintext backup data.
+     *
+     * @param result The result from a backup.
+     * @param index The index of the chunk in question.
+     * @return the start position.
+     */
+    private static int positionOfChunk(BackupEncrypter.Result result, int index) {
+        HashMap<ChunkHash, EncryptedChunk> byKey = chunksIndexedByKey(result.getNewChunks());
+        List<ChunkHash> listing = result.getAllChunks();
+
+        int position = 0;
+        for (int i = 0; i < index - 1; i++) {
+            EncryptedChunk chunk = byKey.get(listing.get(i));
+            position += chunk.encryptedBytes().length - BYTES_OVERHEAD_PER_CHUNK;
+        }
+
+        return position;
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java b/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java
new file mode 100644
index 0000000..8f35db6
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/ClearCryptoStateTask.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.storage.BackupEncryptionDb;
+import com.android.server.backup.encryption.storage.EncryptionDbException;
+
+import java.io.IOException;
+
+/**
+ * Task to clear local crypto state.
+ *
+ * <p>Needs to run whenever the user changes their backup account.
+ */
+public class ClearCryptoStateTask {
+    private static final String TAG = "ClearCryptoStateTask";
+
+    private final Context mContext;
+    private final CryptoSettings mCryptoSettings;
+
+    /**
+     * A new instance.
+     *
+     * @param context for finding local storage.
+     * @param cryptoSettings to clear
+     */
+    public ClearCryptoStateTask(Context context, CryptoSettings cryptoSettings) {
+        mContext = context;
+        mCryptoSettings = cryptoSettings;
+    }
+
+    /** Deletes all local state for backup (not restore). */
+    public void run() {
+        Slog.d(TAG, "Clearing local crypto state.");
+        try {
+            BackupEncryptionDb.newInstance(mContext).clear();
+        } catch (EncryptionDbException e) {
+            Slog.e(TAG, "Error clearing encryption database", e);
+        }
+        mCryptoSettings.clearAllSettingsForBackup();
+        try {
+            ProtoStore.createChunkListingStore(mContext).deleteAllProtos();
+        } catch (IOException e) {
+            Slog.e(TAG, "Error clearing chunk listing store", e);
+        }
+        try {
+            ProtoStore.createKeyValueListingStore(mContext).deleteAllProtos();
+        } catch (IOException e) {
+            Slog.e(TAG, "Error clearing key-value store", e);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java b/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java
new file mode 100644
index 0000000..81bfce1
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/ClearCryptoStateTaskTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto.KeyValueListing;
+import com.android.server.backup.encryption.storage.BackupEncryptionDb;
+import com.android.server.backup.encryption.storage.TertiaryKey;
+import com.android.server.backup.encryption.storage.TertiaryKeysTable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ClearCryptoStateTaskTest {
+    private static final String TEST_PACKAGE_NAME = "com.android.example";
+
+    private ClearCryptoStateTask mClearCryptoStateTask;
+    private CryptoSettings mCryptoSettings;
+    private Context mApplication;
+
+    @Before
+    public void setUp() {
+        mApplication = ApplicationProvider.getApplicationContext();
+        mCryptoSettings = spy(CryptoSettings.getInstanceForTesting(mApplication));
+        mClearCryptoStateTask = new ClearCryptoStateTask(mApplication, mCryptoSettings);
+    }
+
+    @Test
+    public void run_clearsChunkListingProtoState() throws Exception {
+        String packageName = TEST_PACKAGE_NAME;
+        ChunkListing chunkListing = new ChunkListing();
+        ProtoStore.createChunkListingStore(mApplication).saveProto(packageName, chunkListing);
+
+        mClearCryptoStateTask.run();
+
+        assertThat(
+                        ProtoStore.createChunkListingStore(mApplication)
+                                .loadProto(packageName)
+                                .isPresent())
+                .isFalse();
+    }
+
+    @Test
+    public void run_clearsKeyValueProtoState() throws Exception {
+        String packageName = TEST_PACKAGE_NAME;
+        KeyValueListing keyValueListing = new KeyValueListing();
+        ProtoStore.createKeyValueListingStore(mApplication).saveProto(packageName, keyValueListing);
+
+        mClearCryptoStateTask.run();
+
+        assertThat(
+                        ProtoStore.createKeyValueListingStore(mApplication)
+                                .loadProto(packageName)
+                                .isPresent())
+                .isFalse();
+    }
+
+    @Test
+    public void run_clearsTertiaryKeysTable() throws Exception {
+        String secondaryKeyAlias = "bob";
+        TertiaryKeysTable tertiaryKeysTable =
+                BackupEncryptionDb.newInstance(mApplication).getTertiaryKeysTable();
+        tertiaryKeysTable.addKey(
+                new TertiaryKey(
+                        secondaryKeyAlias, "packageName", /*wrappedKeyBytes=*/ new byte[0]));
+
+        mClearCryptoStateTask.run();
+
+        assertThat(tertiaryKeysTable.getAllKeys(secondaryKeyAlias)).isEmpty();
+    }
+
+    @Test
+    public void run_clearsSettings() {
+        mCryptoSettings.setSecondaryLastRotated(100001);
+
+        mClearCryptoStateTask.run();
+
+        assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java b/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
new file mode 100644
index 0000000..f67f100
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
+ * of the message digest of the chunks.
+ */
+public interface DecryptedChunkOutput extends Closeable {
+    /**
+     * Opens whatever output the implementation chooses, ready to process chunks.
+     *
+     * @return {@code this}, to allow use with try-with-resources
+     */
+    DecryptedChunkOutput open() throws IOException, NoSuchAlgorithmException;
+
+    /**
+     * Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
+     * updates the digest with the chunk.
+     *
+     * <p>You must call {@link #open()} before this method, and you may not call it after calling
+     * {@link Closeable#close()}.
+     *
+     * @param plaintextBuffer An array containing the bytes of the plaintext of the chunk, starting
+     *     at index 0.
+     * @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
+     */
+    void processChunk(byte[] plaintextBuffer, int length)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException;
+
+    /**
+     * Returns the message digest of all the chunks processed by {@link #processChunk}.
+     *
+     * <p>You must call {@link Closeable#close()} before calling this method.
+     */
+    byte[] getDigest() throws NoSuchAlgorithmException;
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java b/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java
new file mode 100644
index 0000000..ef13f23
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedBackupTask.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.annotation.Nullable;
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.BackupFileBuilder;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+
+/**
+ * Task which reads encrypted chunks from a {@link BackupEncrypter}, builds a backup file and
+ * uploads it to the server.
+ */
+@TargetApi(VERSION_CODES.P)
+public class EncryptedBackupTask {
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+
+    private static final String TAG = "EncryptedBackupTask";
+
+    private final CryptoBackupServer mCryptoBackupServer;
+    private final SecureRandom mSecureRandom;
+    private final String mPackageName;
+    private final ByteArrayOutputStream mBackupDataOutput;
+    private final BackupEncrypter mBackupEncrypter;
+    private final AtomicBoolean mCancelled;
+
+    /** Creates a new instance which reads data from the given input stream. */
+    public EncryptedBackupTask(
+            CryptoBackupServer cryptoBackupServer,
+            SecureRandom secureRandom,
+            String packageName,
+            BackupEncrypter backupEncrypter) {
+        mCryptoBackupServer = cryptoBackupServer;
+        mSecureRandom = secureRandom;
+        mPackageName = packageName;
+        mBackupEncrypter = backupEncrypter;
+
+        mBackupDataOutput = new ByteArrayOutputStream();
+        mCancelled = new AtomicBoolean(false);
+    }
+
+    /**
+     * Creates a non-incremental backup file and uploads it to the server.
+     *
+     * @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
+     *     full backup. May be {@code null} for a key-value backup.
+     */
+    public ChunksMetadataProto.ChunkListing performNonIncrementalBackup(
+            SecretKey tertiaryKey,
+            WrappedKeyProto.WrappedKey wrappedTertiaryKey,
+            @Nullable byte[] fingerprintMixerSalt)
+            throws IOException, GeneralSecurityException {
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                performBackup(
+                        tertiaryKey,
+                        fingerprintMixerSalt,
+                        BackupFileBuilder.createForNonIncremental(mBackupDataOutput),
+                        new HashSet<>());
+
+        throwIfCancelled();
+
+        newChunkListing.documentId =
+                mCryptoBackupServer.uploadNonIncrementalBackup(
+                        mPackageName, mBackupDataOutput.toByteArray(), wrappedTertiaryKey);
+
+        return newChunkListing;
+    }
+
+    /** Creates an incremental backup file and uploads it to the server. */
+    public ChunksMetadataProto.ChunkListing performIncrementalBackup(
+            SecretKey tertiaryKey,
+            WrappedKeyProto.WrappedKey wrappedTertiaryKey,
+            ChunksMetadataProto.ChunkListing oldChunkListing)
+            throws IOException, GeneralSecurityException {
+
+        ChunksMetadataProto.ChunkListing newChunkListing =
+                performBackup(
+                        tertiaryKey,
+                        oldChunkListing.fingerprintMixerSalt,
+                        BackupFileBuilder.createForIncremental(mBackupDataOutput, oldChunkListing),
+                        getChunkHashes(oldChunkListing));
+
+        throwIfCancelled();
+
+        String oldDocumentId = oldChunkListing.documentId;
+        Slog.v(TAG, "Old doc id: " + oldDocumentId);
+
+        newChunkListing.documentId =
+                mCryptoBackupServer.uploadIncrementalBackup(
+                        mPackageName,
+                        oldDocumentId,
+                        mBackupDataOutput.toByteArray(),
+                        wrappedTertiaryKey);
+        return newChunkListing;
+    }
+
+    /**
+     * Signals to the task that the backup has been cancelled. If the upload has not yet started
+     * then the task will not upload any data to the server or save the new chunk listing.
+     */
+    public void cancel() {
+        mCancelled.getAndSet(true);
+    }
+
+    private void throwIfCancelled() {
+        if (mCancelled.get()) {
+            throw new CancellationException("EncryptedBackupTask was cancelled");
+        }
+    }
+
+    private ChunksMetadataProto.ChunkListing performBackup(
+            SecretKey tertiaryKey,
+            @Nullable byte[] fingerprintMixerSalt,
+            BackupFileBuilder backupFileBuilder,
+            Set<ChunkHash> existingChunkHashes)
+            throws IOException, GeneralSecurityException {
+        BackupEncrypter.Result result =
+                mBackupEncrypter.backup(tertiaryKey, fingerprintMixerSalt, existingChunkHashes);
+        backupFileBuilder.writeChunks(result.getAllChunks(), buildChunkMap(result.getNewChunks()));
+
+        ChunksMetadataProto.ChunkOrdering chunkOrdering =
+                backupFileBuilder.getNewChunkOrdering(result.getDigest());
+        backupFileBuilder.finish(buildMetadata(tertiaryKey, chunkOrdering));
+
+        return backupFileBuilder.getNewChunkListing(fingerprintMixerSalt);
+    }
+
+    /** Returns a set containing the hashes of every chunk in the given listing. */
+    private static Set<ChunkHash> getChunkHashes(ChunksMetadataProto.ChunkListing chunkListing) {
+        Set<ChunkHash> hashes = new HashSet<>();
+        for (ChunksMetadataProto.Chunk chunk : chunkListing.chunks) {
+            hashes.add(new ChunkHash(chunk.hash));
+        }
+        return hashes;
+    }
+
+    /** Returns a map from chunk hash to chunk containing every chunk in the given list. */
+    private static Map<ChunkHash, EncryptedChunk> buildChunkMap(List<EncryptedChunk> chunks) {
+        Map<ChunkHash, EncryptedChunk> chunkMap = new HashMap<>();
+        for (EncryptedChunk chunk : chunks) {
+            chunkMap.put(chunk.key(), chunk);
+        }
+        return chunkMap;
+    }
+
+    private ChunksMetadataProto.ChunksMetadata buildMetadata(
+            SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
+            throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
+                    InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+                    ShortBufferException, NoSuchPaddingException {
+        ChunksMetadataProto.ChunksMetadata metaData = new ChunksMetadataProto.ChunksMetadata();
+        metaData.cipherType = ChunksMetadataProto.AES_256_GCM;
+        metaData.checksumType = ChunksMetadataProto.SHA_256;
+        metaData.chunkOrdering = encryptChunkOrdering(tertiaryKey, chunkOrdering);
+        return metaData;
+    }
+
+    private byte[] encryptChunkOrdering(
+            SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
+            throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
+                    NoSuchPaddingException, NoSuchAlgorithmException,
+                    InvalidAlgorithmParameterException, ShortBufferException {
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+        byte[] nonce = generateNonce();
+
+        cipher.init(
+                Cipher.ENCRYPT_MODE,
+                tertiaryKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
+
+        byte[] orderingBytes = ChunksMetadataProto.ChunkOrdering.toByteArray(chunkOrdering);
+        // We prepend the nonce to the ordering.
+        byte[] output =
+                Arrays.copyOf(
+                        nonce,
+                        GCM_NONCE_LENGTH_BYTES + orderingBytes.length + GCM_TAG_LENGTH_BYTES);
+
+        cipher.doFinal(
+                orderingBytes,
+                /*inputOffset=*/ 0,
+                /*inputLen=*/ orderingBytes.length,
+                output,
+                /*outputOffset=*/ GCM_NONCE_LENGTH_BYTES);
+
+        return output;
+    }
+
+    private byte[] generateNonce() {
+        byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
+        mSecureRandom.nextBytes(nonce);
+        return nonce;
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java b/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java
new file mode 100644
index 0000000..f6914ef
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.AES_256_GCM;
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
+import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.SHA_256;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.BackupFileBuilder;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.chunking.EncryptedChunkEncoder;
+import com.android.server.backup.encryption.chunking.LengthlessEncryptedChunkEncoder;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.TertiaryKeyGenerator;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto.WrappedKey;
+import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
+import com.android.server.backup.testing.CryptoTestUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.nano.MessageNano;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.io.OutputStream;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+@Config(shadows = {EncryptedBackupTaskTest.ShadowBackupFileBuilder.class})
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class EncryptedBackupTaskTest {
+
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_NONCE_LENGTH_BYTES = 12;
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+
+    private static final byte[] TEST_FINGERPRINT_MIXER_SALT =
+            Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
+
+    private static final byte[] TEST_NONCE =
+            Arrays.copyOf(new byte[] {55}, EncryptedChunk.NONCE_LENGTH_BYTES);
+
+    private static final ChunkHash TEST_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_3 =
+            new ChunkHash(Arrays.copyOf(new byte[] {3}, ChunkHash.HASH_LENGTH_BYTES));
+
+    private static final EncryptedChunk TEST_CHUNK_1 =
+            EncryptedChunk.create(TEST_HASH_1, TEST_NONCE, new byte[] {1, 2, 3, 4, 5});
+    private static final EncryptedChunk TEST_CHUNK_2 =
+            EncryptedChunk.create(TEST_HASH_2, TEST_NONCE, new byte[] {6, 7, 8, 9, 10});
+    private static final EncryptedChunk TEST_CHUNK_3 =
+            EncryptedChunk.create(TEST_HASH_3, TEST_NONCE, new byte[] {11, 12, 13, 14, 15});
+
+    private static final byte[] TEST_CHECKSUM = Arrays.copyOf(new byte[] {10}, 258 / 8);
+    private static final String TEST_PACKAGE_NAME = "com.example.package";
+    private static final String TEST_OLD_DOCUMENT_ID = "old_doc_1";
+    private static final String TEST_NEW_DOCUMENT_ID = "new_doc_1";
+
+    @Captor private ArgumentCaptor<ChunksMetadata> mMetadataCaptor;
+
+    @Mock private CryptoBackupServer mCryptoBackupServer;
+    @Mock private BackupEncrypter mBackupEncrypter;
+    @Mock private BackupFileBuilder mBackupFileBuilder;
+
+    private ChunkListing mOldChunkListing;
+    private SecretKey mTertiaryKey;
+    private WrappedKey mWrappedTertiaryKey;
+    private EncryptedChunkEncoder mEncryptedChunkEncoder;
+    private EncryptedBackupTask mTask;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        SecureRandom secureRandom = new SecureRandom();
+        mTertiaryKey = new TertiaryKeyGenerator(secureRandom).generate();
+        mWrappedTertiaryKey = new WrappedKey();
+
+        mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
+
+        ShadowBackupFileBuilder.sInstance = mBackupFileBuilder;
+
+        mTask =
+                new EncryptedBackupTask(
+                        mCryptoBackupServer, secureRandom, TEST_PACKAGE_NAME, mBackupEncrypter);
+    }
+
+    @Test
+    public void performNonIncrementalBackup_performsBackup() throws Exception {
+        setUpWithoutExistingBackup();
+
+        // Chunk listing and ordering don't matter for this test.
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
+                .thenReturn(TEST_NEW_DOCUMENT_ID);
+
+        mTask.performNonIncrementalBackup(
+                mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
+
+        verify(mBackupFileBuilder)
+                .writeChunks(
+                        ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
+                        ImmutableMap.of(TEST_HASH_1, TEST_CHUNK_1, TEST_HASH_2, TEST_CHUNK_2));
+        verify(mBackupFileBuilder).finish(any());
+        verify(mCryptoBackupServer)
+                .uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), eq(mWrappedTertiaryKey));
+    }
+
+    @Test
+    public void performIncrementalBackup_performsBackup() throws Exception {
+        setUpWithExistingBackup();
+
+        // Chunk listing and ordering don't matter for this test.
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        when(mCryptoBackupServer.uploadIncrementalBackup(
+                        eq(TEST_PACKAGE_NAME), eq(TEST_OLD_DOCUMENT_ID), any(), any()))
+                .thenReturn(TEST_NEW_DOCUMENT_ID);
+
+        mTask.performIncrementalBackup(mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing);
+
+        verify(mBackupFileBuilder)
+                .writeChunks(
+                        ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
+                        ImmutableMap.of(TEST_HASH_2, TEST_CHUNK_2));
+        verify(mBackupFileBuilder).finish(any());
+        verify(mCryptoBackupServer)
+                .uploadIncrementalBackup(
+                        eq(TEST_PACKAGE_NAME),
+                        eq(TEST_OLD_DOCUMENT_ID),
+                        any(),
+                        eq(mWrappedTertiaryKey));
+    }
+
+    @Test
+    public void performIncrementalBackup_returnsNewChunkListingWithDocId() throws Exception {
+        setUpWithExistingBackup();
+
+        ChunkListing chunkListingWithoutDocId =
+                CryptoTestUtils.newChunkListingWithoutDocId(
+                        TEST_FINGERPRINT_MIXER_SALT,
+                        AES_256_GCM,
+                        CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                        createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
+                        createChunkProtoFor(TEST_HASH_2, TEST_CHUNK_2));
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(chunkListingWithoutDocId);
+
+        // Chunk ordering doesn't matter for this test.
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        when(mCryptoBackupServer.uploadIncrementalBackup(
+                        eq(TEST_PACKAGE_NAME), eq(TEST_OLD_DOCUMENT_ID), any(), any()))
+                .thenReturn(TEST_NEW_DOCUMENT_ID);
+
+        ChunkListing actualChunkListing =
+                mTask.performIncrementalBackup(mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing);
+
+        ChunkListing expectedChunkListing = CryptoTestUtils.clone(chunkListingWithoutDocId);
+        expectedChunkListing.documentId = TEST_NEW_DOCUMENT_ID;
+        assertChunkListingsAreEqual(actualChunkListing, expectedChunkListing);
+    }
+
+    @Test
+    public void performNonIncrementalBackup_returnsNewChunkListingWithDocId() throws Exception {
+        setUpWithoutExistingBackup();
+
+        ChunkListing chunkListingWithoutDocId =
+                CryptoTestUtils.newChunkListingWithoutDocId(
+                        TEST_FINGERPRINT_MIXER_SALT,
+                        AES_256_GCM,
+                        CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                        createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
+                        createChunkProtoFor(TEST_HASH_2, TEST_CHUNK_2));
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(chunkListingWithoutDocId);
+
+        // Chunk ordering doesn't matter for this test.
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
+                .thenReturn(TEST_NEW_DOCUMENT_ID);
+
+        ChunkListing actualChunkListing =
+                mTask.performNonIncrementalBackup(
+                        mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
+
+        ChunkListing expectedChunkListing = CryptoTestUtils.clone(chunkListingWithoutDocId);
+        expectedChunkListing.documentId = TEST_NEW_DOCUMENT_ID;
+        assertChunkListingsAreEqual(actualChunkListing, expectedChunkListing);
+    }
+
+    @Test
+    public void performNonIncrementalBackup_buildsCorrectChunkMetadata() throws Exception {
+        setUpWithoutExistingBackup();
+
+        // Chunk listing doesn't matter for this test.
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
+
+        ChunkOrdering expectedOrdering =
+                CryptoTestUtils.newChunkOrdering(new int[10], TEST_CHECKSUM);
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(expectedOrdering);
+
+        when(mCryptoBackupServer.uploadNonIncrementalBackup(eq(TEST_PACKAGE_NAME), any(), any()))
+                .thenReturn(TEST_NEW_DOCUMENT_ID);
+
+        mTask.performNonIncrementalBackup(
+                mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT);
+
+        verify(mBackupFileBuilder).finish(mMetadataCaptor.capture());
+
+        ChunksMetadata actualMetadata = mMetadataCaptor.getValue();
+        assertThat(actualMetadata.checksumType).isEqualTo(SHA_256);
+        assertThat(actualMetadata.cipherType).isEqualTo(AES_256_GCM);
+
+        ChunkOrdering actualOrdering = decryptChunkOrdering(actualMetadata.chunkOrdering);
+        assertThat(actualOrdering.checksum).isEqualTo(TEST_CHECKSUM);
+        assertThat(actualOrdering.starts).isEqualTo(expectedOrdering.starts);
+    }
+
+    @Test
+    public void cancel_incrementalBackup_doesNotUploadOrSaveChunkListing() throws Exception {
+        setUpWithExistingBackup();
+
+        // Chunk listing and ordering don't matter for this test.
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        mTask.cancel();
+        assertThrows(
+                CancellationException.class,
+                () ->
+                        mTask.performIncrementalBackup(
+                                mTertiaryKey, mWrappedTertiaryKey, mOldChunkListing));
+
+        verify(mCryptoBackupServer, never()).uploadIncrementalBackup(any(), any(), any(), any());
+        verify(mCryptoBackupServer, never()).uploadNonIncrementalBackup(any(), any(), any());
+    }
+
+    @Test
+    public void cancel_nonIncrementalBackup_doesNotUploadOrSaveChunkListing() throws Exception {
+        setUpWithoutExistingBackup();
+
+        // Chunk listing and ordering don't matter for this test.
+        when(mBackupFileBuilder.getNewChunkListing(any())).thenReturn(new ChunkListing());
+        when(mBackupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM)).thenReturn(new ChunkOrdering());
+
+        mTask.cancel();
+        assertThrows(
+                CancellationException.class,
+                () ->
+                        mTask.performNonIncrementalBackup(
+                                mTertiaryKey, mWrappedTertiaryKey, TEST_FINGERPRINT_MIXER_SALT));
+
+        verify(mCryptoBackupServer, never()).uploadIncrementalBackup(any(), any(), any(), any());
+        verify(mCryptoBackupServer, never()).uploadNonIncrementalBackup(any(), any(), any());
+    }
+
+    /** Sets up a backup of [CHUNK 1][CHUNK 2] with no existing data. */
+    private void setUpWithoutExistingBackup() throws Exception {
+        Result result =
+                new Result(
+                        ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
+                        ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_2),
+                        TEST_CHECKSUM);
+        when(mBackupEncrypter.backup(any(), eq(TEST_FINGERPRINT_MIXER_SALT), eq(ImmutableSet.of())))
+                .thenReturn(result);
+    }
+
+    /**
+     * Sets up a backup of [CHUNK 1][CHUNK 2][CHUNK 3] where the previous backup contained [CHUNK
+     * 1][CHUNK 3].
+     */
+    private void setUpWithExistingBackup() throws Exception {
+        mOldChunkListing =
+                CryptoTestUtils.newChunkListing(
+                        TEST_OLD_DOCUMENT_ID,
+                        TEST_FINGERPRINT_MIXER_SALT,
+                        AES_256_GCM,
+                        CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                        createChunkProtoFor(TEST_HASH_1, TEST_CHUNK_1),
+                        createChunkProtoFor(TEST_HASH_3, TEST_CHUNK_3));
+
+        Result result =
+                new Result(
+                        ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
+                        ImmutableList.of(TEST_CHUNK_2),
+                        TEST_CHECKSUM);
+        when(mBackupEncrypter.backup(
+                        any(),
+                        eq(TEST_FINGERPRINT_MIXER_SALT),
+                        eq(ImmutableSet.of(TEST_HASH_1, TEST_HASH_3))))
+                .thenReturn(result);
+    }
+
+    private ChunksMetadataProto.Chunk createChunkProtoFor(
+            ChunkHash chunkHash, EncryptedChunk encryptedChunk) {
+        return CryptoTestUtils.newChunk(
+                chunkHash, mEncryptedChunkEncoder.getEncodedLengthOfChunk(encryptedChunk));
+    }
+
+    private ChunkOrdering decryptChunkOrdering(byte[] encryptedOrdering) throws Exception {
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(
+                Cipher.DECRYPT_MODE,
+                mTertiaryKey,
+                new GCMParameterSpec(
+                        GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
+                        encryptedOrdering,
+                        /*offset=*/ 0,
+                        GCM_NONCE_LENGTH_BYTES));
+        byte[] decrypted =
+                cipher.doFinal(
+                        encryptedOrdering,
+                        GCM_NONCE_LENGTH_BYTES,
+                        encryptedOrdering.length - GCM_NONCE_LENGTH_BYTES);
+        return ChunkOrdering.parseFrom(decrypted);
+    }
+
+    // This method is needed because nano protobuf generated classes dont implmenent
+    // .equals
+    private void assertChunkListingsAreEqual(ChunkListing a, ChunkListing b) {
+        byte[] aBytes = MessageNano.toByteArray(a);
+        byte[] bBytes = MessageNano.toByteArray(b);
+
+        assertThat(aBytes).isEqualTo(bBytes);
+    }
+
+    @Implements(BackupFileBuilder.class)
+    public static class ShadowBackupFileBuilder {
+
+        private static BackupFileBuilder sInstance;
+
+        @Implementation
+        public static BackupFileBuilder createForNonIncremental(OutputStream outputStream) {
+            return sInstance;
+        }
+
+        @Implementation
+        public static BackupFileBuilder createForIncremental(
+                OutputStream outputStream, ChunkListing oldChunkListing) {
+            return sInstance;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java b/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java
new file mode 100644
index 0000000..71588f6
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupTransport;
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.FullBackupDataProcessor;
+import com.android.server.backup.encryption.StreamUtils;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.security.SecureRandom;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+/**
+ * Accepts backup data from a {@link InputStream} and passes it to the encrypted full data backup
+ * path.
+ */
+public class EncryptedFullBackupDataProcessor implements FullBackupDataProcessor {
+
+    private static final String TAG = "EncryptedFullBackupDP";
+
+    private final Context mContext;
+    private final ExecutorService mExecutorService;
+    private final CryptoBackupServer mCryptoBackupServer;
+    private final SecureRandom mSecureRandom;
+    private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    private final String mPackageName;
+
+    @Nullable private InputStream mInputStream;
+    @Nullable private PipedOutputStream mOutputStream;
+    @Nullable private EncryptedFullBackupTask mBackupTask;
+    @Nullable private Future<Void> mBackupTaskFuture;
+    @Nullable private FullBackupCallbacks mFullBackupCallbacks;
+
+    public EncryptedFullBackupDataProcessor(
+            Context context,
+            ExecutorService executorService,
+            CryptoBackupServer cryptoBackupServer,
+            SecureRandom secureRandom,
+            RecoverableKeyStoreSecondaryKey secondaryKey,
+            String packageName) {
+        mContext = Objects.requireNonNull(context);
+        mExecutorService = Objects.requireNonNull(executorService);
+        mCryptoBackupServer = Objects.requireNonNull(cryptoBackupServer);
+        mSecureRandom = Objects.requireNonNull(secureRandom);
+        mSecondaryKey = Objects.requireNonNull(secondaryKey);
+        mPackageName = Objects.requireNonNull(packageName);
+    }
+
+    @Override
+    public boolean initiate(InputStream inputStream) throws IOException {
+        checkState(mBackupTask == null, "initiate() twice");
+
+        this.mInputStream = inputStream;
+        mOutputStream = new PipedOutputStream();
+
+        mBackupTask =
+                EncryptedFullBackupTask.newInstance(
+                        mContext,
+                        mCryptoBackupServer,
+                        mSecureRandom,
+                        mSecondaryKey,
+                        mPackageName,
+                        new PipedInputStream(mOutputStream));
+
+        return true;
+    }
+
+    @Override
+    public void start() {
+        checkState(mBackupTask != null, "start() before initiate()");
+        mBackupTaskFuture = mExecutorService.submit(mBackupTask);
+    }
+
+    @Override
+    public int pushData(int numBytes) {
+        checkState(
+                mBackupTaskFuture != null && mInputStream != null && mOutputStream != null,
+                "pushData() before start()");
+
+        // If the upload has failed then stop without pushing any more bytes.
+        if (mBackupTaskFuture.isDone()) {
+            Optional<Exception> exception = getTaskException();
+            Slog.e(TAG, "Encrypted upload failed", exception.orElse(null));
+            if (exception.isPresent()) {
+                reportNetworkFailureIfNecessary(exception.get());
+
+                if (exception.get().getCause() instanceof SizeQuotaExceededException) {
+                    return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
+                }
+            }
+
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        try {
+            StreamUtils.copyStream(mInputStream, mOutputStream, numBytes);
+        } catch (IOException e) {
+            Slog.e(TAG, "IOException when processing backup", e);
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        return BackupTransport.TRANSPORT_OK;
+    }
+
+    @Override
+    public void cancel() {
+        checkState(mBackupTaskFuture != null && mBackupTask != null, "cancel() before start()");
+        mBackupTask.cancel();
+        closeStreams();
+    }
+
+    @Override
+    public int finish() {
+        checkState(mBackupTaskFuture != null, "finish() before start()");
+
+        // getTaskException() waits for the task to finish. We must close the streams first, which
+        // causes the task to finish, otherwise it will block forever.
+        closeStreams();
+        Optional<Exception> exception = getTaskException();
+
+        if (exception.isPresent()) {
+            Slog.e(TAG, "Exception during encrypted full backup", exception.get());
+            reportNetworkFailureIfNecessary(exception.get());
+
+            if (exception.get().getCause() instanceof SizeQuotaExceededException) {
+                return BackupTransport.TRANSPORT_QUOTA_EXCEEDED;
+            }
+            return BackupTransport.TRANSPORT_ERROR;
+
+        } else {
+            if (mFullBackupCallbacks != null) {
+                mFullBackupCallbacks.onSuccess();
+            }
+
+            return BackupTransport.TRANSPORT_OK;
+        }
+    }
+
+    private void closeStreams() {
+        StreamUtils.closeQuietly(mInputStream);
+        StreamUtils.closeQuietly(mOutputStream);
+    }
+
+    @Override
+    public void handleCheckSizeRejectionZeroBytes() {
+        cancel();
+    }
+
+    @Override
+    public void handleCheckSizeRejectionQuotaExceeded() {
+        cancel();
+    }
+
+    @Override
+    public void handleSendBytesQuotaExceeded() {
+        cancel();
+    }
+
+    @Override
+    public void attachCallbacks(FullBackupCallbacks fullBackupCallbacks) {
+        this.mFullBackupCallbacks = fullBackupCallbacks;
+    }
+
+    private void reportNetworkFailureIfNecessary(Exception exception) {
+        if (!(exception.getCause() instanceof SizeQuotaExceededException)
+                && mFullBackupCallbacks != null) {
+            mFullBackupCallbacks.onTransferFailed();
+        }
+    }
+
+    private Optional<Exception> getTaskException() {
+        if (mBackupTaskFuture != null) {
+            try {
+                mBackupTaskFuture.get();
+            } catch (InterruptedException | ExecutionException e) {
+                return Optional.of(e);
+            }
+        }
+        return Optional.empty();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java b/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java
new file mode 100644
index 0000000..675d03f
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessorTest.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupTransport;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.FullBackupDataProcessor;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.testing.QueuingNonAutomaticExecutorService;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.primitives.Bytes;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.crypto.spec.SecretKeySpec;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(
+        shadows = {
+            EncryptedFullBackupDataProcessorTest.ShadowEncryptedFullBackupTask.class,
+        })
+public class EncryptedFullBackupDataProcessorTest {
+
+    private static final String KEY_GENERATOR_ALGORITHM = "AES";
+
+    private static final String TEST_PACKAGE = "com.example.app1";
+    private static final byte[] TEST_DATA_1 = {1, 2, 3, 4};
+    private static final byte[] TEST_DATA_2 = {5, 6, 7, 8};
+
+    private final RecoverableKeyStoreSecondaryKey mTestSecondaryKey =
+            new RecoverableKeyStoreSecondaryKey(
+                    /*alias=*/ "test_key",
+                    new SecretKeySpec(
+                            new byte[] {
+                                1, 2, 3,
+                            },
+                            KEY_GENERATOR_ALGORITHM));
+
+    private QueuingNonAutomaticExecutorService mExecutorService;
+    private FullBackupDataProcessor mFullBackupDataProcessor;
+    @Mock private FullBackupDataProcessor.FullBackupCallbacks mFullBackupCallbacks;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mExecutorService = new QueuingNonAutomaticExecutorService();
+        mFullBackupDataProcessor =
+                new EncryptedFullBackupDataProcessor(
+                        ApplicationProvider.getApplicationContext(),
+                        mExecutorService,
+                        mock(CryptoBackupServer.class),
+                        new SecureRandom(),
+                        mTestSecondaryKey,
+                        TEST_PACKAGE);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowEncryptedFullBackupTask.reset();
+    }
+
+    @Test
+    public void initiate_callTwice_throws() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10]));
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10])));
+    }
+
+    @Test
+    public void pushData_writesDataToTask() throws Exception {
+        byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
+        finishBackupTask();
+        mFullBackupDataProcessor.finish();
+
+        byte[] result = ByteStreams.toByteArray(ShadowEncryptedFullBackupTask.sInputStream);
+        assertThat(result).isEqualTo(Bytes.concat(TEST_DATA_1, TEST_DATA_2));
+    }
+
+    @Test
+    public void pushData_noError_returnsOk() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        int result = mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTask();
+        mFullBackupDataProcessor.finish();
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_OK);
+    }
+
+    @Test
+    public void pushData_ioExceptionOnCopy_returnsError() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+
+        // Close the stream so there's an IO error when the processor tries to write to it.
+        ShadowEncryptedFullBackupTask.sInputStream.close();
+        int result = mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+
+        finishBackupTask();
+        mFullBackupDataProcessor.finish();
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void pushData_exceptionDuringUpload_returnsError() throws Exception {
+        byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new IOException("Test exception"));
+        int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void pushData_quotaExceptionDuringUpload_doesNotLogAndReturnsQuotaExceeded()
+            throws Exception {
+        mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
+        byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new SizeQuotaExceededException());
+        int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
+
+        verify(mFullBackupCallbacks, never()).onSuccess();
+        verify(mFullBackupCallbacks, never())
+                .onTransferFailed(); // FullBackupSession will handle this.
+    }
+
+    @Test
+    public void pushData_unexpectedEncryptedBackup_logs() throws Exception {
+        byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new GeneralSecurityException());
+        int result = mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void pushData_permanentExceptionDuringUpload_callsErrorCallback() throws Exception {
+        mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
+        byte[] inputData = Bytes.concat(TEST_DATA_1, TEST_DATA_2);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(inputData));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new IOException());
+        mFullBackupDataProcessor.pushData(TEST_DATA_2.length);
+
+        verify(mFullBackupCallbacks, never()).onSuccess();
+        verify(mFullBackupCallbacks).onTransferFailed();
+    }
+
+    @Test
+    public void pushData_beforeInitiate_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> mFullBackupDataProcessor.pushData(/*numBytes=*/ 10));
+    }
+
+    @Test
+    public void cancel_cancelsTask() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        mFullBackupDataProcessor.cancel();
+
+        assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
+    }
+
+    @Test
+    public void cancel_beforeInitiate_throws() {
+        assertThrows(IllegalStateException.class, () -> mFullBackupDataProcessor.cancel());
+    }
+
+    @Test
+    public void finish_noException_returnsTransportOk() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTask();
+        int result = mFullBackupDataProcessor.finish();
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_OK);
+    }
+
+    @Test
+    public void finish_exceptionDuringUpload_returnsTransportError() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new IOException("Test exception"));
+        int result = mFullBackupDataProcessor.finish();
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void finish_successfulBackup_callsSuccessCallback() throws Exception {
+        mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTask();
+        mFullBackupDataProcessor.finish();
+
+        verify(mFullBackupCallbacks).onSuccess();
+        verify(mFullBackupCallbacks, never()).onTransferFailed();
+    }
+
+    @Test
+    public void finish_backupFailedWithPermanentError_callsErrorCallback() throws Exception {
+        mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new IOException());
+        mFullBackupDataProcessor.finish();
+
+        verify(mFullBackupCallbacks, never()).onSuccess();
+        verify(mFullBackupCallbacks).onTransferFailed();
+    }
+
+    @Test
+    public void finish_backupFailedWithQuotaException_doesNotCallbackAndReturnsQuotaExceeded()
+            throws Exception {
+        mFullBackupDataProcessor.attachCallbacks(mFullBackupCallbacks);
+
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        finishBackupTaskWithException(new SizeQuotaExceededException());
+        int result = mFullBackupDataProcessor.finish();
+
+        assertThat(result).isEqualTo(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
+        verify(mFullBackupCallbacks, never()).onSuccess();
+        verify(mFullBackupCallbacks, never())
+                .onTransferFailed(); // FullBackupSession will handle this.
+    }
+
+    @Test
+    public void finish_beforeInitiate_throws() {
+        assertThrows(IllegalStateException.class, () -> mFullBackupDataProcessor.finish());
+    }
+
+    @Test
+    public void handleCheckSizeRejectionZeroBytes_cancelsTask() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(new byte[10]));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.handleCheckSizeRejectionZeroBytes();
+
+        assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
+    }
+
+    @Test
+    public void handleCheckSizeRejectionQuotaExceeded_cancelsTask() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        mFullBackupDataProcessor.handleCheckSizeRejectionQuotaExceeded();
+
+        assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
+    }
+
+    @Test
+    public void handleSendBytesQuotaExceeded_cancelsTask() throws Exception {
+        mFullBackupDataProcessor.initiate(new ByteArrayInputStream(TEST_DATA_1));
+        mFullBackupDataProcessor.start();
+        mFullBackupDataProcessor.pushData(TEST_DATA_1.length);
+        mFullBackupDataProcessor.handleSendBytesQuotaExceeded();
+
+        assertThat(ShadowEncryptedFullBackupTask.sCancelled).isTrue();
+    }
+
+    private void finishBackupTask() {
+        mExecutorService.runNext();
+    }
+
+    private void finishBackupTaskWithException(Exception exception) {
+        ShadowEncryptedFullBackupTask.sOnCallException = exception;
+        finishBackupTask();
+    }
+
+    @Implements(EncryptedFullBackupTask.class)
+    public static class ShadowEncryptedFullBackupTask {
+
+        private static InputStream sInputStream;
+        @Nullable private static Exception sOnCallException;
+        private static boolean sCancelled;
+
+        public void __constructor__(
+                ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
+                TertiaryKeyManager tertiaryKeyManager,
+                EncryptedBackupTask task,
+                InputStream inputStream,
+                String packageName,
+                SecureRandom secureRandom) {
+            sInputStream = inputStream;
+        }
+
+        @Implementation
+        public Void call() throws Exception {
+            if (sOnCallException != null) {
+                throw sOnCallException;
+            }
+
+            return null;
+        }
+
+        @Implementation
+        public void cancel() {
+            sCancelled = true;
+        }
+
+        public static void reset() {
+            sOnCallException = null;
+            sCancelled = false;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java b/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
new file mode 100644
index 0000000..a938d71
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.StreamUtils;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Task which reads a stream of plaintext full backup data, chunks it, encrypts it and uploads it to
+ * the server.
+ *
+ * <p>Once the backup completes or fails, closes the input stream.
+ */
+public class EncryptedFullBackupTask implements Callable<Void> {
+    private static final String TAG = "EncryptedFullBackupTask";
+
+    private static final int MIN_CHUNK_SIZE_BYTES = 2 * 1024;
+    private static final int MAX_CHUNK_SIZE_BYTES = 64 * 1024;
+    private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * 1024;
+
+    // TODO(b/69350270): Remove this hard-coded salt and related logic once we feel confident that
+    // incremental backup has happened at least once for all existing packages/users since we moved
+    // to
+    // using a randomly generated salt.
+    //
+    // The hard-coded fingerprint mixer salt was used for a short time period before replaced by one
+    // that is randomly generated on initial non-incremental backup and stored in ChunkListing to be
+    // reused for succeeding incremental backups. If an old ChunkListing does not have a
+    // fingerprint_mixer_salt, we assume that it was last backed up before a randomly generated salt
+    // is used so we use the hardcoded salt and set ChunkListing#fingerprint_mixer_salt to this
+    // value.
+    // Eventually all backup ChunkListings will have this field set and then we can remove the
+    // default
+    // value in the code.
+    static final byte[] DEFAULT_FINGERPRINT_MIXER_SALT =
+            Arrays.copyOf(new byte[] {20, 23}, FingerprintMixer.SALT_LENGTH_BYTES);
+
+    private final ProtoStore<ChunkListing> mChunkListingStore;
+    private final TertiaryKeyManager mTertiaryKeyManager;
+    private final InputStream mInputStream;
+    private final EncryptedBackupTask mTask;
+    private final String mPackageName;
+    private final SecureRandom mSecureRandom;
+
+    /** Creates a new instance with the default min, max and average chunk sizes. */
+    public static EncryptedFullBackupTask newInstance(
+            Context context,
+            CryptoBackupServer cryptoBackupServer,
+            SecureRandom secureRandom,
+            RecoverableKeyStoreSecondaryKey secondaryKey,
+            String packageName,
+            InputStream inputStream)
+            throws IOException {
+        EncryptedBackupTask encryptedBackupTask =
+                new EncryptedBackupTask(
+                        cryptoBackupServer,
+                        secureRandom,
+                        packageName,
+                        new BackupStreamEncrypter(
+                                inputStream,
+                                MIN_CHUNK_SIZE_BYTES,
+                                MAX_CHUNK_SIZE_BYTES,
+                                AVERAGE_CHUNK_SIZE_BYTES));
+        TertiaryKeyManager tertiaryKeyManager =
+                new TertiaryKeyManager(
+                        context,
+                        secureRandom,
+                        TertiaryKeyRotationScheduler.getInstance(context),
+                        secondaryKey,
+                        packageName);
+
+        return new EncryptedFullBackupTask(
+                ProtoStore.createChunkListingStore(context),
+                tertiaryKeyManager,
+                encryptedBackupTask,
+                inputStream,
+                packageName,
+                new SecureRandom());
+    }
+
+    @VisibleForTesting
+    EncryptedFullBackupTask(
+            ProtoStore<ChunkListing> chunkListingStore,
+            TertiaryKeyManager tertiaryKeyManager,
+            EncryptedBackupTask task,
+            InputStream inputStream,
+            String packageName,
+            SecureRandom secureRandom) {
+        mChunkListingStore = chunkListingStore;
+        mTertiaryKeyManager = tertiaryKeyManager;
+        mInputStream = inputStream;
+        mTask = task;
+        mPackageName = packageName;
+        mSecureRandom = secureRandom;
+    }
+
+    @Override
+    public Void call() throws Exception {
+        try {
+            Optional<ChunkListing> maybeOldChunkListing =
+                    mChunkListingStore.loadProto(mPackageName);
+
+            if (maybeOldChunkListing.isPresent()) {
+                Slog.i(TAG, "Found previous chunk listing for " + mPackageName);
+            }
+
+            // If the key has been rotated then we must re-encrypt all of the backup data.
+            if (mTertiaryKeyManager.wasKeyRotated()) {
+                Slog.i(
+                        TAG,
+                        "Key was rotated or newly generated for "
+                                + mPackageName
+                                + ", so performing a full backup.");
+                maybeOldChunkListing = Optional.empty();
+                mChunkListingStore.deleteProto(mPackageName);
+            }
+
+            SecretKey tertiaryKey = mTertiaryKeyManager.getKey();
+            WrappedKeyProto.WrappedKey wrappedTertiaryKey = mTertiaryKeyManager.getWrappedKey();
+
+            ChunkListing newChunkListing;
+            if (!maybeOldChunkListing.isPresent()) {
+                byte[] fingerprintMixerSalt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
+                mSecureRandom.nextBytes(fingerprintMixerSalt);
+                newChunkListing =
+                        mTask.performNonIncrementalBackup(
+                                tertiaryKey, wrappedTertiaryKey, fingerprintMixerSalt);
+            } else {
+                ChunkListing oldChunkListing = maybeOldChunkListing.get();
+
+                if (oldChunkListing.fingerprintMixerSalt == null
+                        || oldChunkListing.fingerprintMixerSalt.length == 0) {
+                    oldChunkListing.fingerprintMixerSalt = DEFAULT_FINGERPRINT_MIXER_SALT;
+                }
+
+                newChunkListing =
+                        mTask.performIncrementalBackup(
+                                tertiaryKey, wrappedTertiaryKey, oldChunkListing);
+            }
+
+            mChunkListingStore.saveProto(mPackageName, newChunkListing);
+            Slog.v(TAG, "Saved chunk listing for " + mPackageName);
+        } catch (IOException e) {
+            Slog.e(TAG, "Storage exception, wiping state");
+            mChunkListingStore.deleteProto(mPackageName);
+            throw e;
+        } finally {
+            StreamUtils.closeQuietly(mInputStream);
+        }
+
+        return null;
+    }
+
+    /**
+     * Signals to the task that the backup has been cancelled. If the upload has not yet started
+     * then the task will not upload any data to the server or save the new chunk listing.
+     *
+     * <p>You must then terminate the input stream.
+     */
+    public void cancel() {
+        mTask.cancel();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java b/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
new file mode 100644
index 0000000..096b2da
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto.WrappedKey;
+import com.android.server.backup.testing.CryptoTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+
+@Config(shadows = {EncryptedBackupTaskTest.ShadowBackupFileBuilder.class})
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedFullBackupTaskTest {
+    private static final String TEST_PACKAGE_NAME = "com.example.package";
+    private static final byte[] TEST_EXISTING_FINGERPRINT_MIXER_SALT =
+            Arrays.copyOf(new byte[] {11}, ChunkHash.HASH_LENGTH_BYTES);
+    private static final byte[] TEST_GENERATED_FINGERPRINT_MIXER_SALT =
+            Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
+    private static final ChunkHash TEST_CHUNK_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final ChunkHash TEST_CHUNK_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final int TEST_CHUNK_LENGTH_1 = 20;
+    private static final int TEST_CHUNK_LENGTH_2 = 40;
+
+    @Mock private ProtoStore<ChunkListing> mChunkListingStore;
+    @Mock private TertiaryKeyManager mTertiaryKeyManager;
+    @Mock private InputStream mInputStream;
+    @Mock private EncryptedBackupTask mEncryptedBackupTask;
+    @Mock private SecretKey mTertiaryKey;
+    @Mock private SecureRandom mSecureRandom;
+
+    private EncryptedFullBackupTask mTask;
+    private ChunkListing mOldChunkListing;
+    private ChunkListing mNewChunkListing;
+    private WrappedKey mWrappedTertiaryKey;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mWrappedTertiaryKey = new WrappedKey();
+        when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
+        when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
+
+        mOldChunkListing =
+                CryptoTestUtils.newChunkListing(
+                        /* docId */ null,
+                        TEST_EXISTING_FINGERPRINT_MIXER_SALT,
+                        ChunksMetadataProto.AES_256_GCM,
+                        ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                        CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1));
+        mNewChunkListing =
+                CryptoTestUtils.newChunkListing(
+                        /* docId */ null,
+                        /* fingerprintSalt */ null,
+                        ChunksMetadataProto.AES_256_GCM,
+                        ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                        CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1),
+                        CryptoTestUtils.newChunk(TEST_CHUNK_HASH_2.getHash(), TEST_CHUNK_LENGTH_2));
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenReturn(mNewChunkListing);
+        when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
+                .thenReturn(mNewChunkListing);
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
+
+        doAnswer(invocation -> {
+            byte[] byteArray = (byte[]) invocation.getArguments()[0];
+            System.arraycopy(
+                    TEST_GENERATED_FINGERPRINT_MIXER_SALT,
+                    /* srcPos */ 0,
+                    byteArray,
+                    /* destPos */ 0,
+                    FingerprintMixer.SALT_LENGTH_BYTES);
+            return null;
+        })
+                .when(mSecureRandom)
+                .nextBytes(any(byte[].class));
+
+        mTask =
+                new EncryptedFullBackupTask(
+                        mChunkListingStore,
+                        mTertiaryKeyManager,
+                        mEncryptedBackupTask,
+                        mInputStream,
+                        TEST_PACKAGE_NAME,
+                        mSecureRandom);
+    }
+
+    @Test
+    public void call_existingChunkListingButTertiaryKeyRotated_performsNonIncrementalBackup()
+            throws Exception {
+        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
+                .thenReturn(Optional.of(mOldChunkListing));
+
+        mTask.call();
+
+        verify(mEncryptedBackupTask)
+                .performNonIncrementalBackup(
+                        eq(mTertiaryKey),
+                        eq(mWrappedTertiaryKey),
+                        eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
+    }
+
+    @Test
+    public void call_noExistingChunkListing_performsNonIncrementalBackup() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
+        mTask.call();
+        verify(mEncryptedBackupTask)
+                .performNonIncrementalBackup(
+                        eq(mTertiaryKey),
+                        eq(mWrappedTertiaryKey),
+                        eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
+    }
+
+    @Test
+    public void call_existingChunkListing_performsIncrementalBackup() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
+                .thenReturn(Optional.of(mOldChunkListing));
+        mTask.call();
+        verify(mEncryptedBackupTask)
+                .performIncrementalBackup(
+                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
+    }
+
+    @Test
+    public void
+            call_existingChunkListingWithNoFingerprintMixerSalt_doesntSetSaltBeforeIncBackup()
+                    throws Exception {
+        mOldChunkListing.fingerprintMixerSalt = new byte[0];
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
+                .thenReturn(Optional.of(mOldChunkListing));
+
+        mTask.call();
+
+        verify(mEncryptedBackupTask)
+                .performIncrementalBackup(
+                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
+    }
+
+    @Test
+    public void call_noExistingChunkListing_storesNewChunkListing() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
+        mTask.call();
+        verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
+    }
+
+    @Test
+    public void call_existingChunkListing_storesNewChunkListing() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
+                .thenReturn(Optional.of(mOldChunkListing));
+        mTask.call();
+        verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
+    }
+
+    @Test
+    public void call_exceptionDuringBackup_doesNotSaveNewChunkListing() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenThrow(GeneralSecurityException.class);
+
+        assertThrows(Exception.class, () -> mTask.call());
+
+        assertThat(mChunkListingStore.loadProto(TEST_PACKAGE_NAME).isPresent()).isFalse();
+    }
+
+    @Test
+    public void call_incrementalThrowsPermanentException_clearsState() throws Exception {
+        when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
+                .thenReturn(Optional.of(mOldChunkListing));
+        when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
+                .thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> mTask.call());
+
+        verify(mChunkListingStore).deleteProto(TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void call_closesInputStream() throws Exception {
+        mTask.call();
+        verify(mInputStream).close();
+    }
+
+    @Test
+    public void cancel_cancelsTask() throws Exception {
+        mTask.cancel();
+        verify(mEncryptedBackupTask).cancel();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java b/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
new file mode 100644
index 0000000..04381af
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.FullRestoreDataProcessor;
+import com.android.server.backup.encryption.FullRestoreDownloader;
+import com.android.server.backup.encryption.StreamUtils;
+import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+
+/** Downloads the encrypted backup file, decrypts it and passes the data to backup manager. */
+public class EncryptedFullRestoreTask implements FullRestoreDataProcessor {
+    private static final String DEFAULT_TEMPORARY_FOLDER = "encrypted_restore_temp";
+    private static final String ENCRYPTED_FILE_NAME = "encrypted_restore";
+    private static final String DECRYPTED_FILE_NAME = "decrypted_restore";
+
+    private final FullRestoreToFileTask mFullRestoreToFileTask;
+    private final BackupFileDecryptorTask mBackupFileDecryptorTask;
+    private final File mEncryptedFile;
+    private final File mDecryptedFile;
+    @Nullable private InputStream mDecryptedFileInputStream;
+
+    /**
+     * Creates a new task which stores temporary files in the files directory.
+     *
+     * @param fullRestoreDownloader which will download the backup file
+     * @param tertiaryKey which the backup file is encrypted with
+     */
+    public static EncryptedFullRestoreTask newInstance(
+            Context context, FullRestoreDownloader fullRestoreDownloader, SecretKey tertiaryKey)
+            throws NoSuchAlgorithmException, NoSuchPaddingException {
+        File temporaryFolder = new File(context.getFilesDir(), DEFAULT_TEMPORARY_FOLDER);
+        temporaryFolder.mkdirs();
+        return new EncryptedFullRestoreTask(
+                temporaryFolder, fullRestoreDownloader, new BackupFileDecryptorTask(tertiaryKey));
+    }
+
+    @VisibleForTesting
+    EncryptedFullRestoreTask(
+            File temporaryFolder,
+            FullRestoreDownloader fullRestoreDownloader,
+            BackupFileDecryptorTask backupFileDecryptorTask) {
+        checkArgument(temporaryFolder.isDirectory(), "Temporary folder must be existing directory");
+
+        mEncryptedFile = new File(temporaryFolder, ENCRYPTED_FILE_NAME);
+        mDecryptedFile = new File(temporaryFolder, DECRYPTED_FILE_NAME);
+
+        mFullRestoreToFileTask = new FullRestoreToFileTask(fullRestoreDownloader);
+        mBackupFileDecryptorTask = backupFileDecryptorTask;
+    }
+
+    /**
+     * Reads the next decrypted bytes into the given buffer.
+     *
+     * <p>During the first call this method will download the backup file from the server, decrypt
+     * it and save it to disk. It will then read the bytes from the file on disk.
+     *
+     * <p>Once this method has read all the bytes of the file, the caller must call {@link #finish}
+     * to clean up.
+     *
+     * @return the number of bytes read, or {@code -1} on reaching the end of the file
+     */
+    @Override
+    public int readNextChunk(byte[] buffer) throws IOException {
+        if (mDecryptedFileInputStream == null) {
+            try {
+                mDecryptedFileInputStream = downloadAndDecryptBackup();
+            } catch (BadPaddingException
+                    | InvalidKeyException
+                    | NoSuchAlgorithmException
+                    | IllegalBlockSizeException
+                    | ShortBufferException
+                    | EncryptedRestoreException
+                    | InvalidAlgorithmParameterException e) {
+                throw new IOException("Encryption issue", e);
+            }
+        }
+
+        return mDecryptedFileInputStream.read(buffer);
+    }
+
+    private InputStream downloadAndDecryptBackup()
+            throws IOException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
+                    IllegalBlockSizeException, ShortBufferException, EncryptedRestoreException,
+                    InvalidAlgorithmParameterException {
+        mFullRestoreToFileTask.restoreToFile(mEncryptedFile);
+        mBackupFileDecryptorTask.decryptFile(
+                mEncryptedFile, new DecryptedChunkFileOutput(mDecryptedFile));
+        mEncryptedFile.delete();
+        return new BufferedInputStream(new FileInputStream(mDecryptedFile));
+    }
+
+    /** Cleans up temporary files. */
+    @Override
+    public void finish(FullRestoreDownloader.FinishType unusedFinishType) {
+        // The download is finished and log sent during RestoreToFileTask#restoreToFile(), so we
+        // don't need to do either of those things here.
+
+        StreamUtils.closeQuietly(mDecryptedFileInputStream);
+        mEncryptedFile.delete();
+        mDecryptedFile.delete();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java b/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
new file mode 100644
index 0000000..0affacd
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+
+import static java.util.stream.Collectors.toList;
+
+import com.android.server.backup.encryption.FullRestoreDownloader;
+
+import com.google.common.io.Files;
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedFullRestoreTaskTest {
+    private static final int TEST_BUFFER_SIZE = 10;
+    private static final byte[] TEST_ENCRYPTED_DATA = {1, 2, 3, 4, 5, 6};
+    private static final byte[] TEST_DECRYPTED_DATA = fakeDecrypt(TEST_ENCRYPTED_DATA);
+
+    @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Mock private BackupFileDecryptorTask mDecryptorTask;
+
+    private File mFolder;
+    private FakeFullRestoreDownloader mFullRestorePackageWrapper;
+    private EncryptedFullRestoreTask mTask;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mFolder = temporaryFolder.newFolder();
+        mFullRestorePackageWrapper = new FakeFullRestoreDownloader(TEST_ENCRYPTED_DATA);
+
+        doAnswer(
+            invocation -> {
+                File source = invocation.getArgument(0);
+                DecryptedChunkOutput target = invocation.getArgument(1);
+                byte[] decrypted = fakeDecrypt(Files.toByteArray(source));
+                target.open();
+                target.processChunk(decrypted, decrypted.length);
+                target.close();
+                return null;
+            })
+                .when(mDecryptorTask)
+                .decryptFile(any(), any());
+
+        mTask = new EncryptedFullRestoreTask(mFolder, mFullRestorePackageWrapper, mDecryptorTask);
+    }
+
+    @Test
+    public void readNextChunk_downloadsAndDecryptsBackup() throws Exception {
+        ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
+
+        byte[] buffer = new byte[TEST_BUFFER_SIZE];
+        int bytesRead = mTask.readNextChunk(buffer);
+        while (bytesRead != -1) {
+            decryptedOutput.write(buffer, 0, bytesRead);
+            bytesRead = mTask.readNextChunk(buffer);
+        }
+
+        assertThat(decryptedOutput.toByteArray()).isEqualTo(TEST_DECRYPTED_DATA);
+    }
+
+    @Test
+    public void finish_deletesTemporaryFiles() throws Exception {
+        mTask.readNextChunk(new byte[10]);
+        mTask.finish(FullRestoreDownloader.FinishType.UNKNOWN_FINISH);
+
+        assertThat(mFolder.listFiles()).isEmpty();
+    }
+
+    /** Fake package wrapper which returns data from a byte array. */
+    private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
+        private final ByteArrayInputStream mData;
+
+        FakeFullRestoreDownloader(byte[] data) {
+            // We override all methods of the superclass, so it does not require any collaborators.
+            super();
+            mData = new ByteArrayInputStream(data);
+        }
+
+        @Override
+        public int readNextChunk(byte[] buffer) throws IOException {
+            return mData.read(buffer);
+        }
+
+        @Override
+        public void finish(FinishType finishType) {
+            // Nothing to do.
+        }
+    }
+
+    /** Fake decrypts a byte array by subtracting 1 from each byte. */
+    private static byte[] fakeDecrypt(byte[] input) {
+        return Bytes.toArray(Bytes.asList(input).stream().map(b -> b + 1).collect(toList()));
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java b/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java
new file mode 100644
index 0000000..619438c
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupDataInput;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.LockScreenRequiredException;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.util.Optional;
+
+// TODO(b/141975695): Create a base class for EncryptedKvBackupTask and EncryptedFullBackupTask.
+/** Performs encrypted key value backup, handling rotating the tertiary key as necessary. */
+public class EncryptedKvBackupTask {
+    private static final String TAG = "EncryptedKvBackupTask";
+
+    private final TertiaryKeyManager mTertiaryKeyManager;
+    private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    private final ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
+    private final ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
+    private final KvBackupEncrypter mKvBackupEncrypter;
+    private final EncryptedBackupTask mEncryptedBackupTask;
+    private final String mPackageName;
+
+    /** Constructs new instances of {@link EncryptedKvBackupTask}. */
+    public static class EncryptedKvBackupTaskFactory {
+        /**
+         * Creates a new instance.
+         *
+         * <p>Either initializes encrypted backup or loads an existing secondary key as necessary.
+         *
+         * @param cryptoSettings to load secondary key state from
+         * @param fileDescriptor to read the backup data from
+         */
+        public EncryptedKvBackupTask newInstance(
+                Context context,
+                SecureRandom secureRandom,
+                CryptoBackupServer cryptoBackupServer,
+                CryptoSettings cryptoSettings,
+                RecoverableKeyStoreSecondaryKeyManager
+                                .RecoverableKeyStoreSecondaryKeyManagerProvider
+                        recoverableSecondaryKeyManagerProvider,
+                ParcelFileDescriptor fileDescriptor,
+                String packageName)
+                throws IOException, UnrecoverableKeyException, LockScreenRequiredException,
+                        InternalRecoveryServiceException, InvalidKeyException {
+            RecoverableKeyStoreSecondaryKey secondaryKey =
+                    new InitializeRecoverableSecondaryKeyTask(
+                                    context,
+                                    cryptoSettings,
+                                    recoverableSecondaryKeyManagerProvider.get(),
+                                    cryptoBackupServer)
+                            .run();
+            KvBackupEncrypter backupEncrypter =
+                    new KvBackupEncrypter(new BackupDataInput(fileDescriptor.getFileDescriptor()));
+            TertiaryKeyManager tertiaryKeyManager =
+                    new TertiaryKeyManager(
+                            context,
+                            secureRandom,
+                            TertiaryKeyRotationScheduler.getInstance(context),
+                            secondaryKey,
+                            packageName);
+
+            return new EncryptedKvBackupTask(
+                    tertiaryKeyManager,
+                    ProtoStore.createKeyValueListingStore(context),
+                    secondaryKey,
+                    ProtoStore.createChunkListingStore(context),
+                    backupEncrypter,
+                    new EncryptedBackupTask(
+                            cryptoBackupServer, secureRandom, packageName, backupEncrypter),
+                    packageName);
+        }
+    }
+
+    @VisibleForTesting
+    EncryptedKvBackupTask(
+            TertiaryKeyManager tertiaryKeyManager,
+            ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore,
+            RecoverableKeyStoreSecondaryKey secondaryKey,
+            ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
+            KvBackupEncrypter kvBackupEncrypter,
+            EncryptedBackupTask encryptedBackupTask,
+            String packageName) {
+        mTertiaryKeyManager = tertiaryKeyManager;
+        mSecondaryKey = secondaryKey;
+        mKeyValueListingStore = keyValueListingStore;
+        mChunkListingStore = chunkListingStore;
+        mKvBackupEncrypter = kvBackupEncrypter;
+        mEncryptedBackupTask = encryptedBackupTask;
+        mPackageName = packageName;
+    }
+
+    /**
+     * Reads backup data from the file descriptor provided in the construtor, encrypts it and
+     * uploads it to the server.
+     *
+     * <p>The {@code incremental} flag indicates if the backup data provided is incremental or a
+     * complete set. Incremental backup is not possible if no previous crypto state exists, or the
+     * tertiary key must be rotated in the next backup. If the caller requests incremental backup
+     * but it is not possible, then the backup will not start and this method will throw {@link
+     * NonIncrementalBackupRequiredException}.
+     *
+     * <p>TODO(b/70704456): Update return code to indicate that we require non-incremental backup.
+     *
+     * @param incremental {@code true} if the data provided is a diff from the previous backup,
+     *     {@code false} if it is a complete set
+     * @throws NonIncrementalBackupRequiredException if the caller provides an incremental backup but the task
+     *     requires non-incremental backup
+     */
+    public void performBackup(boolean incremental)
+            throws GeneralSecurityException, IOException, NoSuchMethodException,
+            InstantiationException, IllegalAccessException, InvocationTargetException,
+            NonIncrementalBackupRequiredException {
+        if (mTertiaryKeyManager.wasKeyRotated()) {
+            Slog.d(TAG, "Tertiary key is new so clearing package state.");
+            deleteListings(mPackageName);
+        }
+
+        Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
+                oldListings = getListingsAndEnsureConsistency(mPackageName);
+
+        if (oldListings.isPresent() && !incremental) {
+            Slog.d(
+                    TAG,
+                    "Non-incremental backup requested but incremental state existed, clearing it");
+            deleteListings(mPackageName);
+            oldListings = Optional.empty();
+        }
+
+        if (!oldListings.isPresent() && incremental) {
+            // If we don't have any state then we require a non-incremental backup, but this backup
+            // is incremental.
+            throw new NonIncrementalBackupRequiredException();
+        }
+
+        if (oldListings.isPresent()) {
+            mKvBackupEncrypter.setOldKeyValueListing(oldListings.get().first);
+        }
+
+        ChunksMetadataProto.ChunkListing newChunkListing;
+        if (oldListings.isPresent()) {
+            Slog.v(TAG, "Old listings existed, performing incremental backup");
+            newChunkListing =
+                    mEncryptedBackupTask.performIncrementalBackup(
+                            mTertiaryKeyManager.getKey(),
+                            mTertiaryKeyManager.getWrappedKey(),
+                            oldListings.get().second);
+        } else {
+            Slog.v(TAG, "Old listings did not exist, performing non-incremental backup");
+            // kv backups don't use this salt because they don't involve content-defined chunking.
+            byte[] fingerprintMixerSalt = null;
+            newChunkListing =
+                    mEncryptedBackupTask.performNonIncrementalBackup(
+                            mTertiaryKeyManager.getKey(),
+                            mTertiaryKeyManager.getWrappedKey(),
+                            fingerprintMixerSalt);
+        }
+
+        Slog.v(TAG, "Backup and upload succeeded, saving new listings");
+        saveListings(mPackageName, mKvBackupEncrypter.getNewKeyValueListing(), newChunkListing);
+    }
+
+    private Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
+            getListingsAndEnsureConsistency(String packageName)
+                    throws IOException, InvocationTargetException, NoSuchMethodException,
+                            InstantiationException, IllegalAccessException {
+        Optional<KeyValueListingProto.KeyValueListing> keyValueListing =
+                mKeyValueListingStore.loadProto(packageName);
+        Optional<ChunksMetadataProto.ChunkListing> chunkListing =
+                mChunkListingStore.loadProto(packageName);
+
+        // Normally either both protos exist or neither exist, but we correct this just in case.
+        boolean bothPresent = keyValueListing.isPresent() && chunkListing.isPresent();
+        if (!bothPresent) {
+            Slog.d(
+                    TAG,
+                    "Both listing were not present, clearing state, key value="
+                            + keyValueListing.isPresent()
+                            + ", chunk="
+                            + chunkListing.isPresent());
+            deleteListings(packageName);
+            return Optional.empty();
+        }
+
+        return Optional.of(Pair.create(keyValueListing.get(), chunkListing.get()));
+    }
+
+    private void saveListings(
+            String packageName,
+            KeyValueListingProto.KeyValueListing keyValueListing,
+            ChunksMetadataProto.ChunkListing chunkListing) {
+        try {
+            mKeyValueListingStore.saveProto(packageName, keyValueListing);
+            mChunkListingStore.saveProto(packageName, chunkListing);
+        } catch (IOException e) {
+            // If a problem occurred while saving either listing then they may be inconsistent, so
+            // delete
+            // both.
+            Slog.w(TAG, "Unable to save listings, deleting both for consistency", e);
+            deleteListings(packageName);
+        }
+    }
+
+    private void deleteListings(String packageName) {
+        mKeyValueListingStore.deleteProto(packageName);
+        mChunkListingStore.deleteProto(packageName);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java b/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
new file mode 100644
index 0000000..fa4fef5
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import android.app.Application;
+import android.util.Pair;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.testing.CryptoTestUtils;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.crypto.SecretKey;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedKvBackupTaskTest {
+    private static final boolean INCREMENTAL = true;
+    private static final boolean NON_INCREMENTAL = false;
+
+    private static final String TEST_PACKAGE_1 = "com.example.app1";
+    private static final String TEST_KEY_1 = "key_1";
+    private static final String TEST_KEY_2 = "key_2";
+    private static final ChunkHash TEST_HASH_1 =
+            new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final ChunkHash TEST_HASH_2 =
+            new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
+    private static final int TEST_LENGTH_1 = 200;
+    private static final int TEST_LENGTH_2 = 300;
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    @Captor private ArgumentCaptor<ChunksMetadataProto.ChunkListing> mChunkListingCaptor;
+
+    @Mock private TertiaryKeyManager mTertiaryKeyManager;
+    @Mock private RecoverableKeyStoreSecondaryKey mSecondaryKey;
+    @Mock private ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
+    @Mock private ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
+    @Mock private KvBackupEncrypter mKvBackupEncrypter;
+    @Mock private EncryptedBackupTask mEncryptedBackupTask;
+    @Mock private SecretKey mTertiaryKey;
+
+    private WrappedKeyProto.WrappedKey mWrappedTertiaryKey;
+    private KeyValueListingProto.KeyValueListing mNewKeyValueListing;
+    private ChunksMetadataProto.ChunkListing mNewChunkListing;
+    private EncryptedKvBackupTask mTask;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        Application application = ApplicationProvider.getApplicationContext();
+        mKeyValueListingStore = ProtoStore.createKeyValueListingStore(application);
+        mChunkListingStore = ProtoStore.createChunkListingStore(application);
+
+        mWrappedTertiaryKey = new WrappedKeyProto.WrappedKey();
+
+        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(false);
+        when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
+        when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
+
+        mNewKeyValueListing =
+                createKeyValueListing(
+                        CryptoTestUtils.mapOf(
+                                new Pair<>(TEST_KEY_1, TEST_HASH_1),
+                                new Pair<>(TEST_KEY_2, TEST_HASH_2)));
+        mNewChunkListing =
+                createChunkListing(
+                        CryptoTestUtils.mapOf(
+                                new Pair<>(TEST_HASH_1, TEST_LENGTH_1),
+                                new Pair<>(TEST_HASH_2, TEST_LENGTH_2)));
+        when(mKvBackupEncrypter.getNewKeyValueListing()).thenReturn(mNewKeyValueListing);
+        when(mEncryptedBackupTask.performIncrementalBackup(
+                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
+                .thenReturn(mNewChunkListing);
+        when(mEncryptedBackupTask.performNonIncrementalBackup(
+                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
+                .thenReturn(mNewChunkListing);
+
+        mTask =
+                new EncryptedKvBackupTask(
+                        mTertiaryKeyManager,
+                        mKeyValueListingStore,
+                        mSecondaryKey,
+                        mChunkListingStore,
+                        mKvBackupEncrypter,
+                        mEncryptedBackupTask,
+                        TEST_PACKAGE_1);
+    }
+
+    @Test
+    public void testPerformBackup_rotationRequired_deletesListings() throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+        // Throw an IOException so it aborts before saving the new listings.
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+    }
+
+    @Test
+    public void testPerformBackup_rotationRequiredButIncremental_throws() throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+
+        assertThrows(NonIncrementalBackupRequiredException.class,
+                () -> mTask.performBackup(INCREMENTAL));
+    }
+
+    @Test
+    public void testPerformBackup_rotationRequiredAndNonIncremental_performsNonIncrementalBackup()
+            throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+
+        mTask.performBackup(NON_INCREMENTAL);
+
+        verify(mEncryptedBackupTask)
+                .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), any());
+    }
+
+    @Test
+    public void testPerformBackup_existingStateButNonIncremental_deletesListings() throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        // Throw an IOException so it aborts before saving the new listings.
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+    }
+
+    @Test
+    public void testPerformBackup_keyValueListingMissing_deletesChunkListingAndPerformsNonIncremental()
+            throws Exception {
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        // Throw an IOException so it aborts before saving the new listings.
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+        verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
+        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+    }
+
+    @Test
+    public void testPerformBackup_chunkListingMissing_deletesKeyValueListingAndPerformsNonIncremental()
+            throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+
+        // Throw an IOException so it aborts before saving the new listings.
+        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+                .thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+        verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
+        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+    }
+
+    @Test
+    public void testPerformBackup_existingStateAndIncremental_performsIncrementalBackup()
+            throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        ChunksMetadataProto.ChunkListing oldChunkListing =
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1)));
+        mChunkListingStore.saveProto(TEST_PACKAGE_1, oldChunkListing);
+
+        mTask.performBackup(INCREMENTAL);
+
+        verify(mEncryptedBackupTask)
+                .performIncrementalBackup(
+                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), mChunkListingCaptor.capture());
+        assertChunkListingsEqual(mChunkListingCaptor.getValue(), oldChunkListing);
+    }
+
+    @Test
+    public void testPerformBackup_noExistingStateAndNonIncremental_performsNonIncrementalBackup()
+            throws Exception {
+        mTask.performBackup(NON_INCREMENTAL);
+
+        verify(mEncryptedBackupTask)
+                .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(null));
+    }
+
+    @Test
+    public void testPerformBackup_incremental_savesNewListings() throws Exception {
+        mKeyValueListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+        mChunkListingStore.saveProto(
+                TEST_PACKAGE_1,
+                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+        mTask.performBackup(INCREMENTAL);
+
+        KeyValueListingProto.KeyValueListing actualKeyValueListing =
+                mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+        ChunksMetadataProto.ChunkListing actualChunkListing =
+                mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
+        assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
+        assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
+    }
+
+    @Test
+    public void testPerformBackup_nonIncremental_savesNewListings() throws Exception {
+        mTask.performBackup(NON_INCREMENTAL);
+
+        KeyValueListingProto.KeyValueListing actualKeyValueListing =
+                mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+        ChunksMetadataProto.ChunkListing actualChunkListing =
+                mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
+        assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
+        assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
+    }
+
+    private static KeyValueListingProto.KeyValueListing createKeyValueListing(
+            Map<String, ChunkHash> pairs) {
+        return new KeyValueListingBuilder().addAll(pairs).build();
+    }
+
+    private static ChunksMetadataProto.ChunkListing createChunkListing(
+            Map<ChunkHash, Integer> chunks) {
+        ChunksMetadataProto.Chunk[] listingChunks = new ChunksMetadataProto.Chunk[chunks.size()];
+        int chunksAdded = 0;
+        for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
+            listingChunks[chunksAdded] = CryptoTestUtils.newChunk(entry.getKey(), entry.getValue());
+            chunksAdded++;
+        }
+        return CryptoTestUtils.newChunkListingWithoutDocId(
+                /* fingerprintSalt */ new byte[0],
+                ChunksMetadataProto.AES_256_GCM,
+                ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
+                listingChunks);
+    }
+
+    private static void assertKeyValueListingsEqual(
+            KeyValueListingProto.KeyValueListing actual,
+            KeyValueListingProto.KeyValueListing expected) {
+        KeyValueListingProto.KeyValueEntry[] actualEntries = actual.entries;
+        KeyValueListingProto.KeyValueEntry[] expectedEntries = expected.entries;
+        assertThat(actualEntries.length).isEqualTo(expectedEntries.length);
+        for (int i = 0; i < actualEntries.length; i++) {
+            assertWithMessage("entry " + i)
+                    .that(actualEntries[i].key)
+                    .isEqualTo(expectedEntries[i].key);
+            assertWithMessage("entry " + i)
+                    .that(actualEntries[i].hash)
+                    .isEqualTo(expectedEntries[i].hash);
+        }
+    }
+
+    private static void assertChunkListingsEqual(
+            ChunksMetadataProto.ChunkListing actual, ChunksMetadataProto.ChunkListing expected) {
+        ChunksMetadataProto.Chunk[] actualChunks = actual.chunks;
+        ChunksMetadataProto.Chunk[] expectedChunks = expected.chunks;
+        assertThat(actualChunks.length).isEqualTo(expectedChunks.length);
+        for (int i = 0; i < actualChunks.length; i++) {
+            assertWithMessage("chunk " + i)
+                    .that(actualChunks[i].hash)
+                    .isEqualTo(expectedChunks[i].hash);
+            assertWithMessage("chunk " + i)
+                    .that(actualChunks[i].length)
+                    .isEqualTo(expectedChunks[i].length);
+        }
+        assertThat(actual.cipherType).isEqualTo(expected.cipherType);
+        assertThat(actual.documentId)
+                .isEqualTo(expected.documentId == null ? "" : expected.documentId);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java b/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
new file mode 100644
index 0000000..12b4459
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.app.backup.BackupDataOutput;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.FullRestoreDownloader;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.RestoreKeyFetcher;
+import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+
+/**
+ * Performs a key value restore by downloading the backup set, decrypting it and writing it to the
+ * file provided by backup manager.
+ */
+public class EncryptedKvRestoreTask {
+    private static final String ENCRYPTED_FILE_NAME = "encrypted_kv";
+
+    private final File mTemporaryFolder;
+    private final ChunkHasher mChunkHasher;
+    private final FullRestoreToFileTask mFullRestoreToFileTask;
+    private final BackupFileDecryptorTask mBackupFileDecryptorTask;
+
+    /** Constructs new instances of the task. */
+    public static class EncryptedKvRestoreTaskFactory {
+        /**
+         * Constructs a new instance.
+         *
+         * <p>Fetches the appropriate secondary key and uses this to unwrap the tertiary key. Stores
+         * temporary files in {@link Context#getFilesDir()}.
+         */
+        public EncryptedKvRestoreTask newInstance(
+                Context context,
+                RecoverableKeyStoreSecondaryKeyManager
+                                .RecoverableKeyStoreSecondaryKeyManagerProvider
+                        recoverableSecondaryKeyManagerProvider,
+                FullRestoreDownloader fullRestoreDownloader,
+                String secondaryKeyAlias,
+                WrappedKeyProto.WrappedKey wrappedTertiaryKey)
+                throws EncryptedRestoreException, NoSuchAlgorithmException, NoSuchPaddingException,
+                        KeyException, InvalidAlgorithmParameterException {
+            SecretKey tertiaryKey =
+                    RestoreKeyFetcher.unwrapTertiaryKey(
+                            recoverableSecondaryKeyManagerProvider,
+                            secondaryKeyAlias,
+                            wrappedTertiaryKey);
+
+            return new EncryptedKvRestoreTask(
+                    context.getFilesDir(),
+                    new ChunkHasher(tertiaryKey),
+                    new FullRestoreToFileTask(fullRestoreDownloader),
+                    new BackupFileDecryptorTask(tertiaryKey));
+        }
+    }
+
+    @VisibleForTesting
+    EncryptedKvRestoreTask(
+            File temporaryFolder,
+            ChunkHasher chunkHasher,
+            FullRestoreToFileTask fullRestoreToFileTask,
+            BackupFileDecryptorTask backupFileDecryptorTask) {
+        checkArgument(
+                temporaryFolder.isDirectory(), "Temporary folder must be an existing directory");
+
+        mTemporaryFolder = temporaryFolder;
+        mChunkHasher = chunkHasher;
+        mFullRestoreToFileTask = fullRestoreToFileTask;
+        mBackupFileDecryptorTask = backupFileDecryptorTask;
+    }
+
+    /**
+     * Runs the restore, writing the pairs in lexicographical order to the given file descriptor.
+     *
+     * <p>This will block for the duration of the restore.
+     *
+     * @throws EncryptedRestoreException if there is a problem decrypting or verifying the backup
+     */
+    public void getRestoreData(ParcelFileDescriptor output)
+            throws IOException, EncryptedRestoreException, BadPaddingException,
+                    InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+                    IllegalBlockSizeException, ShortBufferException, InvalidKeyException {
+        File encryptedFile = new File(mTemporaryFolder, ENCRYPTED_FILE_NAME);
+        try {
+            downloadDecryptAndWriteBackup(encryptedFile, output);
+        } finally {
+            encryptedFile.delete();
+        }
+    }
+
+    private void downloadDecryptAndWriteBackup(File encryptedFile, ParcelFileDescriptor output)
+            throws EncryptedRestoreException, IOException, BadPaddingException, InvalidKeyException,
+                    NoSuchAlgorithmException, IllegalBlockSizeException, ShortBufferException,
+                    InvalidAlgorithmParameterException {
+        mFullRestoreToFileTask.restoreToFile(encryptedFile);
+        DecryptedChunkKvOutput decryptedChunkKvOutput = new DecryptedChunkKvOutput(mChunkHasher);
+        mBackupFileDecryptorTask.decryptFile(encryptedFile, decryptedChunkKvOutput);
+
+        BackupDataOutput backupDataOutput = new BackupDataOutput(output.getFileDescriptor());
+        for (KeyValuePairProto.KeyValuePair pair : decryptedChunkKvOutput.getPairs()) {
+            backupDataOutput.writeEntityHeader(pair.key, pair.value.length);
+            backupDataOutput.writeEntityData(pair.value, pair.value.length);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java b/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
new file mode 100644
index 0000000..6666d95
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.os.ParcelFileDescriptor;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.testing.shadows.DataEntity;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(shadows = {ShadowBackupDataOutput.class})
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedKvRestoreTaskTest {
+    private static final String TEST_KEY_1 = "test_key_1";
+    private static final String TEST_KEY_2 = "test_key_2";
+    private static final String TEST_KEY_3 = "test_key_3";
+    private static final byte[] TEST_VALUE_1 = {1, 2, 3};
+    private static final byte[] TEST_VALUE_2 = {4, 5, 6};
+    private static final byte[] TEST_VALUE_3 = {20, 25, 30, 35};
+
+    @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private File temporaryDirectory;
+
+    @Mock private ParcelFileDescriptor mParcelFileDescriptor;
+    @Mock private ChunkHasher mChunkHasher;
+    @Mock private FullRestoreToFileTask mFullRestoreToFileTask;
+    @Mock private BackupFileDecryptorTask mBackupFileDecryptorTask;
+
+    private EncryptedKvRestoreTask task;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mChunkHasher.computeHash(any()))
+                .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
+        doAnswer(invocation -> writeTestPairsToFile(invocation.getArgument(0)))
+                .when(mFullRestoreToFileTask)
+                .restoreToFile(any());
+        doAnswer(
+                        invocation ->
+                                readPairsFromFile(
+                                        invocation.getArgument(0), invocation.getArgument(1)))
+                .when(mBackupFileDecryptorTask)
+                .decryptFile(any(), any());
+
+        temporaryDirectory = temporaryFolder.newFolder();
+        task =
+                new EncryptedKvRestoreTask(
+                        temporaryDirectory,
+                        mChunkHasher,
+                        mFullRestoreToFileTask,
+                        mBackupFileDecryptorTask);
+    }
+
+    @Test
+    public void testGetRestoreData_writesPairsToOutputInOrder() throws Exception {
+        task.getRestoreData(mParcelFileDescriptor);
+
+        assertThat(ShadowBackupDataOutput.getEntities())
+                .containsExactly(
+                        new DataEntity(TEST_KEY_1, TEST_VALUE_1),
+                        new DataEntity(TEST_KEY_2, TEST_VALUE_2),
+                        new DataEntity(TEST_KEY_3, TEST_VALUE_3))
+                .inOrder();
+    }
+
+    @Test
+    public void testGetRestoreData_exceptionDuringDecryption_throws() throws Exception {
+        doThrow(IOException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
+        assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+    }
+
+    @Test
+    public void testGetRestoreData_exceptionDuringDownload_throws() throws Exception {
+        doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
+        assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+    }
+
+    @Test
+    public void testGetRestoreData_exceptionDuringDecryption_deletesTemporaryFiles() throws Exception {
+        doThrow(InvalidKeyException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
+        assertThrows(InvalidKeyException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+        assertThat(temporaryDirectory.listFiles()).isEmpty();
+    }
+
+    @Test
+    public void testGetRestoreData_exceptionDuringDownload_deletesTemporaryFiles() throws Exception {
+        doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
+        assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+        assertThat(temporaryDirectory.listFiles()).isEmpty();
+    }
+
+    private static Void writeTestPairsToFile(File file) throws IOException {
+        // Write the pairs out of order to check the task sorts them.
+        Set<byte[]> pairs =
+                new HashSet<>(
+                        Arrays.asList(
+                                createPair(TEST_KEY_1, TEST_VALUE_1),
+                                createPair(TEST_KEY_3, TEST_VALUE_3),
+                                createPair(TEST_KEY_2, TEST_VALUE_2)));
+
+        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
+            oos.writeObject(pairs);
+        }
+        return null;
+    }
+
+    private static Void readPairsFromFile(File file, DecryptedChunkOutput decryptedChunkOutput)
+            throws IOException, ClassNotFoundException, InvalidKeyException,
+                    NoSuchAlgorithmException {
+        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
+                DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+            Set<byte[]> pairs = readPairs(ois);
+            for (byte[] pair : pairs) {
+                output.processChunk(pair, pair.length);
+            }
+        }
+
+        return null;
+    }
+
+    private static byte[] createPair(String key, byte[] value) {
+        return MessageNano.toByteArray(CryptoTestUtils.newPair(key, value));
+    }
+
+    @SuppressWarnings("unchecked") // deserialization.
+    private static Set<byte[]> readPairs(ObjectInputStream ois)
+            throws IOException, ClassNotFoundException {
+        return (Set<byte[]>) ois.readObject();
+    }
+
+    private static ChunkHash fakeHash(byte[] data) {
+        return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java b/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java
new file mode 100644
index 0000000..487c0d9
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/EncryptedRestoreException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/** Wraps any exception related to encryption which occurs during restore. */
+public class EncryptedRestoreException extends Exception {
+    public EncryptedRestoreException(String message) {
+        super(message);
+    }
+
+    public EncryptedRestoreException(Throwable cause) {
+        super(cause);
+    }
+
+    public EncryptedRestoreException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java b/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java
new file mode 100644
index 0000000..82f83f9
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/FullRestoreToFileTask.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.FullRestoreDownloader;
+import com.android.server.backup.encryption.FullRestoreDownloader.FinishType;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Reads a stream from a {@link FullRestoreDownloader} and writes it to a file for consumption by
+ * {@link BackupFileDecryptorTask}.
+ */
+public class FullRestoreToFileTask {
+    /**
+     * Maximum number of bytes which the framework can request from the full restore data stream in
+     * one call to {@link BackupTransport#getNextFullRestoreDataChunk}.
+     */
+    public static final int MAX_BYTES_FULL_RESTORE_CHUNK = 1024 * 32;
+
+    /** Returned when the end of a backup stream has been reached. */
+    private static final int END_OF_STREAM = -1;
+
+    private final FullRestoreDownloader mFullRestoreDownloader;
+    private final int mBufferSize;
+
+    /**
+     * Constructs a new instance which reads from the given package wrapper, using a buffer of size
+     * {@link #MAX_BYTES_FULL_RESTORE_CHUNK}.
+     */
+    public FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader) {
+        this(fullRestoreDownloader, MAX_BYTES_FULL_RESTORE_CHUNK);
+    }
+
+    @VisibleForTesting
+    FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader, int bufferSize) {
+        checkArgument(bufferSize > 0, "Buffer must have positive size");
+
+        this.mFullRestoreDownloader = fullRestoreDownloader;
+        this.mBufferSize = bufferSize;
+    }
+
+    /**
+     * Downloads the backup file from the server and writes it to the given file.
+     *
+     * <p>At the end of the download (success or failure), closes the connection and sends a
+     * Clearcut log.
+     */
+    public void restoreToFile(File targetFile) throws IOException {
+        try (BufferedOutputStream outputStream =
+                new BufferedOutputStream(new FileOutputStream(targetFile))) {
+            byte[] buffer = new byte[mBufferSize];
+            int bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
+            while (bytesRead != END_OF_STREAM) {
+                outputStream.write(buffer, /* off=*/ 0, bytesRead);
+                bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
+            }
+
+            outputStream.flush();
+
+            mFullRestoreDownloader.finish(FinishType.FINISHED);
+        } catch (IOException e) {
+            mFullRestoreDownloader.finish(FinishType.TRANSFER_FAILURE);
+            throw e;
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java b/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java
new file mode 100644
index 0000000..de8b734
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/FullRestoreToFileTaskTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.FullRestoreDownloader;
+import com.android.server.backup.encryption.FullRestoreDownloader.FinishType;
+
+import com.google.common.io.Files;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Random;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class FullRestoreToFileTaskTest {
+    private static final int TEST_RANDOM_SEED = 34;
+    private static final int TEST_MAX_CHUNK_SIZE_BYTES = 5;
+    private static final int TEST_DATA_LENGTH_BYTES = TEST_MAX_CHUNK_SIZE_BYTES * 20;
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private byte[] mTestData;
+    private File mTargetFile;
+    private FakeFullRestoreDownloader mFakeFullRestoreDownloader;
+    @Mock private FullRestoreDownloader mMockFullRestoreDownloader;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTargetFile = mTemporaryFolder.newFile();
+
+        mTestData = new byte[TEST_DATA_LENGTH_BYTES];
+        new Random(TEST_RANDOM_SEED).nextBytes(mTestData);
+        mFakeFullRestoreDownloader = new FakeFullRestoreDownloader(mTestData);
+    }
+
+    private FullRestoreToFileTask createTaskWithFakeDownloader() {
+        return new FullRestoreToFileTask(mFakeFullRestoreDownloader, TEST_MAX_CHUNK_SIZE_BYTES);
+    }
+
+    private FullRestoreToFileTask createTaskWithMockDownloader() {
+        return new FullRestoreToFileTask(mMockFullRestoreDownloader, TEST_MAX_CHUNK_SIZE_BYTES);
+    }
+
+    @Test
+    public void restoreToFile_readsDataAndWritesToFile() throws Exception {
+        FullRestoreToFileTask task = createTaskWithFakeDownloader();
+        task.restoreToFile(mTargetFile);
+        assertThat(Files.toByteArray(mTargetFile)).isEqualTo(mTestData);
+    }
+
+    @Test
+    public void restoreToFile_noErrors_closesDownloaderWithFinished() throws Exception {
+        FullRestoreToFileTask task = createTaskWithMockDownloader();
+        when(mMockFullRestoreDownloader.readNextChunk(any())).thenReturn(-1);
+
+        task.restoreToFile(mTargetFile);
+
+        verify(mMockFullRestoreDownloader).finish(FinishType.FINISHED);
+    }
+
+    @Test
+    public void restoreToFile_ioException_closesDownloaderWithTransferFailure() throws Exception {
+        FullRestoreToFileTask task = createTaskWithMockDownloader();
+        when(mMockFullRestoreDownloader.readNextChunk(any())).thenThrow(IOException.class);
+
+        assertThrows(IOException.class, () -> task.restoreToFile(mTargetFile));
+
+        verify(mMockFullRestoreDownloader).finish(FinishType.TRANSFER_FAILURE);
+    }
+
+    /** Fake package wrapper which returns data from a byte array. */
+    private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
+
+        private final ByteArrayInputStream mData;
+
+        FakeFullRestoreDownloader(byte[] data) {
+            // We override all methods of the superclass, so it does not require any collaborators.
+            super();
+            this.mData = new ByteArrayInputStream(data);
+        }
+
+        @Override
+        public int readNextChunk(byte[] buffer) throws IOException {
+            return mData.read(buffer);
+        }
+
+        @Override
+        public void finish(FinishType finishType) {
+            // Do nothing.
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java b/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java
new file mode 100644
index 0000000..d436554
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTask.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.LockScreenRequiredException;
+import android.security.keystore.recovery.RecoveryController;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+
+import java.security.InvalidKeyException;
+import java.security.UnrecoverableKeyException;
+import java.util.Collections;
+import java.util.Optional;
+
+/**
+ * Initializes the device for encrypted backup, through generating a secondary key, and setting its
+ * alias in the settings.
+ *
+ * <p>If the device is already initialized, this is a no-op.
+ */
+public class InitializeRecoverableSecondaryKeyTask {
+    private static final String TAG = "InitializeRecoverableSecondaryKeyTask";
+
+    private final Context mContext;
+    private final CryptoSettings mCryptoSettings;
+    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+    private final CryptoBackupServer mBackupServer;
+
+    /**
+     * A new instance.
+     *
+     * @param cryptoSettings Settings to store the active key alias.
+     * @param secondaryKeyManager Key manager to generate the new active secondary key.
+     * @param backupServer Server with which to sync the active key alias.
+     */
+    public InitializeRecoverableSecondaryKeyTask(
+            Context context,
+            CryptoSettings cryptoSettings,
+            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
+            CryptoBackupServer backupServer) {
+        mContext = context;
+        mCryptoSettings = cryptoSettings;
+        mSecondaryKeyManager = secondaryKeyManager;
+        mBackupServer = backupServer;
+    }
+
+    /**
+     * Initializes the device for encrypted backup, by generating a recoverable secondary key, then
+     * sending that alias to the backup server and saving it in local settings.
+     *
+     * <p>If there is already an active secondary key then does nothing. If the active secondary key
+     * is destroyed then throws {@link InvalidKeyException}.
+     *
+     * <p>If a key rotation is pending and able to finish (i.e., the new key has synced with the
+     * remote trusted hardware module), then it completes the rotation before returning the key.
+     *
+     * @return The active secondary key.
+     * @throws InvalidKeyException if the secondary key is in a bad state.
+     */
+    public RecoverableKeyStoreSecondaryKey run()
+            throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
+                    InternalRecoveryServiceException {
+        // Complete any pending key rotations
+        new RotateSecondaryKeyTask(
+                        mContext,
+                        mSecondaryKeyManager,
+                        mBackupServer,
+                        mCryptoSettings,
+                        RecoveryController.getInstance(mContext))
+                .run();
+
+        return runInternal();
+    }
+
+    private RecoverableKeyStoreSecondaryKey runInternal()
+            throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
+                    InternalRecoveryServiceException {
+        Optional<RecoverableKeyStoreSecondaryKey> maybeActiveKey = loadFromSetting();
+
+        if (maybeActiveKey.isPresent()) {
+            assertKeyNotDestroyed(maybeActiveKey.get());
+            Slog.d(TAG, "Secondary key already initialized: " + maybeActiveKey.get().getAlias());
+            return maybeActiveKey.get();
+        }
+
+        Slog.v(TAG, "Initializing for crypto: generating a secondary key.");
+        RecoverableKeyStoreSecondaryKey key = mSecondaryKeyManager.generate();
+
+        String alias = key.getAlias();
+        Slog.i(TAG, "Generated new secondary key " + alias);
+
+        // No tertiary keys yet as we are creating a brand new secondary (without rotation).
+        mBackupServer.setActiveSecondaryKeyAlias(alias, /*tertiaryKeys=*/ Collections.emptyMap());
+        Slog.v(TAG, "Successfully synced %s " + alias + " with server.");
+
+        mCryptoSettings.initializeWithKeyAlias(alias);
+        Slog.v(TAG, "Successfully saved " + alias + " as active secondary to disk.");
+
+        return key;
+    }
+
+    private void assertKeyNotDestroyed(RecoverableKeyStoreSecondaryKey key)
+            throws InvalidKeyException {
+        if (key.getStatus(mContext) == RecoverableKeyStoreSecondaryKey.Status.DESTROYED) {
+            throw new InvalidKeyException("Key destroyed: " + key.getAlias());
+        }
+    }
+
+    private Optional<RecoverableKeyStoreSecondaryKey> loadFromSetting()
+            throws InvalidKeyException, UnrecoverableKeyException,
+                    InternalRecoveryServiceException {
+
+        // TODO: b/141856950.
+        if (!mCryptoSettings.getIsInitialized()) {
+            return Optional.empty();
+        }
+
+        Optional<String> maybeAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
+        if (!maybeAlias.isPresent()) {
+            throw new InvalidKeyException(
+                    "Settings said crypto was initialized, but there was no active secondary"
+                            + " alias");
+        }
+
+        String alias = maybeAlias.get();
+
+        Optional<RecoverableKeyStoreSecondaryKey> key;
+        key = mSecondaryKeyManager.get(alias);
+
+        if (!key.isPresent()) {
+            throw new InvalidKeyException(
+                    "Initialized with key but it was not in key store: " + alias);
+        }
+
+        return key;
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java b/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java
new file mode 100644
index 0000000..4a7ae03
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/InitializeRecoverableSecondaryKeyTaskTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static android.security.keystore.recovery.RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.app.Application;
+import android.security.keystore.recovery.RecoveryController;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.testing.fakes.FakeCryptoBackupServer;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import java.security.InvalidKeyException;
+import java.security.SecureRandom;
+import java.util.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(shadows = {ShadowRecoveryController.class})
+@RunWith(RobolectricTestRunner.class)
+public class InitializeRecoverableSecondaryKeyTaskTest {
+    @Mock private CryptoSettings mMockCryptoSettings;
+
+    private Application mApplication;
+    private InitializeRecoverableSecondaryKeyTask mTask;
+    private CryptoSettings mCryptoSettings;
+    private FakeCryptoBackupServer mFakeCryptoBackupServer;
+    private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        ShadowRecoveryController.reset();
+
+        mApplication = ApplicationProvider.getApplicationContext();
+        mFakeCryptoBackupServer = new FakeCryptoBackupServer();
+        mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
+        mSecondaryKeyManager =
+                new RecoverableKeyStoreSecondaryKeyManager(
+                        RecoveryController.getInstance(mApplication), new SecureRandom());
+
+        mTask =
+                new InitializeRecoverableSecondaryKeyTask(
+                        mApplication, mCryptoSettings, mSecondaryKeyManager, mFakeCryptoBackupServer);
+    }
+
+    @Test
+    public void testRun_generatesNewKeyInRecoveryController() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mTask.run();
+
+        assertThat(RecoveryController.getInstance(mApplication).getAliases())
+                .contains(key.getAlias());
+    }
+
+    @Test
+    public void testRun_setsAliasOnServer() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mTask.run();
+
+        assertThat(mFakeCryptoBackupServer.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(key.getAlias());
+    }
+
+    @Test
+    public void testRun_setsAliasInSettings() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(key.getAlias());
+    }
+
+    @Test
+    public void testRun_initializesSettings() throws Exception {
+        mTask.run();
+
+        assertThat(mCryptoSettings.getIsInitialized()).isTrue();
+    }
+
+    @Test
+    public void testRun_initializeSettingsFails_throws() throws Exception {
+        useMockCryptoSettings();
+        doThrow(IllegalArgumentException.class)
+                .when(mMockCryptoSettings)
+                .initializeWithKeyAlias(any());
+
+
+        assertThrows(IllegalArgumentException.class, () -> mTask.run());
+    }
+
+    @Test
+    public void testRun_doesNotGenerateANewKeyIfOneIsAvailable() throws Exception {
+        RecoverableKeyStoreSecondaryKey key1 = mTask.run();
+        RecoverableKeyStoreSecondaryKey key2 = mTask.run();
+
+        assertThat(key1.getAlias()).isEqualTo(key2.getAlias());
+        assertThat(key2.getSecretKey()).isEqualTo(key2.getSecretKey());
+    }
+
+    @Test
+    public void testRun_existingKeyButDestroyed_throws() throws Exception {
+        RecoverableKeyStoreSecondaryKey key = mTask.run();
+        ShadowRecoveryController.setRecoveryStatus(
+                key.getAlias(), RECOVERY_STATUS_PERMANENT_FAILURE);
+
+        assertThrows(InvalidKeyException.class, () -> mTask.run());
+    }
+
+    @Test
+    public void testRun_settingsInitializedButNotSecondaryKeyAlias_throws() {
+        useMockCryptoSettings();
+        when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
+        when(mMockCryptoSettings.getActiveSecondaryKeyAlias()).thenReturn(Optional.empty());
+
+        assertThrows(InvalidKeyException.class, () -> mTask.run());
+    }
+
+    @Test
+    public void testRun_keyAliasSetButNotInStore_throws() {
+        useMockCryptoSettings();
+        when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
+        when(mMockCryptoSettings.getActiveSecondaryKeyAlias())
+                .thenReturn(Optional.of("missingAlias"));
+
+        assertThrows(InvalidKeyException.class, () -> mTask.run());
+    }
+
+    private void useMockCryptoSettings() {
+        mTask =
+                new InitializeRecoverableSecondaryKeyTask(
+                        mApplication,
+                        mMockCryptoSettings,
+                        mSecondaryKeyManager,
+                        mFakeCryptoBackupServer);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java b/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java
new file mode 100644
index 0000000..d20cd4c
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/KvBackupEncrypter.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupDataInput;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkEncryptor;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+
+/**
+ * Reads key value backup data from an input, converts each pair into a chunk and encrypts the
+ * chunks.
+ *
+ * <p>The caller should pass in the key value listing from the previous backup, if there is one.
+ * This class emits chunks for both existing and new pairs, using the provided listing to
+ * determine the hashes of pairs that already exist. During the backup it computes the new listing,
+ * which the caller should store on disk and pass in at the start of the next backup.
+ *
+ * <p>Also computes the message digest, which is {@code SHA-256(chunk hashes sorted
+ * lexicographically)}.
+ */
+public class KvBackupEncrypter implements BackupEncrypter {
+    private final BackupDataInput mBackupDataInput;
+
+    private KeyValueListingProto.KeyValueListing mOldKeyValueListing;
+    @Nullable private KeyValueListingBuilder mNewKeyValueListing;
+
+    /**
+     * Constructs a new instance which reads data from the given input.
+     *
+     * <p>By default this performs non-incremental backup, call {@link #setOldKeyValueListing} to
+     * perform incremental backup.
+     */
+    public KvBackupEncrypter(BackupDataInput backupDataInput) {
+        mBackupDataInput = backupDataInput;
+        mOldKeyValueListing = KeyValueListingBuilder.emptyListing();
+    }
+
+    /** Sets the old listing to perform incremental backup against. */
+    public void setOldKeyValueListing(KeyValueListingProto.KeyValueListing oldKeyValueListing) {
+        mOldKeyValueListing = oldKeyValueListing;
+    }
+
+    @Override
+    public Result backup(
+            SecretKey secretKey,
+            @Nullable byte[] unusedFingerprintMixerSalt,
+            Set<ChunkHash> unusedExistingChunks)
+            throws IOException, GeneralSecurityException {
+        ChunkHasher chunkHasher = new ChunkHasher(secretKey);
+        ChunkEncryptor chunkEncryptor = new ChunkEncryptor(secretKey, new SecureRandom());
+        mNewKeyValueListing = new KeyValueListingBuilder();
+        List<ChunkHash> allChunks = new ArrayList<>();
+        List<EncryptedChunk> newChunks = new ArrayList<>();
+
+        Map<String, ChunkHash> existingChunksToReuse = buildPairMap(mOldKeyValueListing);
+
+        while (mBackupDataInput.readNextHeader()) {
+            String key = mBackupDataInput.getKey();
+            Optional<byte[]> value = readEntireValue(mBackupDataInput);
+
+            // As this pair exists in the new backup, we don't need to add it from the previous
+            // backup.
+            existingChunksToReuse.remove(key);
+
+            // If the value is not present then this key has been deleted.
+            if (value.isPresent()) {
+                EncryptedChunk newChunk =
+                        createEncryptedChunk(chunkHasher, chunkEncryptor, key, value.get());
+                allChunks.add(newChunk.key());
+                newChunks.add(newChunk);
+                mNewKeyValueListing.addPair(key, newChunk.key());
+            }
+        }
+
+        allChunks.addAll(existingChunksToReuse.values());
+
+        mNewKeyValueListing.addAll(existingChunksToReuse);
+
+        return new Result(allChunks, newChunks, createMessageDigest(allChunks));
+    }
+
+    /**
+     * Returns a listing containing the pairs in the new backup.
+     *
+     * <p>You must call {@link #backup} first.
+     */
+    public KeyValueListingProto.KeyValueListing getNewKeyValueListing() {
+        checkState(mNewKeyValueListing != null, "Must call backup() first");
+        return mNewKeyValueListing.build();
+    }
+
+    private static Map<String, ChunkHash> buildPairMap(
+            KeyValueListingProto.KeyValueListing listing) {
+        Map<String, ChunkHash> map = new HashMap<>();
+        for (KeyValueListingProto.KeyValueEntry entry : listing.entries) {
+            map.put(entry.key, new ChunkHash(entry.hash));
+        }
+        return map;
+    }
+
+    private EncryptedChunk createEncryptedChunk(
+            ChunkHasher chunkHasher, ChunkEncryptor chunkEncryptor, String key, byte[] value)
+            throws InvalidKeyException, IllegalBlockSizeException {
+        KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
+        pair.key = key;
+        pair.value = Arrays.copyOf(value, value.length);
+
+        byte[] plaintext = KeyValuePairProto.KeyValuePair.toByteArray(pair);
+        return chunkEncryptor.encrypt(chunkHasher.computeHash(plaintext), plaintext);
+    }
+
+    private static byte[] createMessageDigest(List<ChunkHash> allChunks)
+            throws NoSuchAlgorithmException {
+        MessageDigest messageDigest =
+                MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
+        // TODO:b/141531271 Extract sorted chunks code to utility class
+        List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
+        Collections.sort(sortedChunks);
+        for (ChunkHash hash : sortedChunks) {
+            messageDigest.update(hash.getHash());
+        }
+        return messageDigest.digest();
+    }
+
+    private static Optional<byte[]> readEntireValue(BackupDataInput input) throws IOException {
+        // A negative data size indicates that this key should be deleted.
+        if (input.getDataSize() < 0) {
+            return Optional.empty();
+        }
+
+        byte[] value = new byte[input.getDataSize()];
+        int bytesRead = 0;
+        while (bytesRead < value.length) {
+            bytesRead += input.readEntityData(value, bytesRead, value.length - bytesRead);
+        }
+        return Optional.of(value);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java b/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java
new file mode 100644
index 0000000..ccfbfa4
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/KvBackupEncrypterTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.backup.BackupDataInput;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.chunking.EncryptedChunk;
+import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto.KeyValueListing;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto.KeyValuePair;
+import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
+import com.android.server.testing.shadows.DataEntity;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowBackupDataInput.class})
+public class KvBackupEncrypterTest {
+    private static final String KEY_ALGORITHM = "AES";
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+
+    private static final byte[] TEST_TERTIARY_KEY = Arrays.copyOf(new byte[0], 256 / Byte.SIZE);
+    private static final String TEST_KEY_1 = "test_key_1";
+    private static final String TEST_KEY_2 = "test_key_2";
+    private static final String TEST_KEY_3 = "test_key_3";
+    private static final byte[] TEST_VALUE_1 = {10, 11, 12};
+    private static final byte[] TEST_VALUE_2 = {13, 14, 15};
+    private static final byte[] TEST_VALUE_2B = {13, 14, 15, 16};
+    private static final byte[] TEST_VALUE_3 = {16, 17, 18};
+
+    private SecretKey mSecretKey;
+    private ChunkHasher mChunkHasher;
+
+    @Before
+    public void setUp() {
+        mSecretKey = new SecretKeySpec(TEST_TERTIARY_KEY, KEY_ALGORITHM);
+        mChunkHasher = new ChunkHasher(mSecretKey);
+
+        ShadowBackupDataInput.reset();
+    }
+
+    private KvBackupEncrypter createEncrypter(KeyValueListing keyValueListing) {
+        KvBackupEncrypter encrypter = new KvBackupEncrypter(new BackupDataInput(null));
+        encrypter.setOldKeyValueListing(keyValueListing);
+        return encrypter;
+    }
+
+    @Test
+    public void backup_noExistingBackup_encryptsAllPairs() throws Exception {
+        ShadowBackupDataInput.addEntity(TEST_KEY_1, TEST_VALUE_1);
+        ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2);
+
+        KeyValueListing emptyKeyValueListing = new KeyValueListingBuilder().build();
+        ImmutableSet<ChunkHash> emptyExistingChunks = ImmutableSet.of();
+        KvBackupEncrypter encrypter = createEncrypter(emptyKeyValueListing);
+
+        Result result =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, emptyExistingChunks);
+
+        assertThat(result.getAllChunks()).hasSize(2);
+        EncryptedChunk chunk1 = result.getNewChunks().get(0);
+        EncryptedChunk chunk2 = result.getNewChunks().get(1);
+        assertThat(chunk1.key()).isEqualTo(getChunkHash(TEST_KEY_1, TEST_VALUE_1));
+        KeyValuePair pair1 = decryptChunk(chunk1);
+        assertThat(pair1.key).isEqualTo(TEST_KEY_1);
+        assertThat(pair1.value).isEqualTo(TEST_VALUE_1);
+        assertThat(chunk2.key()).isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2));
+        KeyValuePair pair2 = decryptChunk(chunk2);
+        assertThat(pair2.key).isEqualTo(TEST_KEY_2);
+        assertThat(pair2.value).isEqualTo(TEST_VALUE_2);
+    }
+
+    @Test
+    public void backup_existingBackup_encryptsNewAndUpdatedPairs() throws Exception {
+        Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
+
+        // Update key 2 and add the new key 3.
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2B);
+        ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
+
+        KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
+        BackupEncrypter.Result secondResult =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
+
+        assertThat(secondResult.getAllChunks()).hasSize(3);
+        assertThat(secondResult.getNewChunks()).hasSize(2);
+        EncryptedChunk newChunk2 = secondResult.getNewChunks().get(0);
+        EncryptedChunk newChunk3 = secondResult.getNewChunks().get(1);
+        assertThat(newChunk2.key()).isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2B));
+        assertThat(decryptChunk(newChunk2).value).isEqualTo(TEST_VALUE_2B);
+        assertThat(newChunk3.key()).isEqualTo(getChunkHash(TEST_KEY_3, TEST_VALUE_3));
+        assertThat(decryptChunk(newChunk3).value).isEqualTo(TEST_VALUE_3);
+    }
+
+    @Test
+    public void backup_allChunksContainsHashesOfAllChunks() throws Exception {
+        Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
+
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
+
+        KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
+        BackupEncrypter.Result secondResult =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
+
+        assertThat(secondResult.getAllChunks())
+                .containsExactly(
+                        getChunkHash(TEST_KEY_1, TEST_VALUE_1),
+                        getChunkHash(TEST_KEY_2, TEST_VALUE_2),
+                        getChunkHash(TEST_KEY_3, TEST_VALUE_3));
+    }
+
+    @Test
+    public void backup_negativeSize_deletesKeyFromExistingBackup() throws Exception {
+        Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
+
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataInput.addEntity(new DataEntity(TEST_KEY_2));
+
+        KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
+        Result secondResult =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
+
+        assertThat(secondResult.getAllChunks())
+                .containsExactly(getChunkHash(TEST_KEY_1, TEST_VALUE_1));
+        assertThat(secondResult.getNewChunks()).isEmpty();
+    }
+
+    @Test
+    public void backup_returnsMessageDigestOverChunkHashes() throws Exception {
+        Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
+
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
+
+        KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
+        Result secondResult =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
+
+        MessageDigest messageDigest =
+                MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
+        ImmutableList<ChunkHash> sortedHashes =
+                Ordering.natural()
+                        .immutableSortedCopy(
+                                ImmutableList.of(
+                                        getChunkHash(TEST_KEY_1, TEST_VALUE_1),
+                                        getChunkHash(TEST_KEY_2, TEST_VALUE_2),
+                                        getChunkHash(TEST_KEY_3, TEST_VALUE_3)));
+        messageDigest.update(sortedHashes.get(0).getHash());
+        messageDigest.update(sortedHashes.get(1).getHash());
+        messageDigest.update(sortedHashes.get(2).getHash());
+        assertThat(secondResult.getDigest()).isEqualTo(messageDigest.digest());
+    }
+
+    @Test
+    public void getNewKeyValueListing_noExistingBackup_returnsCorrectListing() throws Exception {
+        KeyValueListing keyValueListing = runInitialBackupOfPairs1And2().first;
+
+        assertThat(keyValueListing.entries.length).isEqualTo(2);
+        assertThat(keyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
+        assertThat(keyValueListing.entries[0].hash)
+                .isEqualTo(getChunkHash(TEST_KEY_1, TEST_VALUE_1).getHash());
+        assertThat(keyValueListing.entries[1].key).isEqualTo(TEST_KEY_2);
+        assertThat(keyValueListing.entries[1].hash)
+                .isEqualTo(getChunkHash(TEST_KEY_2, TEST_VALUE_2).getHash());
+    }
+
+    @Test
+    public void getNewKeyValueListing_existingBackup_returnsCorrectListing() throws Exception {
+        Pair<KeyValueListing, Set<ChunkHash>> initialResult = runInitialBackupOfPairs1And2();
+
+        ShadowBackupDataInput.reset();
+        ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2B);
+        ShadowBackupDataInput.addEntity(TEST_KEY_3, TEST_VALUE_3);
+
+        KvBackupEncrypter encrypter = createEncrypter(initialResult.first);
+        encrypter.backup(mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialResult.second);
+
+        ImmutableMap<String, ChunkHash> keyValueListing =
+                listingToMap(encrypter.getNewKeyValueListing());
+        assertThat(keyValueListing).hasSize(3);
+        assertThat(keyValueListing)
+                .containsEntry(TEST_KEY_1, getChunkHash(TEST_KEY_1, TEST_VALUE_1));
+        assertThat(keyValueListing)
+                .containsEntry(TEST_KEY_2, getChunkHash(TEST_KEY_2, TEST_VALUE_2B));
+        assertThat(keyValueListing)
+                .containsEntry(TEST_KEY_3, getChunkHash(TEST_KEY_3, TEST_VALUE_3));
+    }
+
+    @Test
+    public void getNewKeyValueChunkListing_beforeBackup_throws() throws Exception {
+        KvBackupEncrypter encrypter = createEncrypter(new KeyValueListing());
+        assertThrows(IllegalStateException.class, encrypter::getNewKeyValueListing);
+    }
+
+    private ImmutableMap<String, ChunkHash> listingToMap(KeyValueListing listing) {
+        // We can't use the ImmutableMap collector directly because it isn't supported in Android
+        // guava.
+        return ImmutableMap.copyOf(
+                Arrays.stream(listing.entries)
+                        .collect(
+                                Collectors.toMap(
+                                        entry -> entry.key, entry -> new ChunkHash(entry.hash))));
+    }
+
+    private Pair<KeyValueListing, Set<ChunkHash>> runInitialBackupOfPairs1And2() throws Exception {
+        ShadowBackupDataInput.addEntity(TEST_KEY_1, TEST_VALUE_1);
+        ShadowBackupDataInput.addEntity(TEST_KEY_2, TEST_VALUE_2);
+
+        KeyValueListing initialKeyValueListing = new KeyValueListingBuilder().build();
+        ImmutableSet<ChunkHash> initialExistingChunks = ImmutableSet.of();
+        KvBackupEncrypter encrypter = createEncrypter(initialKeyValueListing);
+        Result firstResult =
+                encrypter.backup(
+                        mSecretKey, /*unusedFingerprintMixerSalt=*/ null, initialExistingChunks);
+
+        return Pair.create(
+                encrypter.getNewKeyValueListing(), ImmutableSet.copyOf(firstResult.getAllChunks()));
+    }
+
+    private ChunkHash getChunkHash(String key, byte[] value) throws Exception {
+        KeyValuePair pair = new KeyValuePair();
+        pair.key = key;
+        pair.value = Arrays.copyOf(value, value.length);
+        return mChunkHasher.computeHash(KeyValuePair.toByteArray(pair));
+    }
+
+    private KeyValuePair decryptChunk(EncryptedChunk encryptedChunk) throws Exception {
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(
+                Cipher.DECRYPT_MODE,
+                mSecretKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * Byte.SIZE, encryptedChunk.nonce()));
+        byte[] decryptedBytes = cipher.doFinal(encryptedChunk.encryptedBytes());
+        return KeyValuePair.parseFrom(decryptedBytes);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java b/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
new file mode 100644
index 0000000..78c370b
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/** Exception thrown when we cannot parse the encrypted backup file. */
+public class MalformedEncryptedFileException extends EncryptedRestoreException {
+    public MalformedEncryptedFileException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java b/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
new file mode 100644
index 0000000..1e4f43b
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/**
+ * Error thrown if the message digest of the plaintext backup does not match that in the {@link
+ * com.android.server.backup.encryption.protos.ChunksMetadataProto.ChunkOrdering}.
+ */
+public class MessageDigestMismatchException extends EncryptedRestoreException {
+    public MessageDigestMismatchException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java b/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
new file mode 100644
index 0000000..72e8a89
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/**
+ * Error thrown if attempting to rotate key when there is no current active secondary key set
+ * locally. This means the device needs to re-initialize, asking the backup server what the active
+ * secondary key is.
+ */
+public class NoActiveSecondaryKeyException extends Exception {
+    public NoActiveSecondaryKeyException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java b/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java
new file mode 100644
index 0000000..a3eda7d
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+// TODO(141840878): Update documentation.
+/**
+ * Exception thrown when the framework provides an incremental backup but the transport requires a
+ * non-incremental backup.
+ */
+public class NonIncrementalBackupRequiredException extends Exception {}
diff --git a/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java b/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
new file mode 100644
index 0000000..e5e2c1c
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyStore;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Finishes a rotation for a {@link
+ * com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey}.
+ */
+public class RotateSecondaryKeyTask {
+    private static final String TAG = "RotateSecondaryKeyTask";
+
+    private final Context mContext;
+    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+    private final CryptoBackupServer mBackupServer;
+    private final CryptoSettings mCryptoSettings;
+    private final RecoveryController mRecoveryController;
+
+    /**
+     * A new instance.
+     *
+     * @param secondaryKeyManager For loading the currently active and next secondary key.
+     * @param backupServer For loading and storing tertiary keys and for setting active secondary
+     *     key.
+     * @param cryptoSettings For checking the stored aliases for the next and active key.
+     * @param recoveryController For communicating with the Framework apis.
+     */
+    public RotateSecondaryKeyTask(
+            Context context,
+            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
+            CryptoBackupServer backupServer,
+            CryptoSettings cryptoSettings,
+            RecoveryController recoveryController) {
+        mContext = context;
+        mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
+        mCryptoSettings = Objects.requireNonNull(cryptoSettings);
+        mBackupServer = Objects.requireNonNull(backupServer);
+        mRecoveryController = Objects.requireNonNull(recoveryController);
+    }
+
+    /** Runs the task. */
+    public void run() {
+        // Never run more than one of these at the same time.
+        synchronized (RotateSecondaryKeyTask.class) {
+            runInternal();
+        }
+    }
+
+    private void runInternal() {
+        Optional<RecoverableKeyStoreSecondaryKey> maybeNextKey;
+        try {
+            maybeNextKey = getNextKey();
+        } catch (Exception e) {
+            Slog.e(TAG, "Error checking for next key", e);
+            return;
+        }
+
+        if (!maybeNextKey.isPresent()) {
+            Slog.d(TAG, "No secondary key rotation task pending. Exiting.");
+            return;
+        }
+
+        RecoverableKeyStoreSecondaryKey nextKey = maybeNextKey.get();
+        boolean isReady;
+        try {
+            isReady = isSecondaryKeyRotationReady(nextKey);
+        } catch (InternalRecoveryServiceException e) {
+            Slog.e(TAG, "Error encountered checking whether next secondary key is synced", e);
+            return;
+        }
+
+        if (!isReady) {
+            return;
+        }
+
+        try {
+            rotateToKey(nextKey);
+        } catch (Exception e) {
+            Slog.e(TAG, "Error trying to rotate to new secondary key", e);
+        }
+    }
+
+    private Optional<RecoverableKeyStoreSecondaryKey> getNextKey()
+            throws InternalRecoveryServiceException, UnrecoverableKeyException {
+        Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
+        if (!maybeNextAlias.isPresent()) {
+            return Optional.empty();
+        }
+        return mSecondaryKeyManager.get(maybeNextAlias.get());
+    }
+
+    private boolean isSecondaryKeyRotationReady(RecoverableKeyStoreSecondaryKey nextKey)
+            throws InternalRecoveryServiceException {
+        String nextAlias = nextKey.getAlias();
+        Slog.i(TAG, "Key rotation to " + nextAlias + " is pending. Checking key sync status.");
+        int status = mRecoveryController.getRecoveryStatus(nextAlias);
+
+        if (status == RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE) {
+            Slog.e(
+                    TAG,
+                    "Permanent failure to sync " + nextAlias + ". Cannot possibly rotate to it.");
+            mCryptoSettings.removeNextSecondaryKeyAlias();
+            return false;
+        }
+
+        if (status == RecoveryController.RECOVERY_STATUS_SYNCED) {
+            Slog.i(TAG, "Secondary key " + nextAlias + " has now synced! Commencing rotation.");
+        } else {
+            Slog.i(TAG, "Sync still pending for " + nextAlias);
+        }
+        return status == RecoveryController.RECOVERY_STATUS_SYNCED;
+    }
+
+    /**
+     * @throws ActiveSecondaryNotInKeychainException if the currently active secondary key is not in
+     *     the keychain.
+     * @throws IOException if there is an IO issue communicating with the server or loading from
+     *     disk.
+     * @throws NoActiveSecondaryKeyException if there is no active key set.
+     * @throws IllegalBlockSizeException if there is an issue decrypting a tertiary key.
+     * @throws InvalidKeyException if any of the secondary keys cannot be used for wrapping or
+     *     unwrapping tertiary keys.
+     */
+    private void rotateToKey(RecoverableKeyStoreSecondaryKey newSecondaryKey)
+            throws ActiveSecondaryNotInKeychainException, IOException,
+                    NoActiveSecondaryKeyException, IllegalBlockSizeException, InvalidKeyException,
+                    InternalRecoveryServiceException, UnrecoverableKeyException,
+                    InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+                    NoSuchPaddingException {
+        RecoverableKeyStoreSecondaryKey activeSecondaryKey = getActiveSecondaryKey();
+        String activeSecondaryKeyAlias = activeSecondaryKey.getAlias();
+        String newSecondaryKeyAlias = newSecondaryKey.getAlias();
+        if (newSecondaryKeyAlias.equals(activeSecondaryKeyAlias)) {
+            Slog.i(TAG, activeSecondaryKeyAlias + " was already the active alias.");
+            return;
+        }
+
+        TertiaryKeyStore tertiaryKeyStore =
+                TertiaryKeyStore.newInstance(mContext, activeSecondaryKey);
+        Map<String, SecretKey> tertiaryKeys = tertiaryKeyStore.getAll();
+
+        if (tertiaryKeys.isEmpty()) {
+            Slog.i(
+                    TAG,
+                    "No tertiary keys for " + activeSecondaryKeyAlias + ". No need to rewrap. ");
+            mBackupServer.setActiveSecondaryKeyAlias(
+                    newSecondaryKeyAlias, /*tertiaryKeys=*/ Collections.emptyMap());
+        } else {
+            Map<String, WrappedKeyProto.WrappedKey> rewrappedTertiaryKeys =
+                    rewrapAll(newSecondaryKey, tertiaryKeys);
+            TertiaryKeyStore.newInstance(mContext, newSecondaryKey).putAll(rewrappedTertiaryKeys);
+            Slog.i(
+                    TAG,
+                    "Successfully rewrapped " + rewrappedTertiaryKeys.size() + " tertiary keys");
+            mBackupServer.setActiveSecondaryKeyAlias(newSecondaryKeyAlias, rewrappedTertiaryKeys);
+            Slog.i(
+                    TAG,
+                    "Successfully uploaded new set of tertiary keys to "
+                            + newSecondaryKeyAlias
+                            + " alias");
+        }
+
+        mCryptoSettings.setActiveSecondaryKeyAlias(newSecondaryKeyAlias);
+        mCryptoSettings.removeNextSecondaryKeyAlias();
+        try {
+            mRecoveryController.removeKey(activeSecondaryKeyAlias);
+        } catch (InternalRecoveryServiceException e) {
+            Slog.e(TAG, "Error removing old secondary key from RecoverableKeyStoreLoader", e);
+        }
+    }
+
+    private RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
+            throws NoActiveSecondaryKeyException, ActiveSecondaryNotInKeychainException,
+                    InternalRecoveryServiceException, UnrecoverableKeyException {
+
+        Optional<String> activeSecondaryAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
+
+        if (!activeSecondaryAlias.isPresent()) {
+            Slog.i(
+                    TAG,
+                    "Was asked to rotate secondary key, but local config did not have a secondary "
+                            + "key alias set.");
+            throw new NoActiveSecondaryKeyException("No local active secondary key set.");
+        }
+
+        String activeSecondaryKeyAlias = activeSecondaryAlias.get();
+        Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
+                mSecondaryKeyManager.get(activeSecondaryKeyAlias);
+
+        if (!secondaryKey.isPresent()) {
+            throw new ActiveSecondaryNotInKeychainException(
+                    String.format(
+                            Locale.US,
+                            "Had local active recoverable key alias of %s but key was not in"
+                                + " user's keychain.",
+                            activeSecondaryKeyAlias));
+        }
+
+        return secondaryKey.get();
+    }
+
+    /**
+     * Rewraps all the tertiary keys.
+     *
+     * @param newSecondaryKey The secondary key with which to rewrap the tertiaries.
+     * @param tertiaryKeys The tertiary keys, by package name.
+     * @return The newly wrapped tertiary keys, by package name.
+     * @throws InvalidKeyException if any key is unusable.
+     * @throws IllegalBlockSizeException if could not decrypt.
+     */
+    private Map<String, WrappedKeyProto.WrappedKey> rewrapAll(
+            RecoverableKeyStoreSecondaryKey newSecondaryKey, Map<String, SecretKey> tertiaryKeys)
+            throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException,
+                    NoSuchAlgorithmException {
+        Map<String, WrappedKeyProto.WrappedKey> wrappedKeys = new HashMap<>();
+
+        for (String packageName : tertiaryKeys.keySet()) {
+            SecretKey tertiaryKey = tertiaryKeys.get(packageName);
+            wrappedKeys.put(
+                    packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey));
+        }
+
+        return wrappedKeys;
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java b/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
new file mode 100644
index 0000000..cda7317
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertFalse;
+
+import android.app.Application;
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.recovery.RecoveryController;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyStore;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.testing.fakes.FakeCryptoBackupServer;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.SecretKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowRecoveryController.class, ShadowRecoveryController.class})
+public class RotateSecondaryKeyTaskTest {
+    private static final String APP_1 = "app1";
+    private static final String APP_2 = "app2";
+    private static final String APP_3 = "app3";
+
+    private static final String CURRENT_SECONDARY_KEY_ALIAS =
+            "recoverablekey.alias/d524796bd07de3c2225c63d434eff698";
+    private static final String NEXT_SECONDARY_KEY_ALIAS =
+            "recoverablekey.alias/6c6d198a7f12e662b6bc45f4849db170";
+
+    private Application mApplication;
+    private RotateSecondaryKeyTask mTask;
+    private RecoveryController mRecoveryController;
+    private FakeCryptoBackupServer mBackupServer;
+    private CryptoSettings mCryptoSettings;
+    private Map<String, SecretKey> mTertiaryKeysByPackageName;
+    private RecoverableKeyStoreSecondaryKeyManager mRecoverableSecondaryKeyManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mApplication = ApplicationProvider.getApplicationContext();
+
+        mTertiaryKeysByPackageName = new HashMap<>();
+        mTertiaryKeysByPackageName.put(APP_1, generateAesKey());
+        mTertiaryKeysByPackageName.put(APP_2, generateAesKey());
+        mTertiaryKeysByPackageName.put(APP_3, generateAesKey());
+
+        mRecoveryController = RecoveryController.getInstance(mApplication);
+        mRecoverableSecondaryKeyManager =
+                new RecoverableKeyStoreSecondaryKeyManager(
+                        RecoveryController.getInstance(mApplication), new SecureRandom());
+        mBackupServer = new FakeCryptoBackupServer();
+        mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
+        addNextSecondaryKeyToRecoveryController();
+        mCryptoSettings.setNextSecondaryAlias(NEXT_SECONDARY_KEY_ALIAS);
+
+        mTask =
+                new RotateSecondaryKeyTask(
+                        mApplication,
+                        mRecoverableSecondaryKeyManager,
+                        mBackupServer,
+                        mCryptoSettings,
+                        mRecoveryController);
+
+        ShadowRecoveryController.reset();
+    }
+
+    @Test
+    public void run_failsIfThereIsNoActiveSecondaryKey() throws Exception {
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertFalse(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent());
+    }
+
+    @Test
+    public void run_failsIfActiveSecondaryIsNotInRecoveryController() throws Exception {
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        // Have to add it first as otherwise CryptoSettings throws an exception when trying to set
+        // it
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_doesNothingIfFlagIsDisabled() throws Exception {
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+        addWrappedTertiaries();
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_setsActiveSecondary() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+        addWrappedTertiaries();
+
+        mTask.run();
+
+        assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_rewrapsExistingTertiaryKeys() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+        addWrappedTertiaries();
+
+        mTask.run();
+
+        Map<String, WrappedKeyProto.WrappedKey> rewrappedKeys =
+                mBackupServer.getAllTertiaryKeys(NEXT_SECONDARY_KEY_ALIAS);
+        SecretKey secondaryKey = (SecretKey) mRecoveryController.getKey(NEXT_SECONDARY_KEY_ALIAS);
+        for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+            WrappedKeyProto.WrappedKey rewrappedKey = rewrappedKeys.get(packageName);
+            assertThat(KeyWrapUtils.unwrap(secondaryKey, rewrappedKey))
+                    .isEqualTo(mTertiaryKeysByPackageName.get(packageName));
+        }
+    }
+
+    @Test
+    public void run_persistsRewrappedKeysToDisk() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+        addWrappedTertiaries();
+
+        mTask.run();
+
+        RecoverableKeyStoreSecondaryKey secondaryKey = getRecoverableKey(NEXT_SECONDARY_KEY_ALIAS);
+        Map<String, SecretKey> keys =
+                TertiaryKeyStore.newInstance(mApplication, secondaryKey).getAll();
+        for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+            SecretKey tertiaryKey = mTertiaryKeysByPackageName.get(packageName);
+            SecretKey newlyWrappedKey = keys.get(packageName);
+            assertThat(tertiaryKey.getEncoded()).isEqualTo(newlyWrappedKey.getEncoded());
+        }
+    }
+
+    @Test
+    public void run_stillSetsActiveSecondaryIfNoTertiaries() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_setsActiveSecondaryKeyAliasInSettings() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_removesNextSecondaryKeyAliasInSettings() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+    }
+
+    @Test
+    public void run_deletesOldKeyFromRecoverableKeyStoreLoader() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mRecoveryController.getKey(CURRENT_SECONDARY_KEY_ALIAS)).isNull();
+    }
+
+    @Test
+    public void run_doesNotRotateIfNoNextAlias() throws Exception {
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+        mCryptoSettings.removeNextSecondaryKeyAlias();
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+        assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+    }
+
+    @Test
+    public void run_doesNotRotateIfKeyIsNotSyncedYet() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_doesNotClearNextKeyIfSyncIsJustPending() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get())
+                .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_doesNotRotateIfPermanentFailure() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+                .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+    }
+
+    @Test
+    public void run_removesNextKeyIfPermanentFailure() throws Exception {
+        addNextSecondaryKeyToRecoveryController();
+        setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
+        addCurrentSecondaryKeyToRecoveryController();
+        mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+        mBackupServer.setActiveSecondaryKeyAlias(
+                CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+        mTask.run();
+
+        assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+    }
+
+    private void setNextKeyRecoveryStatus(int status) throws Exception {
+        mRecoveryController.setRecoveryStatus(NEXT_SECONDARY_KEY_ALIAS, status);
+    }
+
+    private void addCurrentSecondaryKeyToRecoveryController() throws Exception {
+        mRecoveryController.generateKey(CURRENT_SECONDARY_KEY_ALIAS);
+    }
+
+    private void addNextSecondaryKeyToRecoveryController() throws Exception {
+        mRecoveryController.generateKey(NEXT_SECONDARY_KEY_ALIAS);
+    }
+
+    private void addWrappedTertiaries() throws Exception {
+        TertiaryKeyStore tertiaryKeyStore =
+                TertiaryKeyStore.newInstance(
+                        mApplication, getRecoverableKey(CURRENT_SECONDARY_KEY_ALIAS));
+
+        for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+            tertiaryKeyStore.save(packageName, mTertiaryKeysByPackageName.get(packageName));
+        }
+    }
+
+    private RecoverableKeyStoreSecondaryKey getRecoverableKey(String alias) throws Exception {
+        return mRecoverableSecondaryKeyManager.get(alias).get();
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java b/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
new file mode 100644
index 0000000..515db86
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/** Exception thrown when aa backup has exceeded the space allowed for that user */
+public class SizeQuotaExceededException extends RuntimeException {
+    public SizeQuotaExceededException() {
+        super("Backup size quota exceeded.");
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java b/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java
new file mode 100644
index 0000000..81169e2
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.LockScreenRequiredException;
+import android.util.Slog;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+
+import java.security.UnrecoverableKeyException;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Starts rotating to a new secondary key. Cannot complete until the screen is unlocked and the new
+ * key is synced.
+ */
+public class StartSecondaryKeyRotationTask {
+    private static final String TAG = "BE-StSecondaryKeyRotTsk";
+
+    private final CryptoSettings mCryptoSettings;
+    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+
+    public StartSecondaryKeyRotationTask(
+            CryptoSettings cryptoSettings,
+            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager) {
+        mCryptoSettings = Objects.requireNonNull(cryptoSettings);
+        mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
+    }
+
+    /** Begin the key rotation */
+    public void run() {
+        Slog.i(TAG, "Attempting to initiate a secondary key rotation.");
+
+        Optional<String> maybeCurrentAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
+        if (!maybeCurrentAlias.isPresent()) {
+            Slog.w(TAG, "No active current alias. Cannot trigger a secondary rotation.");
+            return;
+        }
+        String currentAlias = maybeCurrentAlias.get();
+
+        Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
+        if (maybeNextAlias.isPresent()) {
+            String nextAlias = maybeNextAlias.get();
+            if (nextAlias.equals(currentAlias)) {
+                // Shouldn't be possible, but guard against accidentally deleting the active key.
+                Slog.e(TAG, "Was already trying to rotate to what is already the active key.");
+            } else {
+                Slog.w(TAG, "Was already rotating to another key. Cancelling that.");
+                try {
+                    mSecondaryKeyManager.remove(nextAlias);
+                } catch (Exception e) {
+                    Slog.wtf(TAG, "Could not remove old key", e);
+                }
+            }
+            mCryptoSettings.removeNextSecondaryKeyAlias();
+        }
+
+        RecoverableKeyStoreSecondaryKey newSecondaryKey;
+        try {
+            newSecondaryKey = mSecondaryKeyManager.generate();
+        } catch (LockScreenRequiredException e) {
+            Slog.e(TAG, "No lock screen is set - cannot generate a new key to rotate to.", e);
+            return;
+        } catch (InternalRecoveryServiceException e) {
+            Slog.e(TAG, "Internal error in Recovery Controller, failed to rotate key.", e);
+            return;
+        } catch (UnrecoverableKeyException e) {
+            Slog.e(TAG, "Failed to get key after generating, failed to rotate", e);
+            return;
+        }
+
+        String alias = newSecondaryKey.getAlias();
+        Slog.i(TAG, "Generated a new secondary key with alias '" + alias + "'.");
+        try {
+            mCryptoSettings.setNextSecondaryAlias(alias);
+            Slog.i(TAG, "Successfully set '" + alias + "' as next key to rotate to");
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Unexpected error setting next alias", e);
+            try {
+                mSecondaryKeyManager.remove(alias);
+            } catch (Exception err) {
+                Slog.wtf(TAG, "Failed to remove generated key after encountering error", err);
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java b/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java
new file mode 100644
index 0000000..4ac4fa8
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.recovery.RecoveryController;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.security.SecureRandom;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowRecoveryController.class})
+@Presubmit
+public class StartSecondaryKeyRotationTaskTest {
+
+    private CryptoSettings mCryptoSettings;
+    private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+    private StartSecondaryKeyRotationTask mStartSecondaryKeyRotationTask;
+
+    @Before
+    public void setUp() throws Exception {
+        mSecondaryKeyManager =
+                new RecoverableKeyStoreSecondaryKeyManager(
+                        RecoveryController.getInstance(RuntimeEnvironment.application),
+                        new SecureRandom());
+        mCryptoSettings = CryptoSettings.getInstanceForTesting(RuntimeEnvironment.application);
+        mStartSecondaryKeyRotationTask =
+                new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager);
+
+        ShadowRecoveryController.reset();
+    }
+
+    @Test
+    public void run_doesNothingIfNoActiveSecondaryExists() {
+        mStartSecondaryKeyRotationTask.run();
+
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
+    }
+
+    @Test
+    public void run_doesNotRemoveExistingNextSecondaryKeyIfItIsAlreadyActive() throws Exception {
+        generateAnActiveKey();
+        String activeAlias = mCryptoSettings.getActiveSecondaryKeyAlias().get();
+        mCryptoSettings.setNextSecondaryAlias(activeAlias);
+
+        mStartSecondaryKeyRotationTask.run();
+
+        assertThat(mSecondaryKeyManager.get(activeAlias).isPresent()).isTrue();
+    }
+
+    @Test
+    public void run_doesRemoveExistingNextSecondaryKeyIfItIsNotYetActive() throws Exception {
+        generateAnActiveKey();
+        RecoverableKeyStoreSecondaryKey nextKey = mSecondaryKeyManager.generate();
+        String nextAlias = nextKey.getAlias();
+        mCryptoSettings.setNextSecondaryAlias(nextAlias);
+
+        mStartSecondaryKeyRotationTask.run();
+
+        assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isFalse();
+    }
+
+    @Test
+    public void run_generatesANewNextSecondaryKey() throws Exception {
+        generateAnActiveKey();
+
+        mStartSecondaryKeyRotationTask.run();
+
+        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isTrue();
+    }
+
+    @Test
+    public void run_generatesANewKeyThatExistsInKeyStore() throws Exception {
+        generateAnActiveKey();
+
+        mStartSecondaryKeyRotationTask.run();
+
+        String nextAlias = mCryptoSettings.getNextSecondaryKeyAlias().get();
+        assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isTrue();
+    }
+
+    private void generateAnActiveKey() throws Exception {
+        RecoverableKeyStoreSecondaryKey secondaryKey = mSecondaryKeyManager.generate();
+        mCryptoSettings.setActiveSecondaryKeyAlias(secondaryKey.getAlias());
+    }
+}
diff --git a/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java b/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
new file mode 100644
index 0000000..9a97e38
--- /dev/null
+++ b/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 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.encryption.tasks;
+
+/**
+ * Thrown when the backup file provided by the server uses encryption algorithms this version of
+ * backup does not support. This could happen if the backup was created with a newer version of the
+ * code.
+ */
+public class UnsupportedEncryptedFileException extends EncryptedRestoreException {
+    public UnsupportedEncryptedFileException(String message) {
+        super(message);
+    }
+}
diff --git a/com/android/server/backup/encryption/testing/DiffScriptProcessor.java b/com/android/server/backup/encryption/testing/DiffScriptProcessor.java
new file mode 100644
index 0000000..7e97924
--- /dev/null
+++ b/com/android/server/backup/encryption/testing/DiffScriptProcessor.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2019 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.encryption.testing;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+/**
+ * To be used as part of a fake backup server. Processes a Scotty diff script.
+ *
+ * <p>A Scotty diff script consists of an ASCII line denoting a command, optionally followed by a
+ * range of bytes. Command format is either
+ *
+ * <ul>
+ *   <li>A single 64-bit integer, followed by a new line: this denotes that the given number of
+ *       bytes are to follow in the stream. These bytes should be written directly to the new file.
+ *   <li>Two 64-bit integers, separated by a hyphen, followed by a new line: this says that the
+ *       given range of bytes from the original file ought to be copied into the new file.
+ * </ul>
+ */
+public class DiffScriptProcessor {
+
+    private static final int COPY_BUFFER_SIZE = 1024;
+
+    private static final String READ_MODE = "r";
+    private static final Pattern VALID_COMMAND_PATTERN = Pattern.compile("^\\d+(-\\d+)?$");
+
+    private final File mInput;
+    private final File mOutput;
+    private final long mInputLength;
+
+    /**
+     * A new instance, with {@code input} as previous file, and {@code output} as new file.
+     *
+     * @param input Previous file from which ranges of bytes are to be copied. This file should be
+     *     immutable.
+     * @param output Output file, to which the new data should be written.
+     * @throws IllegalArgumentException if input does not exist.
+     */
+    public DiffScriptProcessor(File input, File output) {
+        checkArgument(input.exists(), "input file did not exist.");
+        mInput = input;
+        mInputLength = input.length();
+        mOutput = Objects.requireNonNull(output);
+    }
+
+    public void process(InputStream diffScript) throws IOException, MalformedDiffScriptException {
+        RandomAccessFile randomAccessInput = new RandomAccessFile(mInput, READ_MODE);
+
+        try (FileOutputStream outputStream = new FileOutputStream(mOutput)) {
+            while (true) {
+                Optional<String> commandString = readCommand(diffScript);
+                if (!commandString.isPresent()) {
+                    return;
+                }
+                Command command = Command.parse(commandString.get());
+
+                if (command.mIsRange) {
+                    checkFileRange(command.mCount, command.mLimit);
+                    copyRange(randomAccessInput, outputStream, command.mCount, command.mLimit);
+                } else {
+                    long bytesCopied = copyBytes(diffScript, outputStream, command.mCount);
+                    if (bytesCopied < command.mCount) {
+                        throw new MalformedDiffScriptException(
+                                String.format(
+                                        Locale.US,
+                                        "Command to copy %d bytes from diff script, but only %d"
+                                            + " bytes available",
+                                        command.mCount,
+                                        bytesCopied));
+                    }
+                    if (diffScript.read() != '\n') {
+                        throw new MalformedDiffScriptException("Expected new line after bytes.");
+                    }
+                }
+            }
+        }
+    }
+
+    private void checkFileRange(long start, long end) throws MalformedDiffScriptException {
+        if (end < start) {
+            throw new MalformedDiffScriptException(
+                    String.format(
+                            Locale.US,
+                            "Command to copy %d-%d bytes from original file, but %2$d < %1$d.",
+                            start,
+                            end));
+        }
+
+        if (end >= mInputLength) {
+            throw new MalformedDiffScriptException(
+                    String.format(
+                            Locale.US,
+                            "Command to copy %d-%d bytes from original file, but file is only %d"
+                                + " bytes long.",
+                            start,
+                            end,
+                            mInputLength));
+        }
+    }
+
+    /**
+     * Reads a command from the input stream.
+     *
+     * @param inputStream The input.
+     * @return Optional of command, or empty if EOF.
+     */
+    private static Optional<String> readCommand(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+        int b;
+        while (!isEndOfCommand(b = inputStream.read())) {
+            byteArrayOutputStream.write(b);
+        }
+
+        byte[] bytes = byteArrayOutputStream.toByteArray();
+        if (bytes.length == 0) {
+            return Optional.empty();
+        } else {
+            return Optional.of(new String(bytes, UTF_8));
+        }
+    }
+
+    /**
+     * If the given output from {@link InputStream#read()} is the end of a command - i.e., a new
+     * line or the EOF.
+     *
+     * @param b The byte or -1.
+     * @return {@code true} if ends the command.
+     */
+    private static boolean isEndOfCommand(int b) {
+        return b == -1 || b == '\n';
+    }
+
+    /**
+     * Copies {@code n} bytes from {@code inputStream} to {@code outputStream}.
+     *
+     * @return The number of bytes copied.
+     * @throws IOException if there was a problem reading or writing.
+     */
+    private static long copyBytes(InputStream inputStream, OutputStream outputStream, long n)
+            throws IOException {
+        byte[] buffer = new byte[COPY_BUFFER_SIZE];
+        long copied = 0;
+        while (n - copied > COPY_BUFFER_SIZE) {
+            long read = copyBlock(inputStream, outputStream, buffer, COPY_BUFFER_SIZE);
+            if (read <= 0) {
+                return copied;
+            }
+        }
+        while (n - copied > 0) {
+            copied += copyBlock(inputStream, outputStream, buffer, (int) (n - copied));
+        }
+        return copied;
+    }
+
+    private static long copyBlock(
+            InputStream inputStream, OutputStream outputStream, byte[] buffer, int size)
+            throws IOException {
+        int read = inputStream.read(buffer, 0, size);
+        outputStream.write(buffer, 0, read);
+        return read;
+    }
+
+    /**
+     * Copies the given range of bytes from the input file to the output stream.
+     *
+     * @param input The input file.
+     * @param output The output stream.
+     * @param start Start position in the input file.
+     * @param end End position in the output file (inclusive).
+     * @throws IOException if there was a problem reading or writing.
+     */
+    private static void copyRange(RandomAccessFile input, OutputStream output, long start, long end)
+            throws IOException {
+        input.seek(start);
+
+        // Inefficient but obviously correct. If tests become slow, optimize.
+        for (; start <= end; start++) {
+            output.write(input.read());
+        }
+    }
+
+    /** Error thrown for a malformed diff script. */
+    public static class MalformedDiffScriptException extends Exception {
+        public MalformedDiffScriptException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * A command telling the processor either to insert n bytes, which follow, or copy n-m bytes
+     * from the original file.
+     */
+    private static class Command {
+        private final long mCount;
+        private final long mLimit;
+        private final boolean mIsRange;
+
+        private Command(long count, long limit, boolean isRange) {
+            mCount = count;
+            mLimit = limit;
+            mIsRange = isRange;
+        }
+
+        /**
+         * Attempts to parse the command string into a usable structure.
+         *
+         * @param command The command string, without a new line at the end.
+         * @throws MalformedDiffScriptException if the command is not a valid diff script command.
+         * @return The parsed command.
+         */
+        private static Command parse(String command) throws MalformedDiffScriptException {
+            if (!VALID_COMMAND_PATTERN.matcher(command).matches()) {
+                throw new MalformedDiffScriptException("Bad command: " + command);
+            }
+
+            Scanner commandScanner = new Scanner(command);
+            commandScanner.useDelimiter("-");
+            long n = commandScanner.nextLong();
+            if (!commandScanner.hasNextLong()) {
+                return new Command(n, 0L, /*isRange=*/ false);
+            }
+            long m = commandScanner.nextLong();
+            return new Command(n, m, /*isRange=*/ true);
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java b/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java
new file mode 100644
index 0000000..9d2272e
--- /dev/null
+++ b/com/android/server/backup/encryption/testing/QueuingNonAutomaticExecutorService.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 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.encryption.testing;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * ExecutorService which needs to be stepped through the jobs in its' queue.
+ *
+ * <p>This is a deliberately simple implementation because it's only used in testing. The queued
+ * jobs are run on the main thread to eliminate any race condition bugs.
+ */
+public class QueuingNonAutomaticExecutorService extends AbstractExecutorService {
+
+    private List<Runnable> mWaitingJobs = new ArrayList<>();
+    private int mWaitingJobCount = 0;
+
+    @Override
+    public void shutdown() {
+        mWaitingJobCount = mWaitingJobs.size();
+        mWaitingJobs = null; // This will force an error if jobs are submitted after shutdown
+    }
+
+    @Override
+    public List<Runnable> shutdownNow() {
+        List<Runnable> queuedJobs = mWaitingJobs;
+        shutdown();
+        return queuedJobs;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return mWaitingJobs == null;
+    }
+
+    @Override
+    public boolean isTerminated() {
+        return mWaitingJobs == null && mWaitingJobCount == 0;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+        long expiry = System.currentTimeMillis() + unit.toMillis(timeout);
+        for (Runnable job : mWaitingJobs) {
+            if (System.currentTimeMillis() > expiry) {
+                return false;
+            }
+
+            job.run();
+        }
+        return true;
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        mWaitingJobs.add(command);
+    }
+
+    public void runNext() {
+        if (mWaitingJobs.isEmpty()) {
+            throw new IllegalStateException("Attempted to run jobs on an empty paused executor");
+        }
+
+        mWaitingJobs.remove(0).run();
+    }
+}
diff --git a/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java
new file mode 100644
index 0000000..c3cb335
--- /dev/null
+++ b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransport.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2019 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.encryption.transport;
+
+import static com.android.server.backup.encryption.BackupEncryptionService.TAG;
+
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.encryption.KeyValueEncrypter;
+import com.android.server.backup.transport.DelegatingTransport;
+import com.android.server.backup.transport.TransportClient;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * This is an implementation of {@link IBackupTransport} that encrypts (or decrypts) the data when
+ * sending it (or receiving it) from the {@link IBackupTransport} returned by {@link
+ * TransportClient.connect(String)}.
+ */
+public class IntermediateEncryptingTransport extends DelegatingTransport {
+    private static final String BACKUP_TEMP_DIR = "backup";
+    private static final String RESTORE_TEMP_DIR = "restore";
+
+    private final TransportClient mTransportClient;
+    private final Object mConnectLock = new Object();
+    private final Context mContext;
+    private volatile IBackupTransport mRealTransport;
+    private AtomicReference<String> mNextRestorePackage = new AtomicReference<>();
+    private final KeyValueEncrypter mKeyValueEncrypter;
+    private final boolean mShouldEncrypt;
+
+    IntermediateEncryptingTransport(
+            TransportClient transportClient, Context context, boolean shouldEncrypt) {
+        this(transportClient, context, new KeyValueEncrypter(context), shouldEncrypt);
+    }
+
+    @VisibleForTesting
+    IntermediateEncryptingTransport(
+            TransportClient transportClient, Context context, KeyValueEncrypter keyValueEncrypter,
+            boolean shouldEncrypt) {
+        mTransportClient = transportClient;
+        mContext = context;
+        mKeyValueEncrypter = keyValueEncrypter;
+        mShouldEncrypt = shouldEncrypt;
+    }
+
+    @Override
+    protected IBackupTransport getDelegate() throws RemoteException {
+        if (mRealTransport == null) {
+            connect();
+        }
+        Log.d(TAG, "real transport = " + mRealTransport.name());
+        return mRealTransport;
+    }
+
+    @Override
+    public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags)
+            throws RemoteException {
+        if (!mShouldEncrypt) {
+            return super.performBackup(packageInfo, inFd, flags);
+        }
+
+        File encryptedStorageFile = getBackupTempStorage(packageInfo.packageName);
+        if (encryptedStorageFile == null) {
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        // Encrypt the backup data and write it into a temp file.
+        try (OutputStream encryptedOutput = new FileOutputStream(encryptedStorageFile)) {
+            mKeyValueEncrypter.encryptKeyValueData(packageInfo.packageName, inFd,
+                    encryptedOutput);
+        } catch (Throwable e) {
+            Log.e(TAG, "Failed to encrypt backup data: ", e);
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        // Pass the temp file to the real transport for backup.
+        try (FileInputStream encryptedInput = new FileInputStream(encryptedStorageFile)) {
+            return super.performBackup(
+                    packageInfo, ParcelFileDescriptor.dup(encryptedInput.getFD()), flags);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to read encrypted data from temp storage: ", e);
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+    }
+
+    @Override
+    public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException {
+        if (!mShouldEncrypt) {
+            return super.getRestoreData(outFd);
+        }
+
+        String nextRestorePackage = mNextRestorePackage.get();
+        if (nextRestorePackage == null) {
+            Log.e(TAG, "No next restore package set");
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        File encryptedStorageFile = getRestoreTempStorage(nextRestorePackage);
+        if (encryptedStorageFile == null) {
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        // Get encrypted restore data from the real transport and write it into a temp file.
+        try (FileOutputStream outputStream = new FileOutputStream(encryptedStorageFile)) {
+            int status = super.getRestoreData(ParcelFileDescriptor.dup(outputStream.getFD()));
+            if (status != BackupTransport.TRANSPORT_OK) {
+                Log.e(TAG, "Failed to read restore data from transport, status = " + status);
+                return status;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to write encrypted data to temp storage: ", e);
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        // Decrypt the data and write it into the fd given by the real transport.
+        try (InputStream inputStream = new FileInputStream(encryptedStorageFile)) {
+            mKeyValueEncrypter.decryptKeyValueData(nextRestorePackage, inputStream, outFd);
+            encryptedStorageFile.delete();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to decrypt restored data: ", e);
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        return BackupTransport.TRANSPORT_OK;
+    }
+
+    @Override
+    public RestoreDescription nextRestorePackage() throws RemoteException {
+        if (!mShouldEncrypt) {
+            return super.nextRestorePackage();
+        }
+
+        RestoreDescription restoreDescription = super.nextRestorePackage();
+        mNextRestorePackage.set(restoreDescription.getPackageName());
+
+        return restoreDescription;
+    }
+
+    @VisibleForTesting
+    protected File getBackupTempStorage(String packageName) {
+        return getTempStorage(packageName, BACKUP_TEMP_DIR);
+    }
+
+    @VisibleForTesting
+    protected File getRestoreTempStorage(String packageName) {
+        return getTempStorage(packageName, RESTORE_TEMP_DIR);
+    }
+
+    private File getTempStorage(String packageName, String operationType) {
+        File encryptedDir = new File(mContext.getFilesDir(), operationType);
+        encryptedDir.mkdir();
+        File encryptedFile = new File(encryptedDir, packageName);
+        try {
+            encryptedFile.createNewFile();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to create temp file for encrypted data: ", e);
+        }
+        return encryptedFile;
+    }
+
+    private void connect() throws RemoteException {
+        Log.i(TAG, "connecting " + mTransportClient);
+        synchronized (mConnectLock) {
+            if (mRealTransport == null) {
+                mRealTransport = mTransportClient.connect("IntermediateEncryptingTransport");
+                if (mRealTransport == null) {
+                    throw new RemoteException("Could not connect: " + mTransportClient);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    TransportClient getClient() {
+        return mTransportClient;
+    }
+
+    @VisibleForTesting
+    void setNextRestorePackage(String nextRestorePackage) {
+        mNextRestorePackage.set(nextRestorePackage);
+    }
+}
diff --git a/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java
new file mode 100644
index 0000000..7c4082c
--- /dev/null
+++ b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManager.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2019 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.encryption.transport;
+
+import static com.android.server.backup.encryption.BackupEncryptionService.TAG;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.server.backup.transport.TransportClientManager;
+import com.android.server.backup.transport.TransportStats;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Handles creation and cleanup of {@link IntermediateEncryptingTransport} instances. */
+public class IntermediateEncryptingTransportManager {
+    private static final String CALLER = "IntermediateEncryptingTransportManager";
+    private final TransportClientManager mTransportClientManager;
+    private final Object mTransportsLock = new Object();
+    private final Map<ComponentName, IntermediateEncryptingTransport> mTransports = new HashMap<>();
+    private Context mContext;
+
+    @VisibleForTesting
+    IntermediateEncryptingTransportManager(TransportClientManager transportClientManager) {
+        mTransportClientManager = transportClientManager;
+    }
+
+    public IntermediateEncryptingTransportManager(Context context) {
+        this(new TransportClientManager(UserHandle.myUserId(), context, new TransportStats()));
+        mContext = context;
+    }
+
+    /**
+     * Extract the {@link ComponentName} corresponding to the real {@link IBackupTransport}, and
+     * provide a {@link IntermediateEncryptingTransport} which is an implementation of {@link
+     * IBackupTransport} that encrypts (or decrypts) the data when sending it (or receiving it) from
+     * the real {@link IBackupTransport}.
+     *
+     * @param intent {@link Intent} created with a call to {@link
+     *     TransportClientManager.getEncryptingTransportIntent(ComponentName)}.
+     * @return
+     */
+    public IntermediateEncryptingTransport get(Intent intent) {
+        Intent transportIntent = TransportClientManager.getRealTransportIntent(intent);
+        Log.i(TAG, "get: intent:" + intent + " transportIntent:" + transportIntent);
+        synchronized (mTransportsLock) {
+            return mTransports.computeIfAbsent(
+                    transportIntent.getComponent(), c -> create(transportIntent));
+        }
+    }
+
+    /** Create an instance of {@link IntermediateEncryptingTransport}. */
+    private IntermediateEncryptingTransport create(Intent realTransportIntent) {
+        Log.d(TAG, "create: intent:" + realTransportIntent);
+
+        LockPatternUtils patternUtils = new LockPatternUtils(mContext);
+        boolean shouldEncrypt =
+                realTransportIntent.getComponent().getClassName().contains("EncryptedLocalTransport")
+                        && (patternUtils.isLockPatternEnabled(UserHandle.myUserId())
+                                || patternUtils.isLockPasswordEnabled(UserHandle.myUserId()));
+
+        return new IntermediateEncryptingTransport(
+                mTransportClientManager.getTransportClient(
+                        realTransportIntent.getComponent(),
+                        realTransportIntent.getExtras(),
+                        CALLER),
+                mContext,
+                shouldEncrypt);
+    }
+
+    /**
+     * Cleanup the {@link IntermediateEncryptingTransport} which was created by a call to {@link
+     * #get(Intent)} with this {@link Intent}.
+     */
+    public void cleanup(Intent intent) {
+        Intent transportIntent = TransportClientManager.getRealTransportIntent(intent);
+        Log.i(TAG, "cleanup: intent:" + intent + " transportIntent:" + transportIntent);
+
+        IntermediateEncryptingTransport transport;
+        synchronized (mTransportsLock) {
+            transport = mTransports.remove(transportIntent.getComponent());
+        }
+        if (transport != null) {
+            mTransportClientManager.disposeOfTransportClient(transport.getClient(), CALLER);
+        } else {
+            Log.i(TAG, "Could not find IntermediateEncryptingTransport");
+        }
+    }
+}
diff --git a/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManagerTest.java b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManagerTest.java
new file mode 100644
index 0000000..0d43a19
--- /dev/null
+++ b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportManagerTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2019 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.encryption.transport;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportClientManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class IntermediateEncryptingTransportManagerTest {
+    @Mock private TransportClient mTransportClient;
+    @Mock private TransportClientManager mTransportClientManager;
+
+    private final ComponentName mTransportComponent = new ComponentName("pkg", "class");
+    private final Bundle mExtras = new Bundle();
+    private Intent mEncryptingTransportIntent;
+    private IntermediateEncryptingTransportManager mIntermediateEncryptingTransportManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mExtras.putInt("test", 1);
+        mEncryptingTransportIntent =
+                TransportClientManager.getEncryptingTransportIntent(mTransportComponent)
+                        .putExtras(mExtras);
+        mIntermediateEncryptingTransportManager =
+                new IntermediateEncryptingTransportManager(mTransportClientManager);
+    }
+
+    @Test
+    public void testGet_createsClientWithRealTransportComponentAndExtras() {
+        when(mTransportClientManager.getTransportClient(any(), any(), any()))
+                .thenReturn(mTransportClient);
+
+        IntermediateEncryptingTransport intermediateEncryptingTransport =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+
+        assertEquals(mTransportClient, intermediateEncryptingTransport.getClient());
+        verify(mTransportClientManager, times(1))
+                .getTransportClient(eq(mTransportComponent), argThat(mExtras::kindofEquals), any());
+        verifyNoMoreInteractions(mTransportClientManager);
+    }
+
+    @Test
+    public void testGet_callTwice_returnsSameTransport() {
+        IntermediateEncryptingTransport transport1 =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+        IntermediateEncryptingTransport transport2 =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+
+        assertEquals(transport1, transport2);
+    }
+
+    @Test
+    public void testCleanup_disposesTransportClient() {
+        when(mTransportClientManager.getTransportClient(any(), any(), any()))
+                .thenReturn(mTransportClient);
+
+        IntermediateEncryptingTransport transport =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+        mIntermediateEncryptingTransportManager.cleanup(mEncryptingTransportIntent);
+
+        verify(mTransportClientManager, times(1)).getTransportClient(any(), any(), any());
+        verify(mTransportClientManager, times(1))
+                .disposeOfTransportClient(eq(mTransportClient), any());
+        verifyNoMoreInteractions(mTransportClientManager);
+    }
+
+    @Test
+    public void testCleanup_removesCachedTransport() {
+        when(mTransportClientManager.getTransportClient(any(), any(), any()))
+                .thenReturn(mTransportClient);
+
+        IntermediateEncryptingTransport transport1 =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+        mIntermediateEncryptingTransportManager.cleanup(mEncryptingTransportIntent);
+        IntermediateEncryptingTransport transport2 =
+                mIntermediateEncryptingTransportManager.get(mEncryptingTransportIntent);
+
+        assertNotSame(transport1, transport2);
+    }
+}
diff --git a/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java
new file mode 100644
index 0000000..a85b2e4
--- /dev/null
+++ b/com/android/server/backup/encryption/transport/IntermediateEncryptingTransportTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2019 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.encryption.transport;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.backup.BackupTransport;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.encryption.KeyValueEncrypter;
+import com.android.server.backup.transport.TransportClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class IntermediateEncryptingTransportTest {
+    private static final String TEST_PACKAGE_NAME = "test_package";
+
+    private IntermediateEncryptingTransport mIntermediateEncryptingTransport;
+    private final PackageInfo mTestPackage = new PackageInfo();
+
+    @Mock private IBackupTransport mRealTransport;
+    @Mock private TransportClient mTransportClient;
+    @Mock private ParcelFileDescriptor mParcelFileDescriptor;
+    @Mock private KeyValueEncrypter mKeyValueEncrypter;
+    @Mock private Context mContext;
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private File mTempFile;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mIntermediateEncryptingTransport =
+                new IntermediateEncryptingTransport(
+                        mTransportClient, mContext, mKeyValueEncrypter, true);
+        mTestPackage.packageName = TEST_PACKAGE_NAME;
+        mTempFile = mTemporaryFolder.newFile();
+
+        when(mTransportClient.connect(anyString())).thenReturn(mRealTransport);
+        when(mRealTransport.getRestoreData(any())).thenReturn(BackupTransport.TRANSPORT_OK);
+    }
+
+    @Test
+    public void testGetDelegate_callsConnect() throws Exception {
+        IBackupTransport ret = mIntermediateEncryptingTransport.getDelegate();
+
+        assertEquals(mRealTransport, ret);
+        verify(mTransportClient, times(1)).connect(anyString());
+        verifyNoMoreInteractions(mTransportClient);
+    }
+
+    @Test
+    public void testGetDelegate_callTwice_callsConnectOnce() throws Exception {
+        when(mTransportClient.connect(anyString())).thenReturn(mRealTransport);
+
+        IBackupTransport ret1 = mIntermediateEncryptingTransport.getDelegate();
+        IBackupTransport ret2 = mIntermediateEncryptingTransport.getDelegate();
+
+        assertEquals(mRealTransport, ret1);
+        assertEquals(mRealTransport, ret2);
+        verify(mTransportClient, times(1)).connect(anyString());
+        verifyNoMoreInteractions(mTransportClient);
+    }
+
+    @Test
+    public void testPerformBackup_shouldEncryptTrue_encryptsDataAndPassesToDelegate()
+            throws Exception {
+        mIntermediateEncryptingTransport =
+                new TestIntermediateTransport(mTransportClient, mContext, mKeyValueEncrypter, true);
+
+        mIntermediateEncryptingTransport.performBackup(mTestPackage, mParcelFileDescriptor, 0);
+
+        verify(mKeyValueEncrypter, times(1))
+                .encryptKeyValueData(eq(TEST_PACKAGE_NAME), eq(mParcelFileDescriptor), any());
+        verify(mRealTransport, times(1)).performBackup(eq(mTestPackage), any(), eq(0));
+    }
+
+    @Test
+    public void testPerformBackup_shouldEncryptFalse_doesntEncryptDataAndPassedToDelegate()
+            throws Exception {
+        mIntermediateEncryptingTransport =
+                new TestIntermediateTransport(
+                        mTransportClient, mContext, mKeyValueEncrypter, false);
+
+        mIntermediateEncryptingTransport.performBackup(mTestPackage, mParcelFileDescriptor, 0);
+
+        verifyZeroInteractions(mKeyValueEncrypter);
+        verify(mRealTransport, times(1))
+                .performBackup(eq(mTestPackage), eq(mParcelFileDescriptor), eq(0));
+    }
+
+    @Test
+    public void testGetRestoreData_shouldEncryptTrue_decryptsDataAndPassesToDelegate()
+            throws Exception {
+        mIntermediateEncryptingTransport =
+                new TestIntermediateTransport(mTransportClient, mContext, mKeyValueEncrypter, true);
+        mIntermediateEncryptingTransport.setNextRestorePackage(TEST_PACKAGE_NAME);
+
+        mIntermediateEncryptingTransport.getRestoreData(mParcelFileDescriptor);
+
+        verify(mKeyValueEncrypter, times(1))
+                .decryptKeyValueData(eq(TEST_PACKAGE_NAME), any(), eq(mParcelFileDescriptor));
+        verify(mRealTransport, times(1)).getRestoreData(any());
+    }
+
+    @Test
+    public void testGetRestoreData_shouldEncryptFalse_doesntDecryptDataAndPassesToDelegate()
+            throws Exception {
+        mIntermediateEncryptingTransport =
+                new TestIntermediateTransport(
+                        mTransportClient, mContext, mKeyValueEncrypter, false);
+        mIntermediateEncryptingTransport.setNextRestorePackage(TEST_PACKAGE_NAME);
+
+        mIntermediateEncryptingTransport.getRestoreData(mParcelFileDescriptor);
+
+        verifyZeroInteractions(mKeyValueEncrypter);
+        verify(mRealTransport, times(1)).getRestoreData(eq(mParcelFileDescriptor));
+    }
+
+    private final class TestIntermediateTransport extends IntermediateEncryptingTransport {
+        TestIntermediateTransport(
+                TransportClient transportClient,
+                Context context,
+                KeyValueEncrypter keyValueEncrypter,
+                boolean shouldEncrypt) {
+            super(transportClient, context, keyValueEncrypter, shouldEncrypt);
+        }
+
+        @Override
+        protected File getBackupTempStorage(String packageName) {
+            return mTempFile;
+        }
+
+        @Override
+        protected File getRestoreTempStorage(String packageName) {
+            return mTempFile;
+        }
+    }
+}
diff --git a/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java b/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
new file mode 100644
index 0000000..d13f711
--- /dev/null
+++ b/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
@@ -0,0 +1,282 @@
+package com.android.server.backup.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_VERSION;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+import android.util.StringBuilderPrinter;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Writes the backup of app-specific metadata to {@link FullBackupDataOutput}. This data is not
+ * backed up by the app's backup agent and is written before the agent writes its own data. This
+ * includes the app's:
+ *
+ * <ul>
+ *   <li>manifest
+ *   <li>widget data
+ *   <li>apk
+ *   <li>obb content
+ * </ul>
+ */
+// TODO(b/113807190): Fix or remove apk and obb implementation (only used for adb).
+public class AppMetadataBackupWriter {
+    private final FullBackupDataOutput mOutput;
+    private final PackageManager mPackageManager;
+
+    /** The destination of the backup is specified by {@code output}. */
+    public AppMetadataBackupWriter(FullBackupDataOutput output, PackageManager packageManager) {
+        mOutput = output;
+        mPackageManager = packageManager;
+    }
+
+    /**
+     * Back up the app's manifest without specifying a pseudo-directory for the TAR stream.
+     *
+     * @see #backupManifest(PackageInfo, File, File, String, String, boolean)
+     */
+    public void backupManifest(
+            PackageInfo packageInfo, File manifestFile, File filesDir, boolean withApk)
+            throws IOException {
+        backupManifest(
+                packageInfo,
+                manifestFile,
+                filesDir,
+                /* domain */ null,
+                /* linkDomain */ null,
+                withApk);
+    }
+
+    /**
+     * Back up the app's manifest.
+     *
+     * <ol>
+     *   <li>Write the app's manifest data to the specified temporary file {@code manifestFile}.
+     *   <li>Backup the file in TAR format to the backup destination {@link #mOutput}.
+     * </ol>
+     *
+     * <p>Note: {@code domain} and {@code linkDomain} are only used by adb to specify a
+     * pseudo-directory for the TAR stream.
+     */
+    // TODO(b/113806991): Look into streaming the backup data directly.
+    public void backupManifest(
+            PackageInfo packageInfo,
+            File manifestFile,
+            File filesDir,
+            @Nullable String domain,
+            @Nullable String linkDomain,
+            boolean withApk)
+            throws IOException {
+        byte[] manifestBytes = getManifestBytes(packageInfo, withApk);
+        FileOutputStream outputStream = new FileOutputStream(manifestFile);
+        outputStream.write(manifestBytes);
+        outputStream.close();
+
+        // We want the manifest block in the archive stream to be constant each time we generate
+        // a backup stream for the app. However, the underlying TAR mechanism sees it as a file and
+        // will propagate its last modified time. We pin the last modified time to zero to prevent
+        // the TAR header from varying.
+        manifestFile.setLastModified(0);
+
+        FullBackup.backupToTar(
+                packageInfo.packageName,
+                domain,
+                linkDomain,
+                filesDir.getAbsolutePath(),
+                manifestFile.getAbsolutePath(),
+                mOutput);
+    }
+
+    /**
+     * Gets the app's manifest as a byte array. All data are strings ending in LF.
+     *
+     * <p>The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    private byte[] getManifestBytes(PackageInfo packageInfo, boolean withApk) {
+        String packageName = packageInfo.packageName;
+        StringBuilder builder = new StringBuilder(4096);
+        StringBuilderPrinter printer = new StringBuilderPrinter(builder);
+
+        printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
+        printer.println(packageName);
+        printer.println(Long.toString(packageInfo.getLongVersionCode()));
+        printer.println(Integer.toString(Build.VERSION.SDK_INT));
+
+        String installerName = mPackageManager.getInstallerPackageName(packageName);
+        printer.println((installerName != null) ? installerName : "");
+
+        printer.println(withApk ? "1" : "0");
+
+        // Write the signature block.
+        SigningInfo signingInfo = packageInfo.signingInfo;
+        if (signingInfo == null) {
+            printer.println("0");
+        } else {
+            // Retrieve the newest signatures to write.
+            // TODO (b/73988180) use entire signing history in case of rollbacks.
+            Signature[] signatures = signingInfo.getApkContentsSigners();
+            printer.println(Integer.toString(signatures.length));
+            for (Signature sig : signatures) {
+                printer.println(sig.toCharsString());
+            }
+        }
+        return builder.toString().getBytes();
+    }
+
+    /**
+     * Backup specified widget data. The widget data is prefaced by a metadata header.
+     *
+     * <ol>
+     *   <li>Write a metadata header to the specified temporary file {@code metadataFile}.
+     *   <li>Write widget data bytes to the same file.
+     *   <li>Backup the file in TAR format to the backup destination {@link #mOutput}.
+     * </ol>
+     *
+     * @throws IllegalArgumentException if the widget data provided is empty.
+     */
+    // TODO(b/113806991): Look into streaming the backup data directly.
+    public void backupWidget(
+            PackageInfo packageInfo, File metadataFile, File filesDir, byte[] widgetData)
+            throws IOException {
+        Preconditions.checkArgument(widgetData.length > 0, "Can't backup widget with no data.");
+
+        String packageName = packageInfo.packageName;
+        FileOutputStream fileOutputStream = new FileOutputStream(metadataFile);
+        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+        DataOutputStream dataOutputStream = new DataOutputStream(bufferedOutputStream);
+
+        byte[] metadata = getMetadataBytes(packageName);
+        bufferedOutputStream.write(metadata); // bypassing DataOutputStream
+        writeWidgetData(dataOutputStream, widgetData);
+        bufferedOutputStream.flush();
+        dataOutputStream.close();
+
+        // As with the manifest file, guarantee consistency of the archive metadata for the widget
+        // block by using a fixed last modified time on the metadata file.
+        metadataFile.setLastModified(0);
+
+        FullBackup.backupToTar(
+                packageName,
+                /* domain */ null,
+                /* linkDomain */ null,
+                filesDir.getAbsolutePath(),
+                metadataFile.getAbsolutePath(),
+                mOutput);
+    }
+
+    /**
+     * Gets the app's metadata as a byte array. All entries are strings ending in LF.
+     *
+     * <p>The metadata format is:
+     *
+     * <pre>
+     *     BACKUP_METADATA_VERSION
+     *     package name
+     * </pre>
+     */
+    private byte[] getMetadataBytes(String packageName) {
+        StringBuilder builder = new StringBuilder(512);
+        StringBuilderPrinter printer = new StringBuilderPrinter(builder);
+        printer.println(Integer.toString(BACKUP_METADATA_VERSION));
+        printer.println(packageName);
+        return builder.toString().getBytes();
+    }
+
+    /**
+     * Write a byte array of widget data to the specified output stream. All integers are binary in
+     * network byte order.
+     *
+     * <p>The widget data format:
+     *
+     * <pre>
+     *     4 : Integer token identifying the widget data blob.
+     *     4 : Integer size of the widget data.
+     *     N : Raw bytes of the widget data.
+     * </pre>
+     */
+    private void writeWidgetData(DataOutputStream out, byte[] widgetData) throws IOException {
+        out.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
+        out.writeInt(widgetData.length);
+        out.write(widgetData);
+    }
+
+    /**
+     * Backup the app's .apk to the backup destination {@link #mOutput}. Currently only used for
+     * 'adb backup'.
+     */
+    // TODO(b/113807190): Investigate and potentially remove.
+    public void backupApk(PackageInfo packageInfo) {
+        // TODO: handle backing up split APKs
+        String appSourceDir = packageInfo.applicationInfo.getBaseCodePath();
+        String apkDir = new File(appSourceDir).getParent();
+        FullBackup.backupToTar(
+                packageInfo.packageName,
+                FullBackup.APK_TREE_TOKEN,
+                /* linkDomain */ null,
+                apkDir,
+                appSourceDir,
+                mOutput);
+    }
+
+    /**
+     * Backup the app's .obb files to the backup destination {@link #mOutput}. Currently only used
+     * for 'adb backup'.
+     */
+    // TODO(b/113807190): Investigate and potentially remove.
+    public void backupObb(@UserIdInt int userId, PackageInfo packageInfo) {
+        // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM doesn't have access to
+        // external storage.
+        Environment.UserEnvironment userEnv =
+                new Environment.UserEnvironment(userId);
+        File obbDir = userEnv.buildExternalStorageAppObbDirs(packageInfo.packageName)[0];
+        if (obbDir != null) {
+            if (MORE_DEBUG) {
+                Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
+            }
+            File[] obbFiles = obbDir.listFiles();
+            if (obbFiles != null) {
+                String obbDirName = obbDir.getAbsolutePath();
+                for (File obb : obbFiles) {
+                    FullBackup.backupToTar(
+                            packageInfo.packageName,
+                            FullBackup.OBB_TREE_TOKEN,
+                            /* linkDomain */ null,
+                            obbDirName,
+                            obb.getAbsolutePath(),
+                            mOutput);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java b/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
new file mode 100644
index 0000000..aca48b6
--- /dev/null
+++ b/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
@@ -0,0 +1,463 @@
+package com.android.server.backup.fullbackup;
+
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_VERSION;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.Application;
+import android.app.backup.BackupDataInput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser.SigningDetails;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+import com.android.server.testing.shadows.ShadowFullBackup;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+import org.robolectric.shadows.ShadowEnvironment;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileTime;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBackupDataInput.class,
+            ShadowBackupDataOutput.class,
+            ShadowEnvironment.class,
+            ShadowFullBackup.class,
+        })
+public class AppMetadataBackupWriterTest {
+    private static final String TEST_PACKAGE = "com.test.package";
+    private static final String TEST_PACKAGE_INSTALLER = "com.test.package.installer";
+    private static final Long TEST_PACKAGE_VERSION_CODE = 100L;
+
+    private @UserIdInt int mUserId;
+    private PackageManager mPackageManager;
+    private ShadowApplicationPackageManager mShadowPackageManager;
+    private File mFilesDir;
+    private File mBackupDataOutputFile;
+    private AppMetadataBackupWriter mBackupWriter;
+
+    @Before
+    public void setUp() throws Exception {
+        Application application = RuntimeEnvironment.application;
+
+        mUserId = UserHandle.USER_SYSTEM;
+        mPackageManager = application.getPackageManager();
+        mShadowPackageManager = (ShadowApplicationPackageManager) shadowOf(mPackageManager);
+
+        mFilesDir = RuntimeEnvironment.application.getFilesDir();
+        mBackupDataOutputFile = new File(mFilesDir, "output");
+        mBackupDataOutputFile.createNewFile();
+        ParcelFileDescriptor pfd =
+                ParcelFileDescriptor.open(
+                        mBackupDataOutputFile, ParcelFileDescriptor.MODE_READ_WRITE);
+        FullBackupDataOutput output =
+                new FullBackupDataOutput(pfd, /* quota */ -1, /* transportFlags */ 0);
+        mBackupWriter = new AppMetadataBackupWriter(output, mPackageManager);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mBackupDataOutputFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withoutApkOrSignatures_writesCorrectData() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[0]).isEqualTo(Integer.toString(BACKUP_MANIFEST_VERSION));
+        assertThat(manifest[1]).isEqualTo(TEST_PACKAGE);
+        assertThat(manifest[2]).isEqualTo(Long.toString(TEST_PACKAGE_VERSION_CODE));
+        assertThat(manifest[3]).isEqualTo(Integer.toString(Build.VERSION.SDK_INT));
+        assertThat(manifest[4]).isEqualTo(TEST_PACKAGE_INSTALLER);
+        assertThat(manifest[5]).isEqualTo("0"); // withApk
+        assertThat(manifest[6]).isEqualTo("0"); // signatures
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withApk_writesApk() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ true);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[5]).isEqualTo("1"); // withApk
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withSignatures_writesCorrectSignatures() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        packageInfo.signingInfo =
+                new SigningInfo(
+                        new SigningDetails(
+                                new Signature[] {new Signature("1234"), new Signature("5678")},
+                                SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                                null,
+                                null));
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(9);
+        assertThat(manifest[6]).isEqualTo("2"); // # of signatures
+        assertThat(manifest[7]).isEqualTo("1234"); // first signature
+        assertThat(manifest[8]).isEqualTo("5678"); // second signature
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withoutInstallerPackage_writesEmptyInstaller() throws Exception {
+        PackageInfo packageInfo = createPackageInfo(TEST_PACKAGE, null, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[4]).isEqualTo(""); // installer package name
+        manifestFile.delete();
+    }
+
+    @Test
+    public void testBackupManifest_whenRunPreviouslyWithSameData_producesSameBytesOnSecondRun()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+        byte[] firstRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        // Simulate modifying the manifest file to ensure that file metadata does not change the
+        // backup bytes produced.
+        modifyFileMetadata(manifestFile);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] secondRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        assertThat(firstRunBytes).isEqualTo(secondRunBytes);
+        manifestFile.delete();
+    }
+
+    /**
+     * The widget data format with metadata is:
+     *
+     * <pre>
+     *     BACKUP_METADATA_VERSION
+     *     package name
+     *     4 : Integer token identifying the widget data blob.
+     *     4 : Integer size of the widget data.
+     *     N : Raw bytes of the widget data.
+     * </pre>
+     */
+    @Test
+    public void testBackupWidget_writesCorrectData() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+        byte[] widgetBytes = "widget".getBytes();
+
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] widgetData = new String(writtenBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(widgetData.length).isEqualTo(3);
+        // Metadata header
+        assertThat(widgetData[0]).isEqualTo(Integer.toString(BACKUP_METADATA_VERSION));
+        assertThat(widgetData[1]).isEqualTo(packageInfo.packageName);
+        // Widget data
+        ByteArrayOutputStream expectedBytes = new ByteArrayOutputStream();
+        DataOutputStream stream = new DataOutputStream(expectedBytes);
+        stream.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
+        stream.writeInt(widgetBytes.length);
+        stream.write(widgetBytes);
+        stream.flush();
+        assertThat(widgetData[2]).isEqualTo(expectedBytes.toString());
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_withNullWidgetData_throwsNullPointerException() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        mBackupWriter.backupWidget(
+                                packageInfo, metadataFile, mFilesDir, /* widgetData */ null));
+
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_withEmptyWidgetData_throwsIllegalArgumentException()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+
+        expectThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mBackupWriter.backupWidget(
+                                packageInfo, metadataFile, mFilesDir, new byte[0]));
+
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_whenRunPreviouslyWithSameData_producesSameBytesOnSecondRun()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+        byte[] widgetBytes = "widget".getBytes();
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+        byte[] firstRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        // Simulate modifying the metadata file to ensure that file metadata does not change the
+        // backup bytes produced.
+        modifyFileMetadata(metadataFile);
+
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+
+        byte[] secondRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        assertThat(firstRunBytes).isEqualTo(secondRunBytes);
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupApk_writesCorrectBytesToOutput() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        byte[] apkBytes = "apk".getBytes();
+        File apkFile = createApkFileAndWrite(apkBytes);
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.sourceDir = apkFile.getPath();
+
+        mBackupWriter.backupApk(packageInfo);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        assertThat(writtenBytes).isEqualTo(apkBytes);
+        apkFile.delete();
+    }
+
+    @Test
+    public void testBackupObb_withObbData_writesCorrectBytesToOutput() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File obbDir = createObbDirForPackage(packageInfo.packageName);
+        byte[] obbBytes = "obb".getBytes();
+        File obbFile = createObbFileAndWrite(obbDir, obbBytes);
+
+        mBackupWriter.backupObb(mUserId, packageInfo);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        assertThat(writtenBytes).isEqualTo(obbBytes);
+        obbFile.delete();
+    }
+
+    @Test
+    public void testBackupObb_withNoObbData_doesNotWriteBytesToOutput() {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File obbDir = createObbDirForPackage(packageInfo.packageName);
+        // No obb file created.
+
+        mBackupWriter.backupObb(mUserId, packageInfo);
+
+        assertThat(mBackupDataOutputFile.length()).isEqualTo(0);
+    }
+
+    /**
+     * Creates a test package and registers it with the package manager. Also sets the installer
+     * package name if not {@code null}.
+     */
+    private PackageInfo createPackageInfo(
+            String packageName, @Nullable String installerPackageName, long versionCode) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.setLongVersionCode(versionCode);
+        mShadowPackageManager.addPackage(packageInfo);
+        if (installerPackageName != null) {
+            mPackageManager.setInstallerPackageName(packageName, installerPackageName);
+        }
+        return packageInfo;
+    }
+
+    /**
+     * Reads backup data written to the {@code file} by {@link ShadowBackupDataOutput}. Uses {@link
+     * ShadowBackupDataInput} to parse the data. Follows the format used by {@link
+     * ShadowFullBackup#backupToTar(String, String, String, String, String, FullBackupDataOutput)}.
+     *
+     * @param includeTarHeader If {@code true}, returns the TAR header and data bytes combined.
+     *     Otherwise, only returns the data bytes.
+     */
+    private byte[] getWrittenBytes(File file, boolean includeTarHeader) throws IOException {
+        BackupDataInput input = new BackupDataInput(new FileInputStream(file).getFD());
+        input.readNextHeader();
+        int dataSize = input.getDataSize();
+
+        byte[] bytes;
+        if (includeTarHeader) {
+            bytes = new byte[dataSize + 512];
+            input.readEntityData(bytes, 0, dataSize + 512);
+        } else {
+            input.readEntityData(new byte[512], 0, 512); // skip TAR header
+            bytes = new byte[dataSize];
+            input.readEntityData(bytes, 0, dataSize);
+        }
+
+        return bytes;
+    }
+
+    private File createFile(String fileName) throws IOException {
+        File file = new File(mFilesDir, fileName);
+        file.createNewFile();
+        return file;
+    }
+
+    /**
+     * Sets the last modified time of the {@code file} to the current time to edit the file's
+     * metadata.
+     */
+    private void modifyFileMetadata(File file) throws IOException {
+        Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(System.currentTimeMillis()));
+    }
+
+    private File createApkFileAndWrite(byte[] data) throws IOException {
+        File apkFile = new File(mFilesDir, "apk");
+        apkFile.createNewFile();
+        Files.write(apkFile.toPath(), data);
+        return apkFile;
+    }
+
+    /** Creates an .obb file in the input directory. */
+    private File createObbFileAndWrite(File obbDir, byte[] data) throws IOException {
+        File obbFile = new File(obbDir, "obb");
+        obbFile.createNewFile();
+        Files.write(obbFile.toPath(), data);
+        return obbFile;
+    }
+
+    /**
+     * Creates a package specific obb data directory since the backup method checks for obb data
+     * there. See {@link Environment#buildExternalStorageAppObbDirs(String)}.
+     */
+    private File createObbDirForPackage(String packageName) {
+        ShadowEnvironment.addExternalDir("test");
+        Environment.UserEnvironment userEnv =
+                new Environment.UserEnvironment(UserHandle.USER_SYSTEM);
+        File obbDir =
+                new File(
+                        userEnv.getExternalDirs()[0],
+                        Environment.DIR_ANDROID + "/obb/" + packageName);
+        obbDir.mkdirs();
+        return obbDir;
+    }
+}
diff --git a/com/android/server/backup/fullbackup/FullBackupEngine.java b/com/android/server/backup/fullbackup/FullBackupEngine.java
new file mode 100644
index 0000000..846c6a2
--- /dev/null
+++ b/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -0,0 +1,315 @@
+/*
+ * 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.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
+import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+
+import android.annotation.UserIdInt;
+import android.app.ApplicationThreadConstants;
+import android.app.IBackupAgent;
+import android.app.backup.BackupTransport;
+import android.app.backup.FullBackupDataOutput;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.server.AppWidgetBackupBridge;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.remote.RemoteCall;
+import com.android.server.backup.utils.FullBackupUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Objects;
+
+/**
+ * Core logic for performing one package's full backup, gathering the tarball from the application
+ * and emitting it to the designated OutputStream.
+ */
+public class FullBackupEngine {
+    private UserBackupManagerService backupManagerService;
+    private OutputStream mOutput;
+    private FullBackupPreflight mPreflightHook;
+    private BackupRestoreTask mTimeoutMonitor;
+    private IBackupAgent mAgent;
+    private boolean mIncludeApks;
+    private PackageInfo mPkg;
+    private final long mQuota;
+    private final int mOpToken;
+    private final int mTransportFlags;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    class FullBackupRunner implements Runnable {
+        private final @UserIdInt int mUserId;
+        private final PackageManager mPackageManager;
+        private final PackageInfo mPackage;
+        private final IBackupAgent mAgent;
+        private final ParcelFileDescriptor mPipe;
+        private final int mToken;
+        private final boolean mIncludeApks;
+        private final File mFilesDir;
+
+        FullBackupRunner(
+                UserBackupManagerService userBackupManagerService,
+                PackageInfo packageInfo,
+                IBackupAgent agent,
+                ParcelFileDescriptor pipe,
+                int token,
+                boolean includeApks)
+                throws IOException {
+            mUserId = userBackupManagerService.getUserId();
+            mPackageManager = backupManagerService.getPackageManager();
+            mPackage = packageInfo;
+            mAgent = agent;
+            mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor());
+            mToken = token;
+            mIncludeApks = includeApks;
+            mFilesDir = userBackupManagerService.getDataDir();
+        }
+
+        @Override
+        public void run() {
+            try {
+                FullBackupDataOutput output =
+                        new FullBackupDataOutput(mPipe, /* quota */ -1, mTransportFlags);
+                AppMetadataBackupWriter appMetadataBackupWriter =
+                        new AppMetadataBackupWriter(output, mPackageManager);
+
+                String packageName = mPackage.packageName;
+                boolean isSharedStorage = SHARED_BACKUP_AGENT_PACKAGE.equals(packageName);
+                boolean writeApk =
+                        shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage);
+
+                if (!isSharedStorage) {
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Writing manifest for " + packageName);
+                    }
+
+                    File manifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
+                    appMetadataBackupWriter.backupManifest(
+                            mPackage, manifestFile, mFilesDir, writeApk);
+                    manifestFile.delete();
+
+                    // Write widget data.
+                    byte[] widgetData =
+                            AppWidgetBackupBridge.getWidgetState(packageName, mUserId);
+                    if (widgetData != null && widgetData.length > 0) {
+                        File metadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
+                        appMetadataBackupWriter.backupWidget(
+                                mPackage, metadataFile, mFilesDir, widgetData);
+                        metadataFile.delete();
+                    }
+                }
+
+                // TODO(b/113807190): Look into removing, only used for 'adb backup'.
+                if (writeApk) {
+                    appMetadataBackupWriter.backupApk(mPackage);
+                    appMetadataBackupWriter.backupObb(mUserId, mPackage);
+                }
+
+                if (DEBUG) {
+                    Slog.d(TAG, "Calling doFullBackup() on " + packageName);
+                }
+
+                long timeout =
+                        isSharedStorage
+                                ? mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis()
+                                : mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+                backupManagerService.prepareOperationTimeout(
+                        mToken,
+                        timeout,
+                        mTimeoutMonitor /* in parent class */,
+                        OP_TYPE_BACKUP_WAIT);
+                mAgent.doFullBackup(
+                        mPipe,
+                        mQuota,
+                        mToken,
+                        backupManagerService.getBackupManagerBinder(),
+                        mTransportFlags);
+            } catch (IOException e) {
+                Slog.e(TAG, "Error running full backup for " + mPackage.packageName, e);
+            } catch (RemoteException e) {
+                Slog.e(
+                        TAG,
+                        "Remote agent vanished during full backup of " + mPackage.packageName,
+                        e);
+            } finally {
+                try {
+                    mPipe.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+
+        /**
+         * Don't write apks for system-bundled apps that are not upgraded.
+         */
+        private boolean shouldWriteApk(
+                ApplicationInfo applicationInfo, boolean includeApks, boolean isSharedStorage) {
+            boolean isSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+            boolean isUpdatedSystemApp =
+                    (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+            return includeApks
+                    && !isSharedStorage
+                    && (!isSystemApp || isUpdatedSystemApp);
+        }
+    }
+
+    public FullBackupEngine(
+            UserBackupManagerService backupManagerService,
+            OutputStream output,
+            FullBackupPreflight preflightHook,
+            PackageInfo pkg,
+            boolean alsoApks,
+            BackupRestoreTask timeoutMonitor,
+            long quota,
+            int opToken,
+            int transportFlags) {
+        this.backupManagerService = backupManagerService;
+        mOutput = output;
+        mPreflightHook = preflightHook;
+        mPkg = pkg;
+        mIncludeApks = alsoApks;
+        mTimeoutMonitor = timeoutMonitor;
+        mQuota = quota;
+        mOpToken = opToken;
+        mTransportFlags = transportFlags;
+        mAgentTimeoutParameters =
+                Objects.requireNonNull(
+                        backupManagerService.getAgentTimeoutParameters(),
+                        "Timeout parameters cannot be null");
+    }
+
+    public int preflightCheck() throws RemoteException {
+        if (mPreflightHook == null) {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "No preflight check");
+            }
+            return BackupTransport.TRANSPORT_OK;
+        }
+        if (initializeAgent()) {
+            int result = mPreflightHook.preflightFullBackup(mPkg, mAgent);
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "preflight returned " + result);
+            }
+            return result;
+        } else {
+            Slog.w(TAG, "Unable to bind to full agent for " + mPkg.packageName);
+            return BackupTransport.AGENT_ERROR;
+        }
+    }
+
+    public int backupOnePackage() throws RemoteException {
+        int result = BackupTransport.AGENT_ERROR;
+
+        if (initializeAgent()) {
+            ParcelFileDescriptor[] pipes = null;
+            try {
+                pipes = ParcelFileDescriptor.createPipe();
+
+                FullBackupRunner runner =
+                        new FullBackupRunner(
+                                backupManagerService,
+                                mPkg,
+                                mAgent,
+                                pipes[1],
+                                mOpToken,
+                                mIncludeApks);
+                pipes[1].close(); // the runner has dup'd it
+                pipes[1] = null;
+                Thread t = new Thread(runner, "app-data-runner");
+                t.start();
+
+                FullBackupUtils.routeSocketDataToOutput(pipes[0], mOutput);
+
+                if (!backupManagerService.waitUntilOperationComplete(mOpToken)) {
+                    Slog.e(TAG, "Full backup failed on package " + mPkg.packageName);
+                } else {
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Full package backup success: " + mPkg.packageName);
+                    }
+                    result = BackupTransport.TRANSPORT_OK;
+                }
+            } catch (IOException e) {
+                Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage());
+                result = BackupTransport.AGENT_ERROR;
+            } finally {
+                try {
+                    // flush after every package
+                    mOutput.flush();
+                    if (pipes != null) {
+                        if (pipes[0] != null) {
+                            pipes[0].close();
+                        }
+                        if (pipes[1] != null) {
+                            pipes[1].close();
+                        }
+                    }
+                } catch (IOException e) {
+                    Slog.w(TAG, "Error bringing down backup stack");
+                    result = BackupTransport.TRANSPORT_ERROR;
+                }
+            }
+        } else {
+            Slog.w(TAG, "Unable to bind to full agent for " + mPkg.packageName);
+        }
+        tearDown();
+        return result;
+    }
+
+    public void sendQuotaExceeded(long backupDataBytes, long quotaBytes) {
+        if (initializeAgent()) {
+            try {
+                RemoteCall.execute(
+                        callback -> mAgent.doQuotaExceeded(backupDataBytes, quotaBytes, callback),
+                        mAgentTimeoutParameters.getQuotaExceededTimeoutMillis());
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception while telling agent about quota exceeded");
+            }
+        }
+    }
+
+    private boolean initializeAgent() {
+        if (mAgent == null) {
+            if (MORE_DEBUG) {
+                Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName);
+            }
+            mAgent =
+                    backupManagerService.bindToAgentSynchronous(
+                            mPkg.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_FULL);
+        }
+        return mAgent != null;
+    }
+
+    private void tearDown() {
+        if (mPkg != null) {
+            backupManagerService.tearDownAgentAndKill(mPkg.applicationInfo);
+        }
+    }
+}
diff --git a/com/android/server/backup/fullbackup/FullBackupEntry.java b/com/android/server/backup/fullbackup/FullBackupEntry.java
new file mode 100644
index 0000000..a62f1c0
--- /dev/null
+++ b/com/android/server/backup/fullbackup/FullBackupEntry.java
@@ -0,0 +1,39 @@
+/*
+ * 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.fullbackup;
+
+public class FullBackupEntry implements Comparable<FullBackupEntry> {
+
+    public String packageName;
+    public long lastBackup;
+
+    public FullBackupEntry(String pkg, long when) {
+        packageName = pkg;
+        lastBackup = when;
+    }
+
+    @Override
+    public int compareTo(FullBackupEntry other) {
+        if (lastBackup < other.lastBackup) {
+            return -1;
+        } else if (lastBackup > other.lastBackup) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/com/android/server/backup/fullbackup/FullBackupObbConnection.java b/com/android/server/backup/fullbackup/FullBackupObbConnection.java
new file mode 100644
index 0000000..aaf1f0a
--- /dev/null
+++ b/com/android/server/backup/fullbackup/FullBackupObbConnection.java
@@ -0,0 +1,162 @@
+/*
+ * 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.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
+
+import android.app.backup.IBackupManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.backup.IObbBackupService;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.utils.FullBackupUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Objects;
+
+/**
+ * Full backup/restore to a file/socket.
+ */
+public class FullBackupObbConnection implements ServiceConnection {
+
+    private UserBackupManagerService backupManagerService;
+    volatile IObbBackupService mService;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    public FullBackupObbConnection(UserBackupManagerService backupManagerService) {
+        this.backupManagerService = backupManagerService;
+        mService = null;
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+    }
+
+    public void establish() {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "Initiating bind of OBB service on " + this);
+        }
+        Intent obbIntent = new Intent().setComponent(new ComponentName(
+                "com.android.sharedstoragebackup",
+                "com.android.sharedstoragebackup.ObbBackupService"));
+        backupManagerService.getContext().bindServiceAsUser(
+                obbIntent, this, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+    }
+
+    public void tearDown() {
+        backupManagerService.getContext().unbindService(this);
+    }
+
+    public boolean backupObbs(PackageInfo pkg, OutputStream out) {
+        boolean success = false;
+        waitForConnection();
+
+        ParcelFileDescriptor[] pipes = null;
+        try {
+            pipes = ParcelFileDescriptor.createPipe();
+            int token = backupManagerService.generateRandomIntegerToken();
+            long fullBackupAgentTimeoutMillis =
+                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+            backupManagerService.prepareOperationTimeout(
+                    token, fullBackupAgentTimeoutMillis, null, OP_TYPE_BACKUP_WAIT);
+            mService.backupObbs(pkg.packageName, pipes[1], token,
+                    backupManagerService.getBackupManagerBinder());
+            FullBackupUtils.routeSocketDataToOutput(pipes[0], out);
+            success = backupManagerService.waitUntilOperationComplete(token);
+        } catch (Exception e) {
+            Slog.w(TAG, "Unable to back up OBBs for " + pkg, e);
+        } finally {
+            try {
+                out.flush();
+                if (pipes != null) {
+                    if (pipes[0] != null) {
+                        pipes[0].close();
+                    }
+                    if (pipes[1] != null) {
+                        pipes[1].close();
+                    }
+                }
+            } catch (IOException e) {
+                Slog.w(TAG, "I/O error closing down OBB backup", e);
+            }
+        }
+        return success;
+    }
+
+    public void restoreObbFile(String pkgName, ParcelFileDescriptor data,
+            long fileSize, int type, String path, long mode, long mtime,
+            int token, IBackupManager callbackBinder) {
+        waitForConnection();
+
+        try {
+            mService.restoreObbFile(pkgName, data, fileSize, type, path, mode, mtime,
+                    token, callbackBinder);
+        } catch (Exception e) {
+            Slog.w(TAG, "Unable to restore OBBs for " + pkgName, e);
+        }
+    }
+
+    private void waitForConnection() {
+        synchronized (this) {
+            while (mService == null) {
+                if (MORE_DEBUG) {
+                    Slog.i(TAG, "...waiting for OBB service binding...");
+                }
+                try {
+                    this.wait();
+                } catch (InterruptedException e) { /* never interrupted */ }
+            }
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Connected to OBB service; continuing");
+            }
+        }
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        synchronized (this) {
+            mService = IObbBackupService.Stub.asInterface(service);
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "OBB service connection " + mService + " connected on " + this);
+            }
+            this.notifyAll();
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        synchronized (this) {
+            mService = null;
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "OBB service connection disconnected on " + this);
+            }
+            this.notifyAll();
+        }
+    }
+
+}
diff --git a/com/android/server/backup/fullbackup/FullBackupPreflight.java b/com/android/server/backup/fullbackup/FullBackupPreflight.java
new file mode 100644
index 0000000..f12587f
--- /dev/null
+++ b/com/android/server/backup/fullbackup/FullBackupPreflight.java
@@ -0,0 +1,39 @@
+/*
+ * 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.fullbackup;
+
+import android.app.IBackupAgent;
+import android.content.pm.PackageInfo;
+
+/**
+ * Callout from the engine to an interested participant that might need to communicate with the
+ * agent prior to asking it to move data.
+ */
+public interface FullBackupPreflight {
+
+    /**
+     * Perform the preflight operation necessary for the given package.
+     *
+     * @param pkg The name of the package being proposed for full-data backup
+     * @param agent Live BackupAgent binding to the target app's agent
+     * @return BackupTransport.TRANSPORT_OK to proceed with the backup operation, or one of the
+     * other BackupTransport.* error codes as appropriate
+     */
+    int preflightFullBackup(PackageInfo pkg, IBackupAgent agent);
+
+    long getExpectedSizeOrErrorCode();
+}
diff --git a/com/android/server/backup/fullbackup/FullBackupTask.java b/com/android/server/backup/fullbackup/FullBackupTask.java
new file mode 100644
index 0000000..8f6923b
--- /dev/null
+++ b/com/android/server/backup/fullbackup/FullBackupTask.java
@@ -0,0 +1,70 @@
+/*
+ * 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.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.app.backup.IFullBackupRestoreObserver;
+import android.os.RemoteException;
+import android.util.Slog;
+
+/**
+ * Generic driver skeleton for full backup operations.
+ */
+public abstract class FullBackupTask implements Runnable {
+
+    IFullBackupRestoreObserver mObserver;
+
+    FullBackupTask(IFullBackupRestoreObserver observer) {
+        mObserver = observer;
+    }
+
+    // wrappers for observer use
+    final void sendStartBackup() {
+        if (mObserver != null) {
+            try {
+                mObserver.onStartBackup();
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full backup observer went away: startBackup");
+                mObserver = null;
+            }
+        }
+    }
+
+    final void sendOnBackupPackage(String name) {
+        if (mObserver != null) {
+            try {
+                // TODO: use a more user-friendly name string
+                mObserver.onBackupPackage(name);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full backup observer went away: backupPackage");
+                mObserver = null;
+            }
+        }
+    }
+
+    final void sendEndBackup() {
+        if (mObserver != null) {
+            try {
+                mObserver.onEndBackup();
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full backup observer went away: endBackup");
+                mObserver = null;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
new file mode 100644
index 0000000..0a7159b
--- /dev/null
+++ b/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -0,0 +1,507 @@
+/*
+ * 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.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_VERSION;
+import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+
+import android.app.backup.IFullBackupRestoreObserver;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.server.AppWidgetBackupBridge;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.KeyValueAdbBackupEngine;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.utils.AppBackupUtils;
+import com.android.server.backup.utils.PasswordUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Full backup task variant used for adb backup.
+ */
+public class PerformAdbBackupTask extends FullBackupTask implements BackupRestoreTask {
+
+    private final UserBackupManagerService mUserBackupManagerService;
+    private final AtomicBoolean mLatch;
+
+    private final ParcelFileDescriptor mOutputFile;
+    private final boolean mIncludeApks;
+    private final boolean mIncludeObbs;
+    private final boolean mIncludeShared;
+    private final boolean mDoWidgets;
+    private final boolean mAllApps;
+    private final boolean mIncludeSystem;
+    private final boolean mCompress;
+    private final boolean mKeyValue;
+    private final ArrayList<String> mPackages;
+    private PackageInfo mCurrentTarget;
+    private final String mCurrentPassword;
+    private final String mEncryptPassword;
+    private final int mCurrentOpToken;
+
+    public PerformAdbBackupTask(UserBackupManagerService backupManagerService,
+            ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
+            boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets,
+            String curPassword, String encryptPassword, boolean doAllApps, boolean doSystem,
+            boolean doCompress, boolean doKeyValue, String[] packages, AtomicBoolean latch) {
+        super(observer);
+        mUserBackupManagerService = backupManagerService;
+        mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
+        mLatch = latch;
+
+        mOutputFile = fd;
+        mIncludeApks = includeApks;
+        mIncludeObbs = includeObbs;
+        mIncludeShared = includeShared;
+        mDoWidgets = doWidgets;
+        mAllApps = doAllApps;
+        mIncludeSystem = doSystem;
+        mPackages = (packages == null)
+                ? new ArrayList<>()
+                : new ArrayList<>(Arrays.asList(packages));
+        mCurrentPassword = curPassword;
+        // when backing up, if there is a current backup password, we require that
+        // the user use a nonempty encryption password as well.  if one is supplied
+        // in the UI we use that, but if the UI was left empty we fall back to the
+        // current backup password (which was supplied by the user as well).
+        if (encryptPassword == null || "".equals(encryptPassword)) {
+            mEncryptPassword = curPassword;
+        } else {
+            mEncryptPassword = encryptPassword;
+        }
+        if (MORE_DEBUG) {
+            Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword);
+        }
+        mCompress = doCompress;
+        mKeyValue = doKeyValue;
+    }
+
+    private void addPackagesToSet(TreeMap<String, PackageInfo> set, List<String> pkgNames) {
+        for (String pkgName : pkgNames) {
+            if (!set.containsKey(pkgName)) {
+                try {
+                    PackageInfo info = mUserBackupManagerService.getPackageManager().getPackageInfo(
+                            pkgName,
+                            PackageManager.GET_SIGNING_CERTIFICATES);
+                    set.put(pkgName, info);
+                } catch (NameNotFoundException e) {
+                    Slog.w(TAG, "Unknown package " + pkgName + ", skipping");
+                }
+            }
+        }
+    }
+
+    private OutputStream emitAesBackupHeader(StringBuilder headerbuf,
+            OutputStream ofstream) throws Exception {
+        // User key will be used to encrypt the master key.
+        byte[] newUserSalt = mUserBackupManagerService
+                .randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
+        SecretKey userKey = PasswordUtils
+                .buildPasswordKey(PBKDF_CURRENT, mEncryptPassword,
+                        newUserSalt,
+                        PasswordUtils.PBKDF2_HASH_ROUNDS);
+
+        // the master key is random for each backup
+        byte[] masterPw = new byte[256 / 8];
+        mUserBackupManagerService.getRng().nextBytes(masterPw);
+        byte[] checksumSalt = mUserBackupManagerService
+                .randomBytes(PasswordUtils.PBKDF2_SALT_SIZE);
+
+        // primary encryption of the datastream with the random key
+        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES");
+        c.init(Cipher.ENCRYPT_MODE, masterKeySpec);
+        OutputStream finalOutput = new CipherOutputStream(ofstream, c);
+
+        // line 4: name of encryption algorithm
+        headerbuf.append(PasswordUtils.ENCRYPTION_ALGORITHM_NAME);
+        headerbuf.append('\n');
+        // line 5: user password salt [hex]
+        headerbuf.append(PasswordUtils.byteArrayToHex(newUserSalt));
+        headerbuf.append('\n');
+        // line 6: master key checksum salt [hex]
+        headerbuf.append(PasswordUtils.byteArrayToHex(checksumSalt));
+        headerbuf.append('\n');
+        // line 7: number of PBKDF2 rounds used [decimal]
+        headerbuf.append(PasswordUtils.PBKDF2_HASH_ROUNDS);
+        headerbuf.append('\n');
+
+        // line 8: IV of the user key [hex]
+        Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        mkC.init(Cipher.ENCRYPT_MODE, userKey);
+
+        byte[] IV = mkC.getIV();
+        headerbuf.append(PasswordUtils.byteArrayToHex(IV));
+        headerbuf.append('\n');
+
+        // line 9: master IV + key blob, encrypted by the user key [hex].  Blob format:
+        //    [byte] IV length = Niv
+        //    [array of Niv bytes] IV itself
+        //    [byte] master key length = Nmk
+        //    [array of Nmk bytes] master key itself
+        //    [byte] MK checksum hash length = Nck
+        //    [array of Nck bytes] master key checksum hash
+        //
+        // The checksum is the (master key + checksum salt), run through the
+        // stated number of PBKDF2 rounds
+        IV = c.getIV();
+        byte[] mk = masterKeySpec.getEncoded();
+        byte[] checksum = PasswordUtils
+                .makeKeyChecksum(PBKDF_CURRENT,
+                        masterKeySpec.getEncoded(),
+                        checksumSalt, PasswordUtils.PBKDF2_HASH_ROUNDS);
+
+        ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
+                + checksum.length + 3);
+        DataOutputStream mkOut = new DataOutputStream(blob);
+        mkOut.writeByte(IV.length);
+        mkOut.write(IV);
+        mkOut.writeByte(mk.length);
+        mkOut.write(mk);
+        mkOut.writeByte(checksum.length);
+        mkOut.write(checksum);
+        mkOut.flush();
+        byte[] encryptedMk = mkC.doFinal(blob.toByteArray());
+        headerbuf.append(PasswordUtils.byteArrayToHex(encryptedMk));
+        headerbuf.append('\n');
+
+        return finalOutput;
+    }
+
+    private void finalizeBackup(OutputStream out) {
+        try {
+            // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes.
+            byte[] eof = new byte[512 * 2]; // newly allocated == zero filled
+            out.write(eof);
+        } catch (IOException e) {
+            Slog.w(TAG, "Error attempting to finalize backup stream");
+        }
+    }
+
+    @Override
+    public void run() {
+        String includeKeyValue = mKeyValue ? ", including key-value backups" : "";
+        Slog.i(TAG, "--- Performing adb backup" + includeKeyValue + " ---");
+
+        TreeMap<String, PackageInfo> packagesToBackup = new TreeMap<>();
+        FullBackupObbConnection obbConnection = new FullBackupObbConnection(
+                mUserBackupManagerService);
+        obbConnection.establish();  // we'll want this later
+
+        sendStartBackup();
+        PackageManager pm = mUserBackupManagerService.getPackageManager();
+
+        // doAllApps supersedes the package set if any
+        if (mAllApps) {
+            List<PackageInfo> allPackages = pm.getInstalledPackages(
+                    PackageManager.GET_SIGNING_CERTIFICATES);
+            for (int i = 0; i < allPackages.size(); i++) {
+                PackageInfo pkg = allPackages.get(i);
+                // Exclude system apps if we've been asked to do so
+                if (mIncludeSystem
+                        || ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0)) {
+                    packagesToBackup.put(pkg.packageName, pkg);
+                }
+            }
+        }
+
+        // If we're doing widget state as well, ensure that we have all the involved
+        // host & provider packages in the set
+        if (mDoWidgets) {
+            // TODO: http://b/22388012
+            List<String> pkgs =
+                    AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM);
+            if (pkgs != null) {
+                if (MORE_DEBUG) {
+                    Slog.i(TAG, "Adding widget participants to backup set:");
+                    StringBuilder sb = new StringBuilder(128);
+                    sb.append("   ");
+                    for (String s : pkgs) {
+                        sb.append(' ');
+                        sb.append(s);
+                    }
+                    Slog.i(TAG, sb.toString());
+                }
+                addPackagesToSet(packagesToBackup, pkgs);
+            }
+        }
+
+        // Now process the command line argument packages, if any. Note that explicitly-
+        // named system-partition packages will be included even if includeSystem was
+        // set to false.
+        if (mPackages != null) {
+            addPackagesToSet(packagesToBackup, mPackages);
+        }
+
+        // Now we cull any inapplicable / inappropriate packages from the set.  This
+        // includes the special shared-storage agent package; we handle that one
+        // explicitly at the end of the backup pass. Packages supporting key-value backup are
+        // added to their own queue, and handled after packages supporting fullbackup.
+        ArrayList<PackageInfo> keyValueBackupQueue = new ArrayList<>();
+        Iterator<Entry<String, PackageInfo>> iter = packagesToBackup.entrySet().iterator();
+        while (iter.hasNext()) {
+            PackageInfo pkg = iter.next().getValue();
+            if (!AppBackupUtils.appIsEligibleForBackup(pkg.applicationInfo,
+                    mUserBackupManagerService.getUserId())
+                    || AppBackupUtils.appIsStopped(pkg.applicationInfo)) {
+                iter.remove();
+                if (DEBUG) {
+                    Slog.i(TAG, "Package " + pkg.packageName
+                            + " is not eligible for backup, removing.");
+                }
+            } else if (AppBackupUtils.appIsKeyValueOnly(pkg)) {
+                iter.remove();
+                if (DEBUG) {
+                    Slog.i(TAG, "Package " + pkg.packageName
+                            + " is key-value.");
+                }
+                keyValueBackupQueue.add(pkg);
+            }
+        }
+
+        // flatten the set of packages now so we can explicitly control the ordering
+        ArrayList<PackageInfo> backupQueue =
+                new ArrayList<>(packagesToBackup.values());
+        FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());
+        OutputStream out = null;
+
+        PackageInfo pkg = null;
+        try {
+            boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0);
+
+            // Only allow encrypted backups of encrypted devices
+            if (mUserBackupManagerService.deviceIsEncrypted() && !encrypting) {
+                Slog.e(TAG, "Unencrypted backup of encrypted device; aborting");
+                return;
+            }
+
+            OutputStream finalOutput = ofstream;
+
+            // Verify that the given password matches the currently-active
+            // backup password, if any
+            if (!mUserBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Backup password mismatch; aborting");
+                }
+                return;
+            }
+
+            // Write the global file header.  All strings are UTF-8 encoded; lines end
+            // with a '\n' byte.  Actual backup data begins immediately following the
+            // final '\n'.
+            //
+            // line 1: "ANDROID BACKUP"
+            // line 2: backup file format version, currently "5"
+            // line 3: compressed?  "0" if not compressed, "1" if compressed.
+            // line 4: name of encryption algorithm [currently only "none" or "AES-256"]
+            //
+            // When line 4 is not "none", then additional header data follows:
+            //
+            // line 5: user password salt [hex]
+            // line 6: master key checksum salt [hex]
+            // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal]
+            // line 8: IV of the user key [hex]
+            // line 9: master key blob [hex]
+            //     IV of the master key, master key itself, master key checksum hash
+            //
+            // The master key checksum is the master key plus its checksum salt, run through
+            // 10k rounds of PBKDF2.  This is used to verify that the user has supplied the
+            // correct password for decrypting the archive:  the master key decrypted from
+            // the archive using the user-supplied password is also run through PBKDF2 in
+            // this way, and if the result does not match the checksum as stored in the
+            // archive, then we know that the user-supplied password does not match the
+            // archive's.
+            StringBuilder headerbuf = new StringBuilder(1024);
+
+            headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
+            headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n
+            headerbuf.append(mCompress ? "\n1\n" : "\n0\n");
+
+            try {
+                // Set up the encryption stage if appropriate, and emit the correct header
+                if (encrypting) {
+                    finalOutput = emitAesBackupHeader(headerbuf, finalOutput);
+                } else {
+                    headerbuf.append("none\n");
+                }
+
+                byte[] header = headerbuf.toString().getBytes("UTF-8");
+                ofstream.write(header);
+
+                // Set up the compression stage feeding into the encryption stage (if any)
+                if (mCompress) {
+                    Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
+                    finalOutput = new DeflaterOutputStream(finalOutput, deflater, true);
+                }
+
+                out = finalOutput;
+            } catch (Exception e) {
+                // Should never happen!
+                Slog.e(TAG, "Unable to emit archive header", e);
+                return;
+            }
+
+            // Shared storage if requested
+            if (mIncludeShared) {
+                try {
+                    pkg = mUserBackupManagerService.getPackageManager().getPackageInfo(
+                            SHARED_BACKUP_AGENT_PACKAGE, 0);
+                    backupQueue.add(pkg);
+                } catch (NameNotFoundException e) {
+                    Slog.e(TAG, "Unable to find shared-storage backup handler");
+                }
+            }
+
+            // Now actually run the constructed backup sequence for full backup
+            int N = backupQueue.size();
+            for (int i = 0; i < N; i++) {
+                pkg = backupQueue.get(i);
+                if (DEBUG) {
+                    Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName
+                            + " ---");
+                }
+                final boolean isSharedStorage =
+                        pkg.packageName.equals(
+                                SHARED_BACKUP_AGENT_PACKAGE);
+
+                FullBackupEngine mBackupEngine =
+                        new FullBackupEngine(
+                                mUserBackupManagerService,
+                                out,
+                                null,
+                                pkg,
+                                mIncludeApks,
+                                this,
+                                Long.MAX_VALUE,
+                                mCurrentOpToken,
+                                /*transportFlags=*/ 0);
+                sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
+
+                // Don't need to check preflight result as there is no preflight hook.
+                mCurrentTarget = pkg;
+                mBackupEngine.backupOnePackage();
+
+                // after the app's agent runs to handle its private filesystem
+                // contents, back up any OBB content it has on its behalf.
+                if (mIncludeObbs && !isSharedStorage) {
+                    boolean obbOkay = obbConnection.backupObbs(pkg, out);
+                    if (!obbOkay) {
+                        throw new RuntimeException("Failure writing OBB stack for " + pkg);
+                    }
+                }
+            }
+            // And for key-value backup if enabled
+            if (mKeyValue) {
+                for (PackageInfo keyValuePackage : keyValueBackupQueue) {
+                    if (DEBUG) {
+                        Slog.i(TAG, "--- Performing key-value backup for package "
+                                + keyValuePackage.packageName + " ---");
+                    }
+                    KeyValueAdbBackupEngine kvBackupEngine =
+                            new KeyValueAdbBackupEngine(out, keyValuePackage,
+                                    mUserBackupManagerService,
+                                    mUserBackupManagerService.getPackageManager(),
+                                    mUserBackupManagerService.getBaseStateDir(),
+                                    mUserBackupManagerService.getDataDir());
+                    sendOnBackupPackage(keyValuePackage.packageName);
+                    kvBackupEngine.backupOnePackage();
+                }
+            }
+
+            // Done!
+            finalizeBackup(out);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "App died during full backup");
+        } catch (Exception e) {
+            Slog.e(TAG, "Internal exception during full backup", e);
+        } finally {
+            try {
+                if (out != null) {
+                    out.flush();
+                    out.close();
+                }
+                mOutputFile.close();
+            } catch (IOException e) {
+                Slog.e(TAG, "IO error closing adb backup file: " + e.getMessage());
+            }
+            synchronized (mLatch) {
+                mLatch.set(true);
+                mLatch.notifyAll();
+            }
+            sendEndBackup();
+            obbConnection.tearDown();
+            if (DEBUG) {
+                Slog.d(TAG, "Full backup pass complete.");
+            }
+            mUserBackupManagerService.getWakelock().release();
+        }
+    }
+
+    // BackupRestoreTask methods, used for timeout handling
+    @Override
+    public void execute() {
+        // Unused
+    }
+
+    @Override
+    public void operationComplete(long result) {
+        // Unused
+    }
+
+    @Override
+    public void handleCancel(boolean cancelAll) {
+        final PackageInfo target = mCurrentTarget;
+        if (DEBUG) {
+            Slog.w(TAG, "adb backup cancel of " + target);
+        }
+        if (target != null) {
+            mUserBackupManagerService.tearDownAgentAndKill(mCurrentTarget.applicationInfo);
+        }
+        mUserBackupManagerService.removeOperation(mCurrentOpToken);
+    }
+}
diff --git a/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
new file mode 100644
index 0000000..738dd9b
--- /dev/null
+++ b/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -0,0 +1,955 @@
+/*
+ * 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.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
+
+import android.annotation.Nullable;
+import android.app.IBackupAgent;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupProgress;
+import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.app.backup.IFullBackupRestoreObserver;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.EventLogTags;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.FullBackupJob;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+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.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.backup.utils.AppBackupUtils;
+import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupObserverUtils;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Full backup task extension used for transport-oriented operation.
+ *
+ * Flow:
+ * For each requested package:
+ *     - Spin off a new SinglePackageBackupRunner (mBackupRunner) for the current package.
+ *     - Wait until preflight is complete. (mBackupRunner.getPreflightResultBlocking())
+ *     - If preflight data size is within limit, start reading data from agent pipe and writing
+ *       to transport pipe. While there is data to send, call transport.sendBackupData(int) to
+ *       tell the transport how many bytes to expect on its pipe.
+ *     - After sending all data, call transport.finishBackup() if things went well. And
+ *       transport.cancelFullBackup() otherwise.
+ *
+ * Interactions with mCurrentOperations:
+ *     - An entry for this object is added to mCurrentOperations for the entire lifetime of this
+ *       object. Used to cancel the operation.
+ *     - SinglePackageBackupRunner and SinglePackageBackupPreflight will put ephemeral entries
+ *       to get timeouts or operation complete callbacks.
+ *
+ * Handling cancels:
+ *     - The contract we provide is that the task won't interact with the transport after
+ *       handleCancel() is done executing.
+ *     - This task blocks at 3 points: 1. Preflight result check 2. Reading on agent side pipe
+ *       and 3. Get backup result from mBackupRunner.
+ *     - Bubbling up handleCancel to mBackupRunner handles all 3: 1. Calls handleCancel on the
+ *       preflight operation which counts down on the preflight latch. 2. Tears down the agent,
+ *       so read() returns -1. 3. Notifies mCurrentOpLock which unblocks
+ *       mBackupRunner.getBackupResultBlocking().
+ */
+public class PerformFullTransportBackupTask extends FullBackupTask implements BackupRestoreTask {
+    public static PerformFullTransportBackupTask newWithCurrentTransport(
+            UserBackupManagerService backupManagerService,
+            IFullBackupRestoreObserver observer,
+            String[] whichPackages,
+            boolean updateSchedule,
+            FullBackupJob runningJob,
+            CountDownLatch latch,
+            IBackupObserver backupObserver,
+            IBackupManagerMonitor monitor,
+            boolean userInitiated,
+            String caller) {
+        TransportManager transportManager = backupManagerService.getTransportManager();
+        TransportClient transportClient = transportManager.getCurrentTransportClient(caller);
+        OnTaskFinishedListener listener =
+                listenerCaller ->
+                        transportManager.disposeOfTransportClient(transportClient, listenerCaller);
+        return new PerformFullTransportBackupTask(
+                backupManagerService,
+                transportClient,
+                observer,
+                whichPackages,
+                updateSchedule,
+                runningJob,
+                latch,
+                backupObserver,
+                monitor,
+                listener,
+                userInitiated);
+    }
+
+    private static final String TAG = "PFTBT";
+
+    private UserBackupManagerService mUserBackupManagerService;
+    private final Object mCancelLock = new Object();
+
+    List<PackageInfo> mPackages;
+    PackageInfo mCurrentPackage;
+    boolean mUpdateSchedule;
+    CountDownLatch mLatch;
+    FullBackupJob mJob;             // if a scheduled job needs to be finished afterwards
+    IBackupObserver mBackupObserver;
+    @Nullable private IBackupManagerMonitor mMonitor;
+    boolean mUserInitiated;
+    SinglePackageBackupRunner mBackupRunner;
+    private final int mBackupRunnerOpToken;
+    private final OnTaskFinishedListener mListener;
+    private final TransportClient mTransportClient;
+    private final int mUserId;
+
+    // This is true when a backup operation for some package is in progress.
+    private volatile boolean mIsDoingBackup;
+    private volatile boolean mCancelAll;
+    private final int mCurrentOpToken;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    public PerformFullTransportBackupTask(UserBackupManagerService backupManagerService,
+            TransportClient transportClient,
+            IFullBackupRestoreObserver observer,
+            String[] whichPackages, boolean updateSchedule,
+            FullBackupJob runningJob, CountDownLatch latch, IBackupObserver backupObserver,
+            @Nullable IBackupManagerMonitor monitor, @Nullable OnTaskFinishedListener listener,
+            boolean userInitiated) {
+        super(observer);
+        this.mUserBackupManagerService = backupManagerService;
+        mTransportClient = transportClient;
+        mUpdateSchedule = updateSchedule;
+        mLatch = latch;
+        mJob = runningJob;
+        mPackages = new ArrayList<>(whichPackages.length);
+        mBackupObserver = backupObserver;
+        mMonitor = monitor;
+        mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP;
+        mUserInitiated = userInitiated;
+        mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
+        mBackupRunnerOpToken = backupManagerService.generateRandomIntegerToken();
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+        mUserId = backupManagerService.getUserId();
+
+        if (backupManagerService.isBackupOperationInProgress()) {
+            if (DEBUG) {
+                Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
+            }
+            mCancelAll = true;
+            return;
+        }
+
+        registerTask();
+
+        for (String pkg : whichPackages) {
+            try {
+                PackageManager pm = backupManagerService.getPackageManager();
+                PackageInfo info = pm.getPackageInfoAsUser(pkg,
+                        PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+                mCurrentPackage = info;
+                if (!AppBackupUtils.appIsEligibleForBackup(info.applicationInfo, mUserId)) {
+                    // Cull any packages that have indicated that backups are not permitted,
+                    // that run as system-domain uids but do not define their own backup agents,
+                    // as well as any explicit mention of the 'special' shared-storage agent
+                    // package (we handle that one at the end).
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Ignoring ineligible package " + pkg);
+                    }
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
+                            mCurrentPackage,
+                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            null);
+                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
+                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                    continue;
+                } else if (!AppBackupUtils.appGetsFullBackup(info)) {
+                    // Cull any packages that are found in the queue but now aren't supposed
+                    // to get full-data backup operations.
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Ignoring full-data backup of key/value participant "
+                                + pkg);
+                    }
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
+                            mCurrentPackage,
+                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            null);
+                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
+                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                    continue;
+                } else if (AppBackupUtils.appIsStopped(info.applicationInfo)) {
+                    // Cull any packages in the 'stopped' state: they've either just been
+                    // installed or have explicitly been force-stopped by the user.  In both
+                    // cases we do not want to launch them for backup.
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Ignoring stopped package " + pkg);
+                    }
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
+                            mCurrentPackage,
+                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            null);
+                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
+                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                    continue;
+                }
+                mPackages.add(info);
+            } catch (NameNotFoundException e) {
+                Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
+                        mCurrentPackage,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        null);
+            }
+        }
+
+        mPackages = backupManagerService.filterUserFacingPackages(mPackages);
+    }
+
+    private void registerTask() {
+        synchronized (mUserBackupManagerService.getCurrentOpLock()) {
+            Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
+            mUserBackupManagerService.getCurrentOperations().put(
+                    mCurrentOpToken,
+                    new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
+        }
+    }
+
+    public void unregisterTask() {
+        mUserBackupManagerService.removeOperation(mCurrentOpToken);
+    }
+
+    @Override
+    public void execute() {
+        // Nothing to do.
+    }
+
+    @Override
+    public void handleCancel(boolean cancelAll) {
+        synchronized (mCancelLock) {
+            // We only support 'cancelAll = true' case for this task. Cancelling of a single package
+
+            // due to timeout is handled by SinglePackageBackupRunner and
+            // SinglePackageBackupPreflight.
+
+            if (!cancelAll) {
+                Slog.wtf(TAG, "Expected cancelAll to be true.");
+            }
+
+            if (mCancelAll) {
+                Slog.d(TAG, "Ignoring duplicate cancel call.");
+                return;
+            }
+
+            mCancelAll = true;
+            if (mIsDoingBackup) {
+                mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll);
+                try {
+                    // If we're running a backup we should be connected to a transport
+                    IBackupTransport transport =
+                            mTransportClient.getConnectedTransport("PFTBT.handleCancel()");
+                    transport.cancelFullBackup();
+                } catch (RemoteException | TransportNotAvailableException e) {
+                    Slog.w(TAG, "Error calling cancelFullBackup() on transport: " + e);
+                    // Can't do much.
+                }
+            }
+        }
+    }
+
+    @Override
+    public void operationComplete(long result) {
+        // Nothing to do.
+    }
+
+    @Override
+    public void run() {
+
+        // data from the app, passed to us for bridging to the transport
+        ParcelFileDescriptor[] enginePipes = null;
+
+        // Pipe through which we write data to the transport
+        ParcelFileDescriptor[] transportPipes = null;
+
+        long backoff = 0;
+        int backupRunStatus = BackupManager.SUCCESS;
+
+        try {
+            if (!mUserBackupManagerService.isEnabled()
+                    || !mUserBackupManagerService.isSetupComplete()) {
+                // Backups are globally disabled, so don't proceed.
+                if (DEBUG) {
+                    Slog.i(TAG, "full backup requested but enabled=" + mUserBackupManagerService
+                            .isEnabled()
+                            + " setupComplete=" + mUserBackupManagerService.isSetupComplete()
+                            + "; ignoring");
+                }
+                int monitoringEvent;
+                if (mUserBackupManagerService.isSetupComplete()) {
+                    monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED;
+                } else {
+                    monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
+                }
+                mMonitor = BackupManagerMonitorUtils
+                        .monitorEvent(mMonitor, monitoringEvent, null,
+                                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                null);
+                mUpdateSchedule = false;
+                backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
+                return;
+            }
+
+            IBackupTransport transport = mTransportClient.connect("PFTBT.run()");
+            if (transport == null) {
+                Slog.w(TAG, "Transport not present; full data backup not performed");
+                backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
+                        mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+                        null);
+                return;
+            }
+
+            // Set up to send data to the transport
+            final int N = mPackages.size();
+            final byte[] buffer = new byte[8192];
+            for (int i = 0; i < N; i++) {
+                mBackupRunner = null;
+                PackageInfo currentPackage = mPackages.get(i);
+                String packageName = currentPackage.packageName;
+                if (DEBUG) {
+                    Slog.i(TAG, "Initiating full-data transport backup of " + packageName
+                            + " token: " + mCurrentOpToken);
+                }
+                EventLog.writeEvent(EventLogTags.FULL_BACKUP_PACKAGE, packageName);
+
+                transportPipes = ParcelFileDescriptor.createPipe();
+
+                // Tell the transport the data's coming
+                int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
+                int backupPackageStatus;
+                long quota = Long.MAX_VALUE;
+                synchronized (mCancelLock) {
+                    if (mCancelAll) {
+                        break;
+                    }
+                    backupPackageStatus = transport.performFullBackup(currentPackage,
+                            transportPipes[0], flags);
+
+                    if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+                        quota = transport.getBackupQuota(currentPackage.packageName,
+                                true /* isFullBackup */);
+                        // Now set up the backup engine / data source end of things
+                        enginePipes = ParcelFileDescriptor.createPipe();
+                        mBackupRunner =
+                                new SinglePackageBackupRunner(enginePipes[1], currentPackage,
+                                        mTransportClient, quota, mBackupRunnerOpToken,
+                                        transport.getTransportFlags());
+                        // The runner dup'd the pipe half, so we close it here
+                        enginePipes[1].close();
+                        enginePipes[1] = null;
+
+                        mIsDoingBackup = true;
+                    }
+                }
+                if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+
+                    // The transport has its own copy of the read end of the pipe,
+                    // so close ours now
+                    transportPipes[0].close();
+                    transportPipes[0] = null;
+
+                    // Spin off the runner to fetch the app's data and pipe it
+                    // into the engine pipes
+                    (new Thread(mBackupRunner, "package-backup-bridge")).start();
+
+                    // Read data off the engine pipe and pass it to the transport
+                    // pipe until we hit EOD on the input stream.  We do not take
+                    // close() responsibility for these FDs into these stream wrappers.
+                    FileInputStream in = new FileInputStream(
+                            enginePipes[0].getFileDescriptor());
+                    FileOutputStream out = new FileOutputStream(
+                            transportPipes[1].getFileDescriptor());
+                    long totalRead = 0;
+                    final long preflightResult = mBackupRunner.getPreflightResultBlocking();
+                    // Preflight result is negative if some error happened on preflight.
+                    if (preflightResult < 0) {
+                        if (MORE_DEBUG) {
+                            Slog.d(TAG, "Backup error after preflight of package "
+                                    + packageName + ": " + preflightResult
+                                    + ", not running backup.");
+                        }
+                        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                                BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
+                                mCurrentPackage,
+                                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                BackupManagerMonitorUtils.putMonitoringExtra(null,
+                                        BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
+                                        preflightResult));
+                        backupPackageStatus = (int) preflightResult;
+                    } else {
+                        int nRead = 0;
+                        do {
+                            nRead = in.read(buffer);
+                            if (MORE_DEBUG) {
+                                Slog.v(TAG, "in.read(buffer) from app: " + nRead);
+                            }
+                            if (nRead > 0) {
+                                out.write(buffer, 0, nRead);
+                                synchronized (mCancelLock) {
+                                    if (!mCancelAll) {
+                                        backupPackageStatus = transport.sendBackupData(nRead);
+                                    }
+                                }
+                                totalRead += nRead;
+                                if (mBackupObserver != null && preflightResult > 0) {
+                                    BackupObserverUtils
+                                            .sendBackupOnUpdate(mBackupObserver, packageName,
+                                                    new BackupProgress(preflightResult, totalRead));
+                                }
+                            }
+                        } while (nRead > 0
+                                && backupPackageStatus == BackupTransport.TRANSPORT_OK);
+                        // Despite preflight succeeded, package still can hit quota on flight.
+                        if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+                            Slog.w(TAG, "Package hit quota limit in-flight " + packageName
+                                    + ": " + totalRead + " of " + quota);
+                            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                                    BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
+                                    mCurrentPackage,
+                                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+                                    null);
+                            mBackupRunner.sendQuotaExceeded(totalRead, quota);
+                        }
+                    }
+
+                    final int backupRunnerResult = mBackupRunner.getBackupResultBlocking();
+
+                    synchronized (mCancelLock) {
+                        mIsDoingBackup = false;
+                        // If mCancelCurrent is true, we have already called cancelFullBackup().
+                        if (!mCancelAll) {
+                            if (backupRunnerResult == BackupTransport.TRANSPORT_OK) {
+                                // If we were otherwise in a good state, now interpret the final
+                                // result based on what finishBackup() returns.  If we're in a
+                                // failure case already, preserve that result and ignore whatever
+                                // finishBackup() reports.
+                                final int finishResult = transport.finishBackup();
+                                if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+                                    backupPackageStatus = finishResult;
+                                }
+                            } else {
+                                transport.cancelFullBackup();
+                            }
+                        }
+                    }
+
+                    // A transport-originated error here means that we've hit an error that the
+                    // runner doesn't know about, so it's still moving data but we're pulling the
+                    // rug out from under it.  Don't ask for its result:  we already know better
+                    // and we'll hang if we block waiting for it, since it relies on us to
+                    // read back the data it's writing into the engine.  Just proceed with
+                    // a graceful failure.  The runner/engine mechanism will tear itself
+                    // down cleanly when we close the pipes from this end.  Transport-level
+                    // errors take precedence over agent/app-specific errors for purposes of
+                    // determining our course of action.
+                    if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
+                        // We still could fail in backup runner thread.
+                        if (backupRunnerResult != BackupTransport.TRANSPORT_OK) {
+                            // If there was an error in runner thread and
+                            // not TRANSPORT_ERROR here, overwrite it.
+                            backupPackageStatus = backupRunnerResult;
+                        }
+                    } else {
+                        if (MORE_DEBUG) {
+                            Slog.i(TAG, "Transport-level failure; cancelling agent work");
+                        }
+                    }
+
+                    if (MORE_DEBUG) {
+                        Slog.i(TAG, "Done delivering backup data: result="
+                                + backupPackageStatus);
+                    }
+
+                    if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
+                        Slog.w(TAG, "Error " + backupPackageStatus + " backing up "
+                                + packageName);
+                    }
+
+                    // Also ask the transport how long it wants us to wait before
+                    // moving on to the next package, if any.
+                    backoff = transport.requestFullBackupTime();
+                    if (DEBUG_SCHEDULING) {
+                        Slog.i(TAG, "Transport suggested backoff=" + backoff);
+                    }
+
+                }
+
+                // Roll this package to the end of the backup queue if we're
+                // in a queue-driven mode (regardless of success/failure)
+                if (mUpdateSchedule) {
+                    mUserBackupManagerService.enqueueFullBackup(
+                            packageName, System.currentTimeMillis());
+                }
+
+                if (backupPackageStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
+                    if (DEBUG) {
+                        Slog.i(TAG, "Transport rejected backup of " + packageName
+                                + ", skipping");
+                    }
+                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_AGENT_FAILURE, packageName,
+                            "transport rejected");
+                    // This failure state can come either a-priori from the transport, or
+                    // from the preflight pass.  If we got as far as preflight, we now need
+                    // to tear down the target process.
+                    if (mBackupRunner != null) {
+                        mUserBackupManagerService.tearDownAgentAndKill(
+                                currentPackage.applicationInfo);
+                    }
+                    // ... and continue looping.
+                } else if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
+                    if (DEBUG) {
+                        Slog.i(TAG, "Transport quota exceeded for package: " + packageName);
+                        EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED,
+                                packageName);
+                    }
+                    mUserBackupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
+                    // Do nothing, clean up, and continue looping.
+                } else if (backupPackageStatus == BackupTransport.AGENT_ERROR) {
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.ERROR_AGENT_FAILURE);
+                    Slog.w(TAG, "Application failure for package: " + packageName);
+                    EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
+                    mUserBackupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
+                    // Do nothing, clean up, and continue looping.
+                } else if (backupPackageStatus == BackupManager.ERROR_BACKUP_CANCELLED) {
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.ERROR_BACKUP_CANCELLED);
+                    Slog.w(TAG, "Backup cancelled. package=" + packageName +
+                            ", cancelAll=" + mCancelAll);
+                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName);
+                    mUserBackupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
+                    // Do nothing, clean up, and continue looping.
+                } else if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.ERROR_TRANSPORT_ABORTED);
+                    Slog.w(TAG, "Transport failed; aborting backup: " + backupPackageStatus);
+                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_TRANSPORT_FAILURE);
+                    // Abort entire backup pass.
+                    backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
+                    mUserBackupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
+                    return;
+                } else {
+                    // Success!
+                    BackupObserverUtils
+                            .sendBackupOnPackageResult(mBackupObserver, packageName,
+                                    BackupManager.SUCCESS);
+                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_SUCCESS, packageName);
+                    mUserBackupManagerService.logBackupComplete(packageName);
+                }
+                cleanUpPipes(transportPipes);
+                cleanUpPipes(enginePipes);
+                if (currentPackage.applicationInfo != null) {
+                    Slog.i(TAG, "Unbinding agent in " + packageName);
+                    try {
+                        mUserBackupManagerService.getActivityManager().unbindBackupAgent(
+                                currentPackage.applicationInfo);
+                    } catch (RemoteException e) { /* can't happen; activity manager is local */ }
+                }
+            }
+        } catch (Exception e) {
+            backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
+            Slog.w(TAG, "Exception trying full transport backup", e);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
+                    mCurrentPackage,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    BackupManagerMonitorUtils.putMonitoringExtra(null,
+                            BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
+                            Log.getStackTraceString(e)));
+
+        } finally {
+
+            if (mCancelAll) {
+                backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
+            }
+
+            if (DEBUG) {
+                Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
+            }
+            BackupObserverUtils.sendBackupFinished(mBackupObserver, backupRunStatus);
+
+            cleanUpPipes(transportPipes);
+            cleanUpPipes(enginePipes);
+
+            unregisterTask();
+
+            if (mJob != null) {
+                mJob.finishBackupPass(mUserId);
+            }
+
+            synchronized (mUserBackupManagerService.getQueueLock()) {
+                mUserBackupManagerService.setRunningFullBackupTask(null);
+            }
+
+            mListener.onFinished("PFTBT.run()");
+
+            mLatch.countDown();
+
+            // Now that we're actually done with schedule-driven work, reschedule
+            // the next pass based on the new queue state.
+            if (mUpdateSchedule) {
+                mUserBackupManagerService.scheduleNextFullBackupJob(backoff);
+            }
+
+            Slog.i(TAG, "Full data backup pass finished.");
+            mUserBackupManagerService.getWakelock().release();
+        }
+    }
+
+    void cleanUpPipes(ParcelFileDescriptor[] pipes) {
+        if (pipes != null) {
+            if (pipes[0] != null) {
+                ParcelFileDescriptor fd = pipes[0];
+                pipes[0] = null;
+                try {
+                    fd.close();
+                } catch (IOException e) {
+                    Slog.w(TAG, "Unable to close pipe!");
+                }
+            }
+            if (pipes[1] != null) {
+                ParcelFileDescriptor fd = pipes[1];
+                pipes[1] = null;
+                try {
+                    fd.close();
+                } catch (IOException e) {
+                    Slog.w(TAG, "Unable to close pipe!");
+                }
+            }
+        }
+    }
+
+    // Run the backup and pipe it back to the given socket -- expects to run on
+    // a standalone thread.  The  runner owns this half of the pipe, and closes
+    // it to indicate EOD to the other end.
+    class SinglePackageBackupPreflight implements BackupRestoreTask, FullBackupPreflight {
+        final AtomicLong mResult = new AtomicLong(BackupTransport.AGENT_ERROR);
+        final CountDownLatch mLatch = new CountDownLatch(1);
+        final TransportClient mTransportClient;
+        final long mQuota;
+        private final int mCurrentOpToken;
+        private final int mTransportFlags;
+
+        SinglePackageBackupPreflight(
+                TransportClient transportClient,
+                long quota,
+                int currentOpToken,
+                int transportFlags) {
+            mTransportClient = transportClient;
+            mQuota = quota;
+            mCurrentOpToken = currentOpToken;
+            mTransportFlags = transportFlags;
+        }
+
+        @Override
+        public int preflightFullBackup(PackageInfo pkg, IBackupAgent agent) {
+            int result;
+            long fullBackupAgentTimeoutMillis =
+                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+            try {
+                mUserBackupManagerService.prepareOperationTimeout(
+                        mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OP_TYPE_BACKUP_WAIT);
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
+                }
+                agent.doMeasureFullBackup(mQuota, mCurrentOpToken,
+                        mUserBackupManagerService.getBackupManagerBinder(), mTransportFlags);
+
+                // Now wait to get our result back.  If this backstop timeout is reached without
+                // the latch being thrown, flow will continue as though a result or "normal"
+                // timeout had been produced.  In case of a real backstop timeout, mResult
+                // will still contain the value it was constructed with, AGENT_ERROR, which
+                // intentionaly falls into the "just report failure" code.
+                mLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
+
+                long totalSize = mResult.get();
+                // If preflight timed out, mResult will contain error code as int.
+                if (totalSize < 0) {
+                    return (int) totalSize;
+                }
+                if (MORE_DEBUG) {
+                    Slog.v(TAG, "Got preflight response; size=" + totalSize);
+                }
+
+                IBackupTransport transport =
+                        mTransportClient.connectOrThrow("PFTBT$SPBP.preflightFullBackup()");
+                result = transport.checkFullBackupSize(totalSize);
+                if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+                    if (MORE_DEBUG) {
+                        Slog.d(TAG, "Package hit quota limit on preflight " +
+                                pkg.packageName + ": " + totalSize + " of " + mQuota);
+                    }
+                    RemoteCall.execute(
+                            callback -> agent.doQuotaExceeded(totalSize, mQuota, callback),
+                            mAgentTimeoutParameters.getQuotaExceededTimeoutMillis());
+                }
+            } catch (Exception e) {
+                Slog.w(TAG, "Exception preflighting " + pkg.packageName + ": " + e.getMessage());
+                result = BackupTransport.AGENT_ERROR;
+            }
+            return result;
+        }
+
+        @Override
+        public void execute() {
+            // Unused.
+        }
+
+        @Override
+        public void operationComplete(long result) {
+            // got the callback, and our preflightFullBackup() method is waiting for the result
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Preflight op complete, result=" + result);
+            }
+            mResult.set(result);
+            mLatch.countDown();
+            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+        }
+
+        @Override
+        public void handleCancel(boolean cancelAll) {
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Preflight cancelled; failing");
+            }
+            mResult.set(BackupTransport.AGENT_ERROR);
+            mLatch.countDown();
+            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+        }
+
+        @Override
+        public long getExpectedSizeOrErrorCode() {
+            long fullBackupAgentTimeoutMillis =
+                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+            try {
+                mLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
+                return mResult.get();
+            } catch (InterruptedException e) {
+                return BackupTransport.NO_MORE_DATA;
+            }
+        }
+    }
+
+    class SinglePackageBackupRunner implements Runnable, BackupRestoreTask {
+        final ParcelFileDescriptor mOutput;
+        final PackageInfo mTarget;
+        final SinglePackageBackupPreflight mPreflight;
+        final CountDownLatch mPreflightLatch;
+        final CountDownLatch mBackupLatch;
+        private final int mCurrentOpToken;
+        private final int mEphemeralToken;
+        private FullBackupEngine mEngine;
+        private volatile int mPreflightResult;
+        private volatile int mBackupResult;
+        private final long mQuota;
+        private volatile boolean mIsCancelled;
+        private final int mTransportFlags;
+
+        SinglePackageBackupRunner(ParcelFileDescriptor output, PackageInfo target,
+                TransportClient transportClient, long quota, int currentOpToken, int transportFlags)
+                throws IOException {
+            mOutput = ParcelFileDescriptor.dup(output.getFileDescriptor());
+            mTarget = target;
+            mCurrentOpToken = currentOpToken;
+            mEphemeralToken = mUserBackupManagerService.generateRandomIntegerToken();
+            mPreflight = new SinglePackageBackupPreflight(
+                    transportClient, quota, mEphemeralToken, transportFlags);
+            mPreflightLatch = new CountDownLatch(1);
+            mBackupLatch = new CountDownLatch(1);
+            mPreflightResult = BackupTransport.AGENT_ERROR;
+            mBackupResult = BackupTransport.AGENT_ERROR;
+            mQuota = quota;
+            mTransportFlags = transportFlags;
+            registerTask();
+        }
+
+        void registerTask() {
+            synchronized (mUserBackupManagerService.getCurrentOpLock()) {
+                mUserBackupManagerService.getCurrentOperations().put(
+                        mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP_WAIT));
+            }
+        }
+
+        void unregisterTask() {
+            synchronized (mUserBackupManagerService.getCurrentOpLock()) {
+                mUserBackupManagerService.getCurrentOperations().remove(mCurrentOpToken);
+            }
+        }
+
+        @Override
+        public void run() {
+            FileOutputStream out = new FileOutputStream(mOutput.getFileDescriptor());
+            mEngine =
+                    new FullBackupEngine(
+                            mUserBackupManagerService,
+                            out,
+                            mPreflight,
+                            mTarget,
+                            false,
+                            this,
+                            mQuota,
+                            mCurrentOpToken,
+                            mTransportFlags);
+            try {
+                try {
+                    if (!mIsCancelled) {
+                        mPreflightResult = mEngine.preflightCheck();
+                    }
+                } finally {
+                    mPreflightLatch.countDown();
+                }
+                // If there is no error on preflight, continue backup.
+                if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
+                    if (!mIsCancelled) {
+                        mBackupResult = mEngine.backupOnePackage();
+                    }
+                }
+            } catch (Exception e) {
+                Slog.w(TAG, "Exception during full package backup of " + mTarget.packageName,
+                        e);
+            } finally {
+                unregisterTask();
+                mBackupLatch.countDown();
+                try {
+                    mOutput.close();
+                } catch (IOException e) {
+                    Slog.w(TAG, "Error closing transport pipe in runner");
+                }
+            }
+        }
+
+        public void sendQuotaExceeded(final long backupDataBytes, final long quotaBytes) {
+            mEngine.sendQuotaExceeded(backupDataBytes, quotaBytes);
+        }
+
+        // If preflight succeeded, returns positive number - preflight size,
+        // otherwise return negative error code.
+        long getPreflightResultBlocking() {
+            long fullBackupAgentTimeoutMillis =
+                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+            try {
+                mPreflightLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
+                if (mIsCancelled) {
+                    return BackupManager.ERROR_BACKUP_CANCELLED;
+                }
+                if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
+                    return mPreflight.getExpectedSizeOrErrorCode();
+                } else {
+                    return mPreflightResult;
+                }
+            } catch (InterruptedException e) {
+                return BackupTransport.AGENT_ERROR;
+            }
+        }
+
+        int getBackupResultBlocking() {
+            long fullBackupAgentTimeoutMillis =
+                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+            try {
+                mBackupLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
+                if (mIsCancelled) {
+                    return BackupManager.ERROR_BACKUP_CANCELLED;
+                }
+                return mBackupResult;
+            } catch (InterruptedException e) {
+                return BackupTransport.AGENT_ERROR;
+            }
+        }
+
+
+        // BackupRestoreTask interface: specifically, timeout detection
+
+        @Override
+        public void execute() { /* intentionally empty */ }
+
+        @Override
+        public void operationComplete(long result) { /* intentionally empty */ }
+
+        @Override
+        public void handleCancel(boolean cancelAll) {
+            if (DEBUG) {
+                Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
+            }
+
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
+                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+            mIsCancelled = true;
+            // Cancel tasks spun off by this task.
+            mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
+            mUserBackupManagerService.tearDownAgentAndKill(mTarget.applicationInfo);
+            // Free up everyone waiting on this task and its children.
+            mPreflightLatch.countDown();
+            mBackupLatch.countDown();
+            // We are done with this operation.
+            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/BackupHandler.java b/com/android/server/backup/internal/BackupHandler.java
new file mode 100644
index 0000000..87a8e49
--- /dev/null
+++ b/com/android/server/backup/internal/BackupHandler.java
@@ -0,0 +1,479 @@
+/*
+ * 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;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.app.backup.RestoreSet;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.RemoteException;
+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.server.EventLogTags;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.DataChangedJournal;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.fullbackup.PerformAdbBackupTask;
+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;
+import com.android.server.backup.params.BackupParams;
+import com.android.server.backup.params.ClearParams;
+import com.android.server.backup.params.ClearRetryParams;
+import com.android.server.backup.params.RestoreGetSetsParams;
+import com.android.server.backup.params.RestoreParams;
+import com.android.server.backup.restore.PerformAdbRestoreTask;
+import com.android.server.backup.restore.PerformUnifiedRestoreTask;
+import com.android.server.backup.transport.TransportClient;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Asynchronous backup/restore handler thread.
+ */
+public class BackupHandler extends Handler {
+
+    public static final int MSG_RUN_BACKUP = 1;
+    public static final int MSG_RUN_ADB_BACKUP = 2;
+    public static final int MSG_RUN_RESTORE = 3;
+    public static final int MSG_RUN_CLEAR = 4;
+    public static final int MSG_RUN_GET_RESTORE_SETS = 6;
+    public static final int MSG_RESTORE_SESSION_TIMEOUT = 8;
+    public static final int MSG_FULL_CONFIRMATION_TIMEOUT = 9;
+    public static final int MSG_RUN_ADB_RESTORE = 10;
+    public static final int MSG_RETRY_CLEAR = 12;
+    public static final int MSG_REQUEST_BACKUP = 15;
+    public static final int MSG_SCHEDULE_BACKUP_PACKAGE = 16;
+    public static final int MSG_BACKUP_OPERATION_TIMEOUT = 17;
+    public static final int MSG_RESTORE_OPERATION_TIMEOUT = 18;
+    // backup task state machine tick
+    public static final int MSG_BACKUP_RESTORE_STEP = 20;
+    public static final int MSG_OP_COMPLETE = 21;
+    // Release the wakelock. This is used to ensure we don't hold it after
+    // a user is removed. This will also terminate the looper thread.
+    public static final int MSG_STOP = 22;
+
+    private final UserBackupManagerService backupManagerService;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    private final HandlerThread mBackupThread;
+
+    @VisibleForTesting
+    volatile boolean mIsStopping = false;
+
+    public BackupHandler(
+            UserBackupManagerService backupManagerService, HandlerThread backupThread) {
+        super(backupThread.getLooper());
+        mBackupThread = backupThread;
+        this.backupManagerService = backupManagerService;
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+    }
+
+    /**
+     * Put the BackupHandler into a stopping state where the remaining messages on the queue will be
+     * silently dropped and the {@link WakeLock} held by the {@link UserBackupManagerService} will
+     * then be released.
+     */
+    public void stop() {
+        mIsStopping = true;
+        sendMessage(obtainMessage(BackupHandler.MSG_STOP));
+    }
+
+    @Override
+    public void dispatchMessage(Message message) {
+        try {
+            dispatchMessageInternal(message);
+        } catch (Exception e) {
+            // If the backup service is stopping, we'll suppress all exceptions to avoid crashes
+            // caused by code still running after the current user has become unavailable.
+            if (!mIsStopping) {
+                throw e;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void dispatchMessageInternal(Message message) {
+        super.dispatchMessage(message);
+    }
+
+    public void handleMessage(Message msg) {
+        if (msg.what == MSG_STOP) {
+            Slog.v(TAG, "Stopping backup handler");
+            backupManagerService.getWakelock().quit();
+            mBackupThread.quitSafely();
+        }
+
+        if (mIsStopping) {
+            // If we're finishing all other types of messages should be ignored
+            return;
+        }
+
+        TransportManager transportManager = backupManagerService.getTransportManager();
+        switch (msg.what) {
+            case MSG_RUN_BACKUP: {
+                backupManagerService.setLastBackupPass(System.currentTimeMillis());
+
+                String callerLogString = "BH/MSG_RUN_BACKUP";
+                TransportClient transportClient =
+                        transportManager.getCurrentTransportClient(callerLogString);
+                IBackupTransport transport =
+                        transportClient != null
+                                ? transportClient.connect(callerLogString)
+                                : null;
+                if (transport == null) {
+                    if (transportClient != null) {
+                        transportManager
+                                .disposeOfTransportClient(transportClient, callerLogString);
+                    }
+                    Slog.v(TAG, "Backup requested but no transport available");
+                    break;
+                }
+
+                // Snapshot the pending-backup set and work on that.
+                List<String> queue = new ArrayList<>();
+                DataChangedJournal oldJournal = backupManagerService.getJournal();
+                synchronized (backupManagerService.getQueueLock()) {
+                    // Don't run backups if one is already running.
+                    if (backupManagerService.isBackupRunning()) {
+                        Slog.i(TAG, "Backup time but one already running");
+                        return;
+                    }
+
+                    if (DEBUG) {
+                        Slog.v(TAG, "Running a backup pass");
+                    }
+
+                    // Acquire the wakelock and pass it to the backup thread. It will be released
+                    // once backup concludes.
+                    backupManagerService.setBackupRunning(true);
+                    backupManagerService.getWakelock().acquire();
+
+                    // Do we have any work to do?  Construct the work queue
+                    // then release the synchronization lock to actually run
+                    // the backup.
+                    if (backupManagerService.getPendingBackups().size() > 0) {
+                        for (BackupRequest b : backupManagerService.getPendingBackups().values()) {
+                            queue.add(b.packageName);
+                        }
+                        if (DEBUG) {
+                            Slog.v(TAG, "clearing pending backups");
+                        }
+                        backupManagerService.getPendingBackups().clear();
+
+                        // Start a new backup-queue journal file too
+                        backupManagerService.setJournal(null);
+
+                    }
+                }
+
+                // At this point, we have started a new journal file, and the old
+                // file identity is being passed to the backup processing task.
+                // When it completes successfully, that old journal file will be
+                // deleted.  If we crash prior to that, the old journal is parsed
+                // at next boot and the journaled requests fulfilled.
+                boolean staged = true;
+                if (queue.size() > 0) {
+                    // Spin up a backup state sequence and set it running
+                    try {
+                        OnTaskFinishedListener listener =
+                                caller ->
+                                        transportManager
+                                                .disposeOfTransportClient(transportClient, caller);
+                        KeyValueBackupTask.start(
+                                backupManagerService,
+                                transportClient,
+                                transport.transportDirName(),
+                                queue,
+                                oldJournal,
+                                /* observer */ null,
+                                /* monitor */ null,
+                                listener,
+                                Collections.emptyList(),
+                                /* userInitiated */ false,
+                                /* nonIncremental */ false);
+                    } catch (Exception e) {
+                        // unable to ask the transport its dir name -- transient failure, since
+                        // the above check succeeded.  Try again next time.
+                        Slog.e(TAG, "Transport became unavailable attempting backup"
+                                + " or error initializing backup task", e);
+                        staged = false;
+                    }
+                } else {
+                    Slog.v(TAG, "Backup requested but nothing pending");
+                    staged = false;
+                }
+
+                if (!staged) {
+                    transportManager.disposeOfTransportClient(transportClient, callerLogString);
+                    // if we didn't actually hand off the wakelock, rewind until next time
+                    synchronized (backupManagerService.getQueueLock()) {
+                        backupManagerService.setBackupRunning(false);
+                    }
+                    backupManagerService.getWakelock().release();
+                }
+                break;
+            }
+
+            case MSG_BACKUP_RESTORE_STEP: {
+                try {
+                    BackupRestoreTask task = (BackupRestoreTask) msg.obj;
+                    if (MORE_DEBUG) {
+                        Slog.v(TAG, "Got next step for " + task + ", executing");
+                    }
+                    task.execute();
+                } catch (ClassCastException e) {
+                    Slog.e(TAG, "Invalid backup/restore task in flight, obj=" + msg.obj);
+                }
+                break;
+            }
+
+            case MSG_OP_COMPLETE: {
+                try {
+                    Pair<BackupRestoreTask, Long> taskWithResult =
+                            (Pair<BackupRestoreTask, Long>) msg.obj;
+                    taskWithResult.first.operationComplete(taskWithResult.second);
+                } catch (ClassCastException e) {
+                    Slog.e(TAG, "Invalid completion in flight, obj=" + msg.obj);
+                }
+                break;
+            }
+
+            case MSG_RUN_ADB_BACKUP: {
+                // TODO: refactor full backup to be a looper-based state machine
+                // similar to normal backup/restore.
+                AdbBackupParams params = (AdbBackupParams) msg.obj;
+                PerformAdbBackupTask task = new PerformAdbBackupTask(backupManagerService,
+                        params.fd,
+                        params.observer, params.includeApks, params.includeObbs,
+                        params.includeShared, params.doWidgets, params.curPassword,
+                        params.encryptPassword, params.allApps, params.includeSystem,
+                        params.doCompress, params.includeKeyValue, params.packages, params.latch);
+                (new Thread(task, "adb-backup")).start();
+                break;
+            }
+
+            case MSG_RUN_RESTORE: {
+                RestoreParams params = (RestoreParams) msg.obj;
+                Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer);
+
+                PerformUnifiedRestoreTask task =
+                        new PerformUnifiedRestoreTask(
+                                backupManagerService,
+                                params.transportClient,
+                                params.observer,
+                                params.monitor,
+                                params.token,
+                                params.packageInfo,
+                                params.pmToken,
+                                params.isSystemRestore,
+                                params.filterSet,
+                                params.listener);
+
+                synchronized (backupManagerService.getPendingRestores()) {
+                    if (backupManagerService.isRestoreInProgress()) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Restore in progress, queueing.");
+                        }
+                        backupManagerService.getPendingRestores().add(task);
+                        // This task will be picked up and executed when the the currently running
+                        // restore task finishes.
+                    } else {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Starting restore.");
+                        }
+                        backupManagerService.setRestoreInProgress(true);
+                        Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task);
+                        sendMessage(restoreMsg);
+                    }
+                }
+                break;
+            }
+
+            case MSG_RUN_ADB_RESTORE: {
+                // TODO: refactor full restore to be a looper-based state machine
+                // similar to normal backup/restore.
+                AdbRestoreParams params = (AdbRestoreParams) msg.obj;
+                PerformAdbRestoreTask task = new PerformAdbRestoreTask(backupManagerService,
+                        params.fd,
+                        params.curPassword, params.encryptPassword,
+                        params.observer, params.latch);
+                (new Thread(task, "adb-restore")).start();
+                break;
+            }
+
+            case MSG_RUN_CLEAR: {
+                ClearParams params = (ClearParams) msg.obj;
+                Runnable task =
+                        new PerformClearTask(
+                                backupManagerService,
+                                params.transportClient,
+                                params.packageInfo,
+                                params.listener);
+                task.run();
+                break;
+            }
+
+            case MSG_RETRY_CLEAR: {
+                // reenqueues if the transport remains unavailable
+                ClearRetryParams params = (ClearRetryParams) msg.obj;
+                backupManagerService.clearBackupData(params.transportName, params.packageName);
+                break;
+            }
+
+            case MSG_RUN_GET_RESTORE_SETS: {
+                // Like other async operations, this is entered with the wakelock held
+                RestoreSet[] sets = null;
+                RestoreGetSetsParams params = (RestoreGetSetsParams) msg.obj;
+                String callerLogString = "BH/MSG_RUN_GET_RESTORE_SETS";
+                try {
+                    IBackupTransport transport =
+                            params.transportClient.connectOrThrow(callerLogString);
+                    sets = transport.getAvailableRestoreSets();
+                    // cache the result in the active session
+                    synchronized (params.session) {
+                        params.session.setRestoreSets(sets);
+                    }
+                    if (sets == null) {
+                        EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+                    }
+                } catch (Exception e) {
+                    Slog.e(TAG, "Error from transport getting set list: " + e.getMessage());
+                } finally {
+                    if (params.observer != null) {
+                        try {
+                            params.observer.restoreSetsAvailable(sets);
+                        } catch (RemoteException re) {
+                            Slog.e(TAG, "Unable to report listing to observer");
+                        } catch (Exception e) {
+                            Slog.e(TAG, "Restore observer threw: " + e.getMessage());
+                        }
+                    }
+
+                    // Done: reset the session timeout clock
+                    removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+                    sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT,
+                            mAgentTimeoutParameters.getRestoreAgentTimeoutMillis());
+
+                    params.listener.onFinished(callerLogString);
+                }
+                break;
+            }
+
+            case MSG_BACKUP_OPERATION_TIMEOUT:
+            case MSG_RESTORE_OPERATION_TIMEOUT: {
+                Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1));
+                backupManagerService.handleCancel(msg.arg1, false);
+                break;
+            }
+
+            case MSG_RESTORE_SESSION_TIMEOUT: {
+                synchronized (backupManagerService) {
+                    if (backupManagerService.getActiveRestoreSession() != null) {
+                        // Client app left the restore session dangling.  We know that it
+                        // can't be in the middle of an actual restore operation because
+                        // the timeout is suspended while a restore is in progress.  Clean
+                        // up now.
+                        Slog.w(TAG, "Restore session timed out; aborting");
+                        backupManagerService.getActiveRestoreSession().markTimedOut();
+                        post(backupManagerService.getActiveRestoreSession().new EndRestoreRunnable(
+                                backupManagerService,
+                                backupManagerService.getActiveRestoreSession()));
+                    }
+                }
+                break;
+            }
+
+            case MSG_FULL_CONFIRMATION_TIMEOUT: {
+                synchronized (backupManagerService.getAdbBackupRestoreConfirmations()) {
+                    AdbParams params = backupManagerService.getAdbBackupRestoreConfirmations().get(
+                            msg.arg1);
+                    if (params != null) {
+                        Slog.i(TAG, "Full backup/restore timed out waiting for user confirmation");
+
+                        // Release the waiter; timeout == completion
+                        backupManagerService.signalAdbBackupRestoreCompletion(params);
+
+                        // Remove the token from the set
+                        backupManagerService.getAdbBackupRestoreConfirmations().delete(msg.arg1);
+
+                        // Report a timeout to the observer, if any
+                        if (params.observer != null) {
+                            try {
+                                params.observer.onTimeout();
+                            } catch (RemoteException e) {
+                                /* don't care if the app has gone away */
+                            }
+                        }
+                    } else {
+                        Slog.d(TAG, "couldn't find params for token " + msg.arg1);
+                    }
+                }
+                break;
+            }
+
+            case MSG_REQUEST_BACKUP: {
+                BackupParams params = (BackupParams) msg.obj;
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, "MSG_REQUEST_BACKUP observer=" + params.observer);
+                }
+                backupManagerService.setBackupRunning(true);
+                backupManagerService.getWakelock().acquire();
+
+                KeyValueBackupTask.start(
+                        backupManagerService,
+                        params.transportClient,
+                        params.dirName,
+                        params.kvPackages,
+                        /* dataChangedJournal */ null,
+                        params.observer,
+                        params.monitor,
+                        params.listener,
+                        params.fullPackages,
+                        /* userInitiated */ true,
+                        params.nonIncrementalBackup);
+                break;
+            }
+
+            case MSG_SCHEDULE_BACKUP_PACKAGE: {
+                String pkgName = (String) msg.obj;
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, "MSG_SCHEDULE_BACKUP_PACKAGE " + pkgName);
+                }
+                backupManagerService.dataChangedImpl(pkgName);
+                break;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/ClearDataObserver.java b/com/android/server/backup/internal/ClearDataObserver.java
new file mode 100644
index 0000000..396f369
--- /dev/null
+++ b/com/android/server/backup/internal/ClearDataObserver.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.content.pm.IPackageDataObserver;
+
+import com.android.server.backup.UserBackupManagerService;
+
+public class ClearDataObserver extends IPackageDataObserver.Stub {
+
+    private UserBackupManagerService backupManagerService;
+
+    public ClearDataObserver(UserBackupManagerService backupManagerService) {
+        this.backupManagerService = backupManagerService;
+    }
+
+    public void onRemoveCompleted(String packageName, boolean succeeded) {
+        synchronized (backupManagerService.getClearDataLock()) {
+            backupManagerService.setClearingData(false);
+            backupManagerService.getClearDataLock().notifyAll();
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/OnTaskFinishedListener.java b/com/android/server/backup/internal/OnTaskFinishedListener.java
new file mode 100644
index 0000000..e417f06
--- /dev/null
+++ b/com/android/server/backup/internal/OnTaskFinishedListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportConnectionListener;
+
+/** Listener to be called when a task finishes, successfully or not. */
+public interface OnTaskFinishedListener {
+    OnTaskFinishedListener NOP = caller -> {};
+
+    /**
+     * Called when a task finishes, successfully or not.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     */
+    void onFinished(String caller);
+}
diff --git a/com/android/server/backup/internal/Operation.java b/com/android/server/backup/internal/Operation.java
new file mode 100644
index 0000000..fd5ad92
--- /dev/null
+++ b/com/android/server/backup/internal/Operation.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.server.backup.BackupRestoreTask;
+
+public class Operation {
+
+    public int state;
+    public final BackupRestoreTask callback;
+    public final int type;
+
+    public Operation(int initialState, BackupRestoreTask callbackObj, int type) {
+        state = initialState;
+        callback = callbackObj;
+        this.type = type;
+    }
+}
diff --git a/com/android/server/backup/internal/PerformClearTask.java b/com/android/server/backup/internal/PerformClearTask.java
new file mode 100644
index 0000000..5ffa795
--- /dev/null
+++ b/com/android/server/backup/internal/PerformClearTask.java
@@ -0,0 +1,80 @@
+/*
+ * 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.TAG;
+
+import android.content.pm.PackageInfo;
+import android.util.Slog;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.transport.TransportClient;
+
+import java.io.File;
+
+public class PerformClearTask implements Runnable {
+    private final UserBackupManagerService mBackupManagerService;
+    private final TransportManager mTransportManager;
+    private final TransportClient mTransportClient;
+    private final PackageInfo mPackage;
+    private final OnTaskFinishedListener mListener;
+
+    PerformClearTask(UserBackupManagerService backupManagerService,
+            TransportClient transportClient, PackageInfo packageInfo,
+            OnTaskFinishedListener listener) {
+        mBackupManagerService = backupManagerService;
+        mTransportManager = backupManagerService.getTransportManager();
+        mTransportClient = transportClient;
+        mPackage = packageInfo;
+        mListener = listener;
+    }
+
+    public void run() {
+        String callerLogString = "PerformClearTask.run()";
+        IBackupTransport transport = null;
+        try {
+            // Clear the on-device backup state to ensure a full backup next time
+            String transportDirName =
+                    mTransportManager.getTransportDirName(mTransportClient.getTransportComponent());
+            File stateDir = new File(mBackupManagerService.getBaseStateDir(), transportDirName);
+            File stateFile = new File(stateDir, mPackage.packageName);
+            stateFile.delete();
+
+            transport = mTransportClient.connectOrThrow(callerLogString);
+            // Tell the transport to remove all the persistent storage for the app
+            // TODO - need to handle failures
+            transport.clearBackupData(mPackage);
+        } catch (Exception e) {
+            Slog.e(TAG, "Transport threw clearing data for " + mPackage + ": " + e.getMessage());
+        } finally {
+            if (transport != null) {
+                try {
+                    // TODO - need to handle failures
+                    transport.finishBackup();
+                } catch (Exception e) {
+                    // Nothing we can do here, alas
+                    Slog.e(TAG, "Unable to mark clear operation finished: " + e.getMessage());
+                }
+            }
+            mListener.onFinished(callerLogString);
+            // Last but not least, release the cpu
+            mBackupManagerService.getWakelock().release();
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/PerformInitializeTask.java b/com/android/server/backup/internal/PerformInitializeTask.java
new file mode 100644
index 0000000..6b78fbf
--- /dev/null
+++ b/com/android/server/backup/internal/PerformInitializeTask.java
@@ -0,0 +1,180 @@
+/*
+ * 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.TAG;
+
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.backup.BackupTransport;
+import android.app.backup.IBackupObserver;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.EventLog;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.EventLogTags;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.transport.TransportClient;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Attempts to call {@link BackupTransport#initializeDevice()} followed by
+ * {@link BackupTransport#finishBackup()} for the transport names passed in with the intent of
+ * wiping backup data from the transport.
+ *
+ * If the transport returns error, it will record the operation as pending and schedule it to run in
+ * a future time according to {@link BackupTransport#requestBackupTime()}. The result status
+ * reported to observers will be the last unsuccessful status reported by the transports. If every
+ * operation was successful then it's {@link BackupTransport#TRANSPORT_OK}.
+ */
+public class PerformInitializeTask implements Runnable {
+    private final UserBackupManagerService mBackupManagerService;
+    private final TransportManager mTransportManager;
+    private final String[] mQueue;
+    private final File mBaseStateDir;
+    private final OnTaskFinishedListener mListener;
+    @Nullable private IBackupObserver mObserver;
+
+    public PerformInitializeTask(
+            UserBackupManagerService backupManagerService,
+            String[] transportNames,
+            @Nullable IBackupObserver observer,
+            OnTaskFinishedListener listener) {
+        this(
+                backupManagerService,
+                backupManagerService.getTransportManager(),
+                transportNames,
+                observer,
+                listener,
+                backupManagerService.getBaseStateDir());
+    }
+
+    @VisibleForTesting
+    PerformInitializeTask(
+            UserBackupManagerService backupManagerService,
+            TransportManager transportManager,
+            String[] transportNames,
+            @Nullable IBackupObserver observer,
+            OnTaskFinishedListener listener,
+            File baseStateDir) {
+        mBackupManagerService = backupManagerService;
+        mTransportManager = transportManager;
+        mQueue = transportNames;
+        mObserver = observer;
+        mListener = listener;
+        mBaseStateDir = baseStateDir;
+    }
+
+    private void notifyResult(String target, int status) {
+        try {
+            if (mObserver != null) {
+                mObserver.onResult(target, status);
+            }
+        } catch (RemoteException ignored) {
+            mObserver = null;       // don't try again
+        }
+    }
+
+    private void notifyFinished(int status) {
+        try {
+            if (mObserver != null) {
+                mObserver.backupFinished(status);
+            }
+        } catch (RemoteException ignored) {
+            mObserver = null;
+        }
+    }
+
+    public void run() {
+        // mWakelock is *acquired* when execution begins here
+        String callerLogString = "PerformInitializeTask.run()";
+        List<TransportClient> transportClientsToDisposeOf = new ArrayList<>(mQueue.length);
+        int result = BackupTransport.TRANSPORT_OK;
+        try {
+            for (String transportName : mQueue) {
+                TransportClient transportClient =
+                        mTransportManager.getTransportClient(transportName, callerLogString);
+                if (transportClient == null) {
+                    Slog.e(TAG, "Requested init for " + transportName + " but not found");
+                    continue;
+                }
+                transportClientsToDisposeOf.add(transportClient);
+
+                Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName);
+                String transportDirName =
+                        mTransportManager.getTransportDirName(
+                                transportClient.getTransportComponent());
+                EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
+                long startRealtime = SystemClock.elapsedRealtime();
+
+                IBackupTransport transport = transportClient.connectOrThrow(callerLogString);
+                int status = transport.initializeDevice();
+                if (status != BackupTransport.TRANSPORT_OK) {
+                    Slog.e(TAG, "Transport error in initializeDevice()");
+                } else {
+                    status = transport.finishBackup();
+                    if (status != BackupTransport.TRANSPORT_OK) {
+                        Slog.e(TAG, "Transport error in finishBackup()");
+                    }
+                }
+
+                // Okay, the wipe really happened.  Clean up our local bookkeeping.
+                if (status == BackupTransport.TRANSPORT_OK) {
+                    Slog.i(TAG, "Device init successful");
+                    int millis = (int) (SystemClock.elapsedRealtime() - startRealtime);
+                    EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
+                    File stateFileDir = new File(mBaseStateDir, transportDirName);
+                    mBackupManagerService.resetBackupState(stateFileDir);
+                    EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis);
+                    mBackupManagerService.recordInitPending(false, transportName, transportDirName);
+                    notifyResult(transportName, BackupTransport.TRANSPORT_OK);
+                } else {
+                    // If this didn't work, requeue this one and try again
+                    // after a suitable interval
+                    EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
+                    mBackupManagerService.recordInitPending(true, transportName, transportDirName);
+                    notifyResult(transportName, status);
+                    result = status;
+
+                    // do this via another alarm to make sure of the wakelock states
+                    long delay = transport.requestBackupTime();
+                    Slog.w(TAG, "Init failed on " + transportName + " resched in " + delay);
+                    mBackupManagerService.getAlarmManager().set(
+                            AlarmManager.RTC_WAKEUP,
+                            System.currentTimeMillis() + delay,
+                            mBackupManagerService.getRunInitIntent());
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Unexpected error performing init", e);
+            result = BackupTransport.TRANSPORT_ERROR;
+        } finally {
+            for (TransportClient transportClient : transportClientsToDisposeOf) {
+                mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+            }
+            notifyFinished(result);
+            mListener.onFinished(callerLogString);
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/PerformInitializeTaskTest.java b/com/android/server/backup/internal/PerformInitializeTaskTest.java
new file mode 100644
index 0000000..a14cc51
--- /dev/null
+++ b/com/android/server/backup/internal/PerformInitializeTaskTest.java
@@ -0,0 +1,393 @@
+/*
+ * 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 android.app.backup.BackupTransport.TRANSPORT_ERROR;
+import static android.app.backup.BackupTransport.TRANSPORT_OK;
+
+import static com.android.server.backup.testing.TestUtils.assertLogcatContains;
+import static com.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.TransportData.d2dTransport;
+import static com.android.server.backup.testing.TransportData.localTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransports;
+
+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.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.app.backup.IBackupObserver;
+import android.os.DeadObjectException;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.BackupManagerService;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+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.transport.TransportClient;
+import com.android.server.testing.shadows.ShadowSlog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowSlog.class)
+@Presubmit
+public class PerformInitializeTaskTest {
+    @Mock private UserBackupManagerService mBackupManagerService;
+    @Mock private TransportManager mTransportManager;
+    @Mock private OnTaskFinishedListener mListener;
+    @Mock private IBackupTransport mTransportBinder;
+    @Mock private IBackupObserver mObserver;
+    @Mock private AlarmManager mAlarmManager;
+    @Mock private PendingIntent mRunInitIntent;
+    private File mBaseStateDir;
+    private TransportData mTransport;
+    private String mTransportName;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTransport = backupTransport();
+        mTransportName = mTransport.transportName;
+
+        Application context = RuntimeEnvironment.application;
+        mBaseStateDir = new File(context.getCacheDir(), "base_state_dir");
+        assertThat(mBaseStateDir.mkdir()).isTrue();
+
+        when(mBackupManagerService.getAlarmManager()).thenReturn(mAlarmManager);
+        when(mBackupManagerService.getRunInitIntent()).thenReturn(mRunInitIntent);
+    }
+
+    @Test
+    public void testRun_callsTransportCorrectly() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportBinder).initializeDevice();
+        verify(mTransportBinder).finishBackup();
+    }
+
+    @Test
+    public void testRun_callsBackupManagerCorrectly() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mBackupManagerService)
+                .recordInitPending(false, mTransportName, mTransport.transportDirName);
+        verify(mBackupManagerService)
+                .resetBackupState(eq(new File(mBaseStateDir, mTransport.transportDirName)));
+    }
+
+    @Test
+    public void testRun_callsObserverAndListenerCorrectly() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mObserver).onResult(eq(mTransportName), eq(TRANSPORT_OK));
+        verify(mObserver).backupFinished(eq(TRANSPORT_OK));
+        verify(mListener).onFinished(any());
+    }
+
+    @Test
+    public void testRun_whenInitializeDeviceFails() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportBinder).initializeDevice();
+        verify(mTransportBinder, never()).finishBackup();
+        verify(mBackupManagerService)
+                .recordInitPending(true, mTransportName, mTransport.transportDirName);
+    }
+
+    @Test
+    public void testRun_whenInitializeDeviceFails_callsObserverAndListenerCorrectly()
+            throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mObserver).onResult(eq(mTransportName), eq(TRANSPORT_ERROR));
+        verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+        verify(mListener).onFinished(any());
+    }
+
+    @Test
+    public void testRun_whenInitializeDeviceFails_schedulesAlarm() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mAlarmManager).set(anyInt(), anyLong(), eq(mRunInitIntent));
+    }
+
+    @Test
+    public void testRun_whenFinishBackupFails() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportBinder).initializeDevice();
+        verify(mTransportBinder).finishBackup();
+        verify(mBackupManagerService)
+                .recordInitPending(true, mTransportName, mTransport.transportDirName);
+    }
+
+    @Test
+    public void testRun_whenFinishBackupFails_callsObserverAndListenerCorrectly() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mObserver).onResult(eq(mTransportName), eq(TRANSPORT_ERROR));
+        verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+        verify(mListener).onFinished(any());
+    }
+
+    @Test
+    public void testRun_whenFinishBackupFails_logs() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        assertLogcatContains(
+                BackupManagerService.TAG,
+                log -> log.msg.contains("finishBackup()") && log.type >= Log.ERROR);
+    }
+
+    @Test
+    public void testRun_whenInitializeDeviceFails_logs() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        assertLogcatContains(
+                BackupManagerService.TAG,
+                log -> log.msg.contains("initializeDevice()") && log.type >= Log.ERROR);
+    }
+
+    @Test
+    public void testRun_whenFinishBackupFails_schedulesAlarm() throws Exception {
+        setUpTransport(mTransport);
+        configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mAlarmManager).set(anyInt(), anyLong(), eq(mRunInitIntent));
+    }
+
+    @Test
+    public void testRun_whenOnlyOneTransportFails() throws Exception {
+        TransportData transport1 = backupTransport();
+        TransportData transport2 = d2dTransport();
+        List<TransportMock> transportMocks =
+                setUpTransports(mTransportManager, transport1, transport2);
+        configureTransport(transportMocks.get(0).transport, TRANSPORT_ERROR, 0);
+        configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
+        PerformInitializeTask performInitializeTask =
+                createPerformInitializeTask(transport1.transportName, transport2.transportName);
+
+        performInitializeTask.run();
+
+        verify(transportMocks.get(1).transport).initializeDevice();
+        verify(mObserver).onResult(eq(transport1.transportName), eq(TRANSPORT_ERROR));
+        verify(mObserver).onResult(eq(transport2.transportName), eq(TRANSPORT_OK));
+        verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+    }
+
+    @Test
+    public void testRun_withMultipleTransports() throws Exception {
+        List<TransportMock> transportMocks =
+                setUpTransports(
+                        mTransportManager, backupTransport(), d2dTransport(), localTransport());
+        configureTransport(transportMocks.get(0).transport, TRANSPORT_OK, TRANSPORT_OK);
+        configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
+        configureTransport(transportMocks.get(2).transport, TRANSPORT_OK, TRANSPORT_OK);
+        String[] transportNames =
+                Stream.of(new TransportData[] {backupTransport(), d2dTransport(), localTransport()})
+                        .map(t -> t.transportName)
+                        .toArray(String[]::new);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(transportNames);
+
+        performInitializeTask.run();
+
+        Iterator<TransportData> transportsIterator =
+                Arrays.asList(
+                                new TransportData[] {
+                                    backupTransport(), d2dTransport(), localTransport()
+                                })
+                        .iterator();
+        for (TransportMock transportMock : transportMocks) {
+            TransportData transport = transportsIterator.next();
+            verify(mTransportManager).getTransportClient(eq(transport.transportName), any());
+            verify(mTransportManager)
+                    .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        }
+    }
+
+    @Test
+    public void testRun_whenOnlyOneTransportFails_disposesAllTransports() throws Exception {
+        TransportData transport1 = backupTransport();
+        TransportData transport2 = d2dTransport();
+        List<TransportMock> transportMocks =
+                setUpTransports(mTransportManager, transport1, transport2);
+        configureTransport(transportMocks.get(0).transport, TRANSPORT_ERROR, 0);
+        configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
+        PerformInitializeTask performInitializeTask =
+                createPerformInitializeTask(transport1.transportName, transport2.transportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMocks.get(0).transportClient), any());
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMocks.get(1).transportClient), any());
+    }
+
+    @Test
+    public void testRun_whenTransportNotRegistered() throws Exception {
+        setUpTransports(mTransportManager, mTransport.unregistered());
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportManager, never()).disposeOfTransportClient(any(), any());
+        verify(mObserver, never()).onResult(any(), anyInt());
+        verify(mObserver).backupFinished(eq(TRANSPORT_OK));
+    }
+
+    @Test
+    public void testRun_whenOnlyOneTransportNotRegistered() throws Exception {
+        TransportData transport1 = backupTransport().unregistered();
+        TransportData transport2 = d2dTransport();
+        List<TransportMock> transportMocks =
+                setUpTransports(mTransportManager, transport1, transport2);
+        String registeredTransportName = transport2.transportName;
+        IBackupTransport registeredTransport = transportMocks.get(1).transport;
+        TransportClient registeredTransportClient = transportMocks.get(1).transportClient;
+        PerformInitializeTask performInitializeTask =
+                createPerformInitializeTask(transport1.transportName, transport2.transportName);
+
+        performInitializeTask.run();
+
+        verify(registeredTransport).initializeDevice();
+        verify(mTransportManager).disposeOfTransportClient(eq(registeredTransportClient), any());
+        verify(mObserver).onResult(eq(registeredTransportName), eq(TRANSPORT_OK));
+    }
+
+    @Test
+    public void testRun_whenTransportNotAvailable() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport.unavailable());
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+        verify(mListener).onFinished(any());
+    }
+
+    @Test
+    public void testRun_whenTransportThrowsDeadObjectException() throws Exception {
+        TransportMock transportMock = setUpTransport(mTransport);
+        IBackupTransport transport = transportMock.transport;
+        TransportClient transportClient = transportMock.transportClient;
+        when(transport.initializeDevice()).thenThrow(DeadObjectException.class);
+        PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
+
+        performInitializeTask.run();
+
+        verify(mTransportManager).disposeOfTransportClient(eq(transportClient), any());
+        verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+        verify(mListener).onFinished(any());
+    }
+
+    private PerformInitializeTask createPerformInitializeTask(String... transportNames) {
+        return new PerformInitializeTask(
+                mBackupManagerService,
+                mTransportManager,
+                transportNames,
+                mObserver,
+                mListener,
+                mBaseStateDir);
+    }
+
+    private void configureTransport(
+            IBackupTransport transportMock, int initializeDeviceStatus, int finishBackupStatus)
+            throws Exception {
+        when(transportMock.initializeDevice()).thenReturn(initializeDeviceStatus);
+        when(transportMock.finishBackup()).thenReturn(finishBackupStatus);
+    }
+
+    private TransportMock setUpTransport(TransportData transport) throws Exception {
+        TransportMock transportMock =
+                TransportTestUtils.setUpTransport(mTransportManager, transport);
+        mTransportBinder = transportMock.transport;
+        return transportMock;
+    }
+}
diff --git a/com/android/server/backup/internal/RunInitializeReceiver.java b/com/android/server/backup/internal/RunInitializeReceiver.java
new file mode 100644
index 0000000..160124b
--- /dev/null
+++ b/com/android/server/backup/internal/RunInitializeReceiver.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.RUN_INITIALIZE_ACTION;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Slog;
+
+import com.android.server.backup.UserBackupManagerService;
+
+import java.util.Set;
+
+/**
+ * A {@link BroadcastReceiver} for the action {@link UserBackupManagerService#RUN_INITIALIZE_ACTION}
+ * that runs an initialization operation on all pending transports.
+ */
+public class RunInitializeReceiver extends BroadcastReceiver {
+    private final UserBackupManagerService mUserBackupManagerService;
+
+    public RunInitializeReceiver(UserBackupManagerService userBackupManagerService) {
+        mUserBackupManagerService = userBackupManagerService;
+    }
+
+    public void onReceive(Context context, Intent intent) {
+        if (!RUN_INITIALIZE_ACTION.equals(intent.getAction())) {
+            return;
+        }
+
+        synchronized (mUserBackupManagerService.getQueueLock()) {
+            Set<String> pendingInits = mUserBackupManagerService.getPendingInits();
+            if (DEBUG) {
+                Slog.v(TAG, "Running a device init; " + pendingInits.size() + " pending");
+            }
+
+            if (pendingInits.size() > 0) {
+                String[] transports = pendingInits.toArray(new String[pendingInits.size()]);
+                mUserBackupManagerService.clearPendingInits();
+                mUserBackupManagerService.initializeTransports(transports, null);
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/SetupObserver.java b/com/android/server/backup/internal/SetupObserver.java
new file mode 100644
index 0000000..c5e912e
--- /dev/null
+++ b/com/android/server/backup/internal/SetupObserver.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.getSetupCompleteSettingForUser;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.server.backup.KeyValueBackupJob;
+import com.android.server.backup.UserBackupManagerService;
+
+/**
+ * A {@link ContentObserver} for changes to the setting {@link Settings.Secure#USER_SETUP_COMPLETE}
+ * for a particular user.
+ */
+public class SetupObserver extends ContentObserver {
+    private final UserBackupManagerService mUserBackupManagerService;
+    private final Context mContext;
+    private final int mUserId;
+
+    public SetupObserver(UserBackupManagerService userBackupManagerService, Handler handler) {
+        super(handler);
+        mUserBackupManagerService = userBackupManagerService;
+        mContext = userBackupManagerService.getContext();
+        mUserId = userBackupManagerService.getUserId();
+    }
+
+    /**
+     * Callback that executes when the setting {@link Settings.Secure#USER_SETUP_COMPLETE} changes
+     * for the user {@link #mUserId}. If the user is newly setup and backup is enabled, then we
+     * schedule a key value and full backup job for the user. If the user was previously setup and
+     * now the setting has changed to {@code false}, we don't reset the state as having gone through
+     * setup is a non-reversible action.
+     */
+    public void onChange(boolean selfChange) {
+        boolean previousSetupComplete = mUserBackupManagerService.isSetupComplete();
+        boolean newSetupComplete = getSetupCompleteSettingForUser(mContext, mUserId);
+
+        boolean resolvedSetupComplete = previousSetupComplete || newSetupComplete;
+        mUserBackupManagerService.setSetupComplete(resolvedSetupComplete);
+        if (MORE_DEBUG) {
+            Slog.d(
+                    TAG,
+                    "Setup complete change: was="
+                            + previousSetupComplete
+                            + " new="
+                            + newSetupComplete
+                            + " resolved="
+                            + resolvedSetupComplete);
+        }
+
+        synchronized (mUserBackupManagerService.getQueueLock()) {
+            // Start backup if the user is newly setup and backup is enabled.
+            if (resolvedSetupComplete
+                    && !previousSetupComplete
+                    && mUserBackupManagerService.isEnabled()) {
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, "Setup complete so starting backups");
+                }
+                KeyValueBackupJob.schedule(mUserBackupManagerService.getUserId(), mContext,
+                        mUserBackupManagerService.getConstants());
+                mUserBackupManagerService.scheduleNextFullBackupJob(0);
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/internal/SetupObserverTest.java b/com/android/server/backup/internal/SetupObserverTest.java
new file mode 100644
index 0000000..e49425b
--- /dev/null
+++ b/com/android/server/backup/internal/SetupObserverTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 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.google.common.truth.Truth.assertThat;
+
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import com.android.server.backup.FullBackupJob;
+import com.android.server.backup.JobIdManager;
+import com.android.server.backup.KeyValueBackupJob;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.testing.BackupManagerServiceTestUtils;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowJobScheduler;
+
+import java.io.File;
+
+/**
+ * Tests verifying the interaction between {@link SetupObserver} and {@link
+ * UserBackupManagerService}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowApplicationPackageManager.class,
+            ShadowJobScheduler.class,
+            ShadowSystemServiceRegistry.class
+        })
+@Presubmit
+public class SetupObserverTest {
+    private static final String TAG = "SetupObserverTest";
+    private static final int USER_ID = 10;
+
+    @Mock private TransportManager mTransportManager;
+
+    private Context mContext;
+    private UserBackupManagerService mUserBackupManagerService;
+    private HandlerThread mHandlerThread;
+    private ShadowJobScheduler mShadowJobScheduler;
+
+    /** Setup state. */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mHandlerThread = BackupManagerServiceTestUtils.startSilentBackupThread(TAG);
+        mUserBackupManagerService =
+                BackupManagerServiceTestUtils.createUserBackupManagerServiceAndRunTasks(
+                        USER_ID,
+                        mContext,
+                        mHandlerThread,
+                        new File(mContext.getDataDir(), "test1"),
+                        new File(mContext.getDataDir(), "test2"),
+                        mTransportManager);
+        mShadowJobScheduler = Shadows.shadowOf(mContext.getSystemService(JobScheduler.class));
+    }
+
+    /** Test observer handles changes from not setup -> setup correctly. */
+    @Test
+    public void testOnChange_whenNewlySetup_updatesState() throws Exception {
+        SetupObserver setupObserver = new SetupObserver(mUserBackupManagerService, new Handler());
+        mUserBackupManagerService.setSetupComplete(false);
+        changeSetupCompleteSettingForUser(true, USER_ID);
+
+        setupObserver.onChange(true);
+
+        assertThat(mUserBackupManagerService.isSetupComplete()).isTrue();
+    }
+
+    /** Test observer handles changes from setup -> not setup correctly. */
+    @Test
+    public void testOnChange_whenPreviouslySetup_doesNotUpdateState() throws Exception {
+        SetupObserver setupObserver = new SetupObserver(mUserBackupManagerService, new Handler());
+        mUserBackupManagerService.setSetupComplete(true);
+        changeSetupCompleteSettingForUser(false, USER_ID);
+
+        setupObserver.onChange(true);
+
+        assertThat(mUserBackupManagerService.isSetupComplete()).isTrue();
+    }
+
+    /** Test observer handles changes from not setup -> not setup correctly. */
+    @Test
+    public void testOnChange_whenNotPreviouslySetup_doesNotUpdateStateIfNoChange()
+            throws Exception {
+        SetupObserver setupObserver = new SetupObserver(mUserBackupManagerService, new Handler());
+        mUserBackupManagerService.setSetupComplete(false);
+        changeSetupCompleteSettingForUser(false, USER_ID);
+
+        setupObserver.onChange(true);
+
+        assertThat(mUserBackupManagerService.isSetupComplete()).isFalse();
+    }
+
+    /** Test observer handles changes from not setup -> setup correctly. */
+    @Test
+    public void testOnChange_whenNewlySetup_schedulesBackup() throws Exception {
+        SetupObserver setupObserver = new SetupObserver(mUserBackupManagerService, new Handler());
+        mUserBackupManagerService.setSetupComplete(false);
+        changeSetupCompleteSettingForUser(true, USER_ID);
+        // Setup conditions for a full backup job to be scheduled.
+        mUserBackupManagerService.setEnabled(true);
+        mUserBackupManagerService.enqueueFullBackup("testPackage", /* lastBackedUp */ 0);
+
+        setupObserver.onChange(true);
+
+        assertThat(
+                        mShadowJobScheduler.getPendingJob(
+                                getJobIdForUser(
+                                        KeyValueBackupJob.MIN_JOB_ID,
+                                        KeyValueBackupJob.MAX_JOB_ID,
+                                        USER_ID)))
+                .isNotNull();
+        assertThat(
+                        mShadowJobScheduler.getPendingJob(
+                                getJobIdForUser(
+                                        FullBackupJob.MIN_JOB_ID,
+                                        FullBackupJob.MAX_JOB_ID,
+                                        USER_ID)))
+                .isNotNull();
+    }
+
+    private int getJobIdForUser(int min, int max, int userId) {
+        return JobIdManager.getJobIdForUserId(min, max, userId);
+    }
+
+    private void changeSetupCompleteSettingForUser(boolean value, int userId) {
+        Settings.Secure.putIntForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE,
+                value ? 1 : 0,
+                userId);
+    }
+}
diff --git a/com/android/server/backup/keyvalue/AgentException.java b/com/android/server/backup/keyvalue/AgentException.java
new file mode 100644
index 0000000..e2ca351
--- /dev/null
+++ b/com/android/server/backup/keyvalue/AgentException.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+/**
+ * This represents something wrong with a specific package. For example:
+ * <ul>
+ *     <li>Package unknown.
+ *     <li>Package is not eligible for backup anymore.
+ *     <li>Backup agent timed out.
+ *     <li>Backup agent wrote protected keys.
+ *     <li>...
+ * </ul>
+ *
+ * @see KeyValueBackupTask
+ * @see TaskException
+ */
+class AgentException extends BackupException {
+    static AgentException transitory() {
+        return new AgentException(/* transitory */ true);
+    }
+
+    static AgentException transitory(Exception cause) {
+        return new AgentException(/* transitory */ true, cause);
+    }
+
+    static AgentException permanent() {
+        return new AgentException(/* transitory */ false);
+    }
+
+    static AgentException permanent(Exception cause) {
+        return new AgentException(/* transitory */ false, cause);
+    }
+
+    private final boolean mTransitory;
+
+    private AgentException(boolean transitory) {
+        mTransitory = transitory;
+    }
+
+    private AgentException(boolean transitory, Exception cause) {
+        super(cause);
+        mTransitory = transitory;
+    }
+
+    boolean isTransitory() {
+        return mTransitory;
+    }
+}
diff --git a/com/android/server/backup/keyvalue/AgentExceptionTest.java b/com/android/server/backup/keyvalue/AgentExceptionTest.java
new file mode 100644
index 0000000..b00b922
--- /dev/null
+++ b/com/android/server/backup/keyvalue/AgentExceptionTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class AgentExceptionTest {
+    @Test
+    public void testTransitory_isTransitory() {
+        AgentException exception = AgentException.transitory();
+
+        assertThat(exception.isTransitory()).isTrue();
+    }
+
+    @Test
+    public void testTransitory_withCause() {
+        Exception cause = new IOException();
+
+        AgentException exception = AgentException.transitory(cause);
+
+        assertThat(exception.isTransitory()).isTrue();
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+
+    @Test
+    public void testPermanent_isNotTransitory() {
+        AgentException exception = AgentException.permanent();
+
+        assertThat(exception.isTransitory()).isFalse();
+    }
+
+    @Test
+    public void testPermanent_withCause() {
+        Exception cause = new IOException();
+
+        AgentException exception = AgentException.permanent(cause);
+
+        assertThat(exception.isTransitory()).isFalse();
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+}
diff --git a/com/android/server/backup/keyvalue/BackupException.java b/com/android/server/backup/keyvalue/BackupException.java
new file mode 100644
index 0000000..27b2d35
--- /dev/null
+++ b/com/android/server/backup/keyvalue/BackupException.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.util.AndroidException;
+
+/**
+ * Key-value backup task exception.
+ *
+ * @see AgentException
+ * @see TaskException
+ */
+class BackupException extends AndroidException {
+    BackupException() {}
+
+    BackupException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/com/android/server/backup/keyvalue/BackupExceptionTest.java b/com/android/server/backup/keyvalue/BackupExceptionTest.java
new file mode 100644
index 0000000..d5603d6
--- /dev/null
+++ b/com/android/server/backup/keyvalue/BackupExceptionTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class BackupExceptionTest {
+    @Test
+    public void testConstructor_passesCause() {
+        Exception cause = new IOException();
+
+        Exception exception = new BackupException(cause);
+
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+}
diff --git a/com/android/server/backup/keyvalue/BackupRequest.java b/com/android/server/backup/keyvalue/BackupRequest.java
new file mode 100644
index 0000000..67b2f72
--- /dev/null
+++ b/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/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
new file mode 100644
index 0000000..4632cb0
--- /dev/null
+++ b/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
@@ -0,0 +1,424 @@
+/*
+ * 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 android.annotation.Nullable;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.content.pm.PackageInfo;
+import android.util.EventLog;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.EventLogTags;
+import com.android.server.backup.BackupManagerService;
+import com.android.server.backup.DataChangedJournal;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.remote.RemoteResult;
+import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupObserverUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+/**
+ * Reports events that happen during a key-value backup task to:
+ *
+ * <ul>
+ *   <li>Logcat (main and event buffers).
+ *   <li>Backup observer (see {@link IBackupObserver}).
+ *   <li>Backup manager monitor (see {@link IBackupManagerMonitor}).
+ * </ul>
+ */
+// TODO: In KeyValueBackupTaskTest, remove direct assertions on logcat, observer or monitor and
+//       verify calls to this object. Add these and more assertions to the test of this class.
+@VisibleForTesting
+public class KeyValueBackupReporter {
+    @VisibleForTesting static final String TAG = "KeyValueBackupTask";
+    private static final boolean DEBUG = BackupManagerService.DEBUG;
+    @VisibleForTesting static final boolean MORE_DEBUG = BackupManagerService.MORE_DEBUG;
+
+    static void onNewThread(String threadName) {
+        if (DEBUG) {
+            Slog.d(TAG, "Spinning thread " + threadName);
+        }
+    }
+
+    private final UserBackupManagerService mBackupManagerService;
+    private final IBackupObserver mObserver;
+    @Nullable private IBackupManagerMonitor mMonitor;
+
+    KeyValueBackupReporter(
+            UserBackupManagerService backupManagerService,
+            IBackupObserver observer,
+            @Nullable IBackupManagerMonitor monitor) {
+        mBackupManagerService = backupManagerService;
+        mObserver = observer;
+        mMonitor = monitor;
+    }
+
+    /** Returns the monitor or {@code null} if we lost connection to it. */
+    @Nullable
+    IBackupManagerMonitor getMonitor() {
+        return mMonitor;
+    }
+
+    IBackupObserver getObserver() {
+        return mObserver;
+    }
+
+    void onSkipBackup() {
+        if (DEBUG) {
+            Slog.d(TAG, "Skipping backup since one is already in progress");
+        }
+    }
+
+    void onEmptyQueueAtStart() {
+        Slog.w(TAG, "Backup begun with an empty queue, nothing to do");
+    }
+
+    void onQueueReady(List<String> queue) {
+        if (DEBUG) {
+            Slog.v(TAG, "Beginning backup of " + queue.size() + " targets");
+        }
+    }
+
+    void onTransportReady(String transportName) {
+        EventLog.writeEvent(EventLogTags.BACKUP_START, transportName);
+    }
+
+    void onInitializeTransport(String transportName) {
+        Slog.i(TAG, "Initializing transport and resetting backup state");
+    }
+
+    void onTransportInitialized(int status) {
+        if (status == BackupTransport.TRANSPORT_OK) {
+            EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
+        } else {
+            EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
+            Slog.e(TAG, "Transport error in initializeDevice()");
+        }
+    }
+
+    void onInitializeTransportError(Exception e) {
+        Slog.e(TAG, "Error during initialization", e);
+    }
+
+    void onSkipPm() {
+        Slog.d(TAG, "Skipping backup of PM metadata");
+    }
+
+    void onExtractPmAgentDataError(Exception e) {
+        Slog.e(TAG, "Error during PM metadata backup", e);
+    }
+
+    void onStartPackageBackup(String packageName) {
+        Slog.d(TAG, "Starting key-value backup of " + packageName);
+    }
+
+    void onPackageNotEligibleForBackup(String packageName) {
+        Slog.i(TAG, "Package " + packageName + " no longer supports backup, skipping");
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+    }
+
+    void onPackageEligibleForFullBackup(String packageName) {
+        Slog.i(
+                TAG,
+                "Package " + packageName + " performs full-backup rather than key-value, skipping");
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+    }
+
+    void onPackageStopped(String packageName) {
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+    }
+
+    void onAgentUnknown(String packageName) {
+        Slog.d(TAG, "Package does not exist, skipping");
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_PACKAGE_NOT_FOUND);
+    }
+
+    void onBindAgentError(String packageName, SecurityException e) {
+        Slog.d(TAG, "Error in bind/backup", e);
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
+    }
+
+    void onAgentError(String packageName) {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "Agent failure for " + packageName + ", re-staging");
+        }
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
+    }
+
+    void onExtractAgentData(String packageName) {
+        if (DEBUG) {
+            Slog.d(TAG, "Invoking agent on " + packageName);
+        }
+    }
+
+    void onAgentFilesReady(File backupDataFile) {
+        if (MORE_DEBUG) {
+            Slog.d(TAG, "Data file: " + backupDataFile);
+        }
+    }
+
+    void onRestoreconFailed(File backupDataFile) {
+        Slog.e(TAG, "SELinux restorecon failed on " + backupDataFile);
+    }
+
+    void onCallAgentDoBackupError(String packageName, boolean callingAgent, Exception e) {
+        if (callingAgent) {
+            Slog.e(TAG, "Error invoking agent on " + packageName + ": " + e);
+            BackupObserverUtils.sendBackupOnPackageResult(
+                    mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
+        } else {
+            Slog.e(TAG, "Error before invoking agent on " + packageName + ": " + e);
+        }
+        EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, e.toString());
+    }
+
+    void onFailAgentError(String packageName) {
+        Slog.w(TAG, "Error conveying failure to " + packageName);
+    }
+
+    void onAgentIllegalKey(PackageInfo packageInfo, String key) {
+        String packageName = packageInfo.packageName;
+        EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, "bad key");
+        mMonitor =
+                BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY,
+                        packageInfo,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        BackupManagerMonitorUtils.putMonitoringExtra(
+                                null, BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, key));
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
+        if (MORE_DEBUG) {
+            Slog.i(
+                    TAG,
+                    "Agent failure for " + packageName + " with illegal key " + key + ", dropped");
+        }
+    }
+
+    void onAgentDataError(String packageName, IOException e) {
+        Slog.w(TAG, "Unable to read/write agent data for " + packageName + ": " + e);
+    }
+
+    void onDigestError(NoSuchAlgorithmException e) {
+        Slog.e(TAG, "Unable to use SHA-1!");
+    }
+
+    void onWriteWidgetData(boolean priorStateExists, @Nullable byte[] widgetState) {
+        if (MORE_DEBUG) {
+            Slog.i(
+                    TAG,
+                    "Checking widget update: state="
+                            + (widgetState != null)
+                            + " prior="
+                            + priorStateExists);
+        }
+    }
+
+    void onTransportPerformBackup(String packageName) {
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "Sending non-empty data to transport for " + packageName);
+        }
+    }
+
+    void onEmptyData(PackageInfo packageInfo) {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "No backup data written, not calling transport");
+        }
+        mMonitor =
+                BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND,
+                        packageInfo,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        null);
+    }
+
+    void onPackageBackupComplete(String packageName, long size) {
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.SUCCESS);
+        EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, packageName, size);
+        mBackupManagerService.logBackupComplete(packageName);
+    }
+
+    void onPackageBackupRejected(String packageName) {
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
+        EventLogTags.writeBackupAgentFailure(packageName, "Transport rejected");
+    }
+
+    void onPackageBackupQuotaExceeded(String packageName) {
+        if (MORE_DEBUG) {
+            Slog.d(TAG, "Package " + packageName + " hit quota limit on key-value backup");
+        }
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
+        EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, packageName);
+    }
+
+    void onAgentDoQuotaExceededError(Exception e) {
+        Slog.e(TAG, "Unable to notify about quota exceeded: " + e);
+    }
+
+    void onPackageBackupNonIncrementalRequired(PackageInfo packageInfo) {
+        Slog.i(TAG, "Transport lost data, retrying package");
+        BackupManagerMonitorUtils.monitorEvent(
+                mMonitor,
+                BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED,
+                packageInfo,
+                BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+                /* extras */ null);
+    }
+
+    void onPackageBackupNonIncrementalAndNonIncrementalRequired(String packageName) {
+        Slog.e(TAG, "Transport requested non-incremental but already the case");
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED);
+        EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, packageName);
+    }
+
+    void onPackageBackupTransportFailure(String packageName) {
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED);
+        EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, packageName);
+    }
+
+    void onPackageBackupTransportError(String packageName, Exception e) {
+        Slog.e(TAG, "Transport error backing up " + packageName, e);
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_TRANSPORT_ABORTED);
+        EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, packageName);
+    }
+
+    void onCloseFileDescriptorError(String logName) {
+        Slog.w(TAG, "Error closing " + logName + " file-descriptor");
+    }
+
+    void onCancel() {
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "Cancel received");
+        }
+    }
+
+    void onAgentTimedOut(@Nullable PackageInfo packageInfo) {
+        String packageName = getPackageName(packageInfo);
+        Slog.i(TAG, "Agent " + packageName + " timed out");
+        EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
+        // Time-out used to be implemented as cancel w/ cancelAll = false.
+        // TODO: Change monitoring event to reflect time-out as an event itself.
+        mMonitor =
+                BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
+                        packageInfo,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                        BackupManagerMonitorUtils.putMonitoringExtra(
+                                null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false));
+    }
+
+    void onAgentCancelled(@Nullable PackageInfo packageInfo) {
+        String packageName = getPackageName(packageInfo);
+        Slog.i(TAG, "Cancel backing up " + packageName);
+        EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
+        mMonitor =
+                BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
+                        packageInfo,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                        BackupManagerMonitorUtils.putMonitoringExtra(
+                                null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true));
+    }
+
+    void onAgentResultError(@Nullable PackageInfo packageInfo) {
+        String packageName = getPackageName(packageInfo);
+        BackupObserverUtils.sendBackupOnPackageResult(
+                mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
+        EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, "result error");
+        Slog.w(TAG, "Agent " + packageName + " error in onBackup()");
+    }
+
+    private String getPackageName(@Nullable PackageInfo packageInfo) {
+        return (packageInfo != null) ? packageInfo.packageName : "no_package_yet";
+    }
+
+    void onRevertTask() {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "Reverting backup queue, re-staging everything");
+        }
+    }
+
+    void onTransportRequestBackupTimeError(Exception e) {
+        Slog.w(TAG, "Unable to contact transport for recommended backoff: " + e);
+    }
+
+    void onRemoteCallReturned(RemoteResult result, String logIdentifier) {
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "Agent call " + logIdentifier + " returned " + result);
+        }
+    }
+
+    void onJournalDeleteFailed(DataChangedJournal journal) {
+        Slog.e(TAG, "Unable to remove backup journal file " + journal);
+    }
+
+    void onSetCurrentTokenError(Exception e) {
+        Slog.e(TAG, "Transport threw reporting restore set: " + e);
+    }
+
+    void onTransportNotInitialized(@Nullable String transportName) {
+        EventLog.writeEvent(EventLogTags.BACKUP_RESET, transportName);
+        if (MORE_DEBUG) {
+            Slog.d(TAG, "Transport requires initialization, rerunning");
+        }
+    }
+
+    void onPendingInitializeTransportError(Exception e) {
+        Slog.w(TAG, "Failed to query transport name for pending init: " + e);
+    }
+
+    /**
+     * This is a bit different from {@link #onTaskFinished()}, it's only called if there is no
+     * full-backup requests associated with the key-value task.
+     */
+    void onBackupFinished(int status) {
+        BackupObserverUtils.sendBackupFinished(mObserver, status);
+    }
+
+    void onStartFullBackup(List<String> pendingFullBackups) {
+        Slog.d(TAG, "Starting full backups for: " + pendingFullBackups);
+    }
+
+    void onTaskFinished() {
+        Slog.i(TAG, "K/V backup pass finished");
+    }
+}
diff --git a/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java b/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
new file mode 100644
index 0000000..14b4dc3
--- /dev/null
+++ b/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 com.android.server.backup.keyvalue.KeyValueBackupReporter.TAG;
+import static com.android.server.backup.testing.TestUtils.assertLogcat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.testing.shadows.ShadowEventLog;
+import com.android.server.testing.shadows.ShadowSlog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowEventLog.class, ShadowSlog.class})
+@Presubmit
+public class KeyValueBackupReporterTest {
+    @Mock private UserBackupManagerService mBackupManagerService;
+    @Mock private IBackupObserver mObserver;
+    @Mock private IBackupManagerMonitor mMonitor;
+
+    private KeyValueBackupReporter mReporter;
+
+    @Before
+    public void setUp() {
+        mReporter = new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor);
+    }
+
+    @Test
+    public void testMoreDebug_isFalse() {
+        assertThat(KeyValueBackupReporter.MORE_DEBUG).isFalse();
+    }
+
+    @Test
+    public void testOnNewThread_logsCorrectly() {
+        KeyValueBackupReporter.onNewThread("foo");
+
+        assertLogcat(TAG, Log.DEBUG);
+    }
+
+    @Test
+    public void testGetMonitor_returnsMonitor() {
+        IBackupManagerMonitor monitor = mReporter.getMonitor();
+
+        assertThat(monitor).isEqualTo(mMonitor);
+    }
+
+    @Test
+    public void testGetObserver_returnsObserver() {
+        IBackupObserver observer = mReporter.getObserver();
+
+        assertThat(observer).isEqualTo(mObserver);
+    }
+
+    /**
+     * Ensure that EventLog is called when logging the transport uninitialised issue.
+     */
+    @Test
+    public void testOnTransportNotInitialized_callsEventLog() {
+        ShadowEventLog.setUp();
+
+        mReporter.onTransportNotInitialized("transport");
+
+        assertThat(ShadowEventLog.getEntries().size()).isEqualTo(1);
+    }
+}
diff --git a/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/com/android/server/backup/keyvalue/KeyValueBackupTask.java
new file mode 100644
index 0000000..5e10916
--- /dev/null
+++ b/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -0,0 +1,1323 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.backup.keyvalue;
+
+import static android.app.ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL;
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
+
+import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
+import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.IBackupAgent;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupManager;
+import android.app.backup.BackupTransport;
+import android.app.backup.IBackupCallback;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.ConditionVariable;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SELinux;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.util.Preconditions;
+import com.android.server.AppWidgetBackupBridge;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.DataChangedJournal;
+import com.android.server.backup.KeyValueBackupJob;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.fullbackup.PerformFullTransportBackupTask;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.internal.Operation;
+import com.android.server.backup.remote.RemoteCall;
+import com.android.server.backup.remote.RemoteCallable;
+import com.android.server.backup.remote.RemoteResult;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.backup.utils.AppBackupUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Represents the task of performing a sequence of key-value backups for a given list of packages.
+ * Method {@link #run()} executes the backups to the transport specified via the {@code
+ * transportClient} parameter in the constructor.
+ *
+ * <p>A few definitions:
+ *
+ * <ul>
+ *   <li>State directory: {@link UserBackupManagerService#getBaseStateDir()}/&lt;transport&gt;
+ *   <li>State file: {@link
+ *       UserBackupManagerService#getBaseStateDir()}/&lt;transport&gt;/&lt;package&gt;<br>
+ *       Represents the state of the backup data for a specific package in the current dataset.
+ *   <li>Stage directory: {@link UserBackupManagerService#getDataDir()}
+ *   <li>Stage file: {@link UserBackupManagerService#getDataDir()}/&lt;package&gt;.data<br>
+ *       Contains staged data that the agents wrote via {@link BackupDataOutput}, to be transmitted
+ *       to the transport.
+ * </ul>
+ *
+ * If there is no PackageManager (PM) pseudo-package state file in the state directory, the
+ * specified transport will be initialized with {@link IBackupTransport#initializeDevice()}.
+ *
+ * <p>The PM pseudo-package is the first package to be backed-up and sent to the transport in case
+ * of incremental choice. If non-incremental, PM will only be backed-up if specified in the queue,
+ * and if it's the case it will be re-positioned at the head of the queue.
+ *
+ * <p>Before starting, this task will register itself in {@link UserBackupManagerService} current
+ * operations.
+ *
+ * <p>In summary, this task will for each package:
+ *
+ * <ul>
+ *   <li>Bind to its {@link IBackupAgent}.
+ *   <li>Request transport quota and flags.
+ *   <li>Call {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor,
+ *       ParcelFileDescriptor, long, IBackupCallback, int)} via {@link RemoteCall} passing the
+ *       old state file descriptor (read), the backup data file descriptor (write), the new state
+ *       file descriptor (write), the quota and the transport flags. This will call {@link
+ *       BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)} with
+ *       the old state file to be read, a {@link BackupDataOutput} object to write the backup data
+ *       and the new state file to write. By writing to {@link BackupDataOutput}, the agent will
+ *       write data to the stage file. The task will block waiting for either:
+ *       <ul>
+ *         <li>Agent response.
+ *         <li>Agent time-out (specified via {@link
+ *             UserBackupManagerService#getAgentTimeoutParameters()}.
+ *         <li>External cancellation or thread interrupt.
+ *       </ul>
+ *   <li>Unbind the agent.
+ *   <li>Assuming agent response, send the staged data that the agent wrote to disk to the transport
+ *       via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}.
+ *   <li>Call {@link IBackupTransport#finishBackup()} if previous call was successful.
+ *   <li>Save the new state in the state file. During the agent call it was being written to
+ *       &lt;state file&gt;.new, here we rename it and replace the old one.
+ *   <li>Delete the stage file.
+ * </ul>
+ *
+ * In the end, this task will:
+ *
+ * <ul>
+ *   <li>Mark data-changed for the remaining packages in the queue (skipped packages).
+ *   <li>Delete the {@link DataChangedJournal} provided. Note that this should not be the current
+ *       journal.
+ *   <li>Set {@link UserBackupManagerService} current token as {@link
+ *       IBackupTransport#getCurrentRestoreSet()}, if applicable.
+ *   <li>Add the transport to the list of transports pending initialization ({@link
+ *       UserBackupManagerService#getPendingInits()}) and kick-off initialization if the transport
+ *       ever returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}.
+ *   <li>Unregister the task in current operations.
+ *   <li>Release the wakelock.
+ *   <li>Kick-off {@link PerformFullTransportBackupTask} if a list of full-backup packages was
+ *       provided.
+ * </ul>
+ *
+ * The caller can specify whether this should be an incremental or non-incremental backup. In the
+ * case of non-incremental the agents will be passed an empty old state file, which signals that a
+ * complete backup should be performed.
+ *
+ * <p>This task is designed to run on a dedicated thread, with the exception of the {@link
+ * #handleCancel(boolean)} method, which can be called from any thread.
+ */
+// TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks)
+// TODO: Consider having the caller responsible for some clean-up (like resetting state)
+// TODO: Distinguish between cancel and time-out where possible for logging/monitoring/observing
+public class KeyValueBackupTask implements BackupRestoreTask, Runnable {
+    private static final String TAG = "KVBT";
+
+    private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND;
+    private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
+    private static final String BLANK_STATE_FILE_NAME = "blank_state";
+    private static final String PM_PACKAGE = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
+    private static final String SUCCESS_STATE_SUBDIR = "backing-up";
+    @VisibleForTesting static final String NO_DATA_END_SENTINEL = "@end@";
+    @VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data";
+    @VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new";
+
+    /**
+     * Creates a new {@link KeyValueBackupTask} for key-value backup operation, spins up a new
+     * dedicated thread and kicks off the operation in it.
+     *
+     * @param backupManagerService The {@link UserBackupManagerService} instance.
+     * @param transportClient The {@link TransportClient} that contains the transport used for the
+     *     operation.
+     * @param transportDirName The value of {@link IBackupTransport#transportDirName()} for the
+     *     transport whose {@link TransportClient} was provided above.
+     * @param queue The list of package names that will be backed-up.
+     * @param dataChangedJournal The old data-changed journal file that will be deleted when the
+     *     operation finishes (successfully or not) or {@code null}.
+     * @param observer A {@link IBackupObserver}.
+     * @param monitor A {@link IBackupManagerMonitor}.
+     * @param listener A {@link OnTaskFinishedListener} or {@code null}.
+     * @param pendingFullBackups The list of packages that will be passed for a new {@link
+     *     PerformFullTransportBackupTask} operation, which will be started when this finishes.
+     * @param userInitiated Whether this was user-initiated or not.
+     * @param nonIncremental If {@code true}, this will be a complete backup for each package,
+     *     otherwise it will be just an incremental one over the current dataset.
+     * @return The {@link KeyValueBackupTask} that was started.
+     */
+    public static KeyValueBackupTask start(
+            UserBackupManagerService backupManagerService,
+            TransportClient transportClient,
+            String transportDirName,
+            List<String> queue,
+            @Nullable DataChangedJournal dataChangedJournal,
+            IBackupObserver observer,
+            @Nullable IBackupManagerMonitor monitor,
+            OnTaskFinishedListener listener,
+            List<String> pendingFullBackups,
+            boolean userInitiated,
+            boolean nonIncremental) {
+        KeyValueBackupReporter reporter =
+                new KeyValueBackupReporter(backupManagerService, observer, monitor);
+        KeyValueBackupTask task =
+                new KeyValueBackupTask(
+                        backupManagerService,
+                        transportClient,
+                        transportDirName,
+                        queue,
+                        dataChangedJournal,
+                        reporter,
+                        listener,
+                        pendingFullBackups,
+                        userInitiated,
+                        nonIncremental);
+        Thread thread = new Thread(task, "key-value-backup-" + THREAD_COUNT.incrementAndGet());
+        thread.start();
+        KeyValueBackupReporter.onNewThread(thread.getName());
+        return task;
+    }
+
+    private final UserBackupManagerService mBackupManagerService;
+    private final PackageManager mPackageManager;
+    private final TransportClient mTransportClient;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+    private final KeyValueBackupReporter mReporter;
+    private final OnTaskFinishedListener mTaskFinishedListener;
+    private final boolean mUserInitiated;
+    private final int mCurrentOpToken;
+    private final int mUserId;
+    private final File mStateDirectory;
+    private final File mDataDirectory;
+    private final File mBlankStateFile;
+    private final List<String> mOriginalQueue;
+    private final List<String> mQueue;
+    private final List<String> mPendingFullBackups;
+    private final Object mQueueLock;
+    @Nullable private final DataChangedJournal mJournal;
+
+    @Nullable private PerformFullTransportBackupTask mFullBackupTask;
+    @Nullable private IBackupAgent mAgent;
+    @Nullable private PackageInfo mCurrentPackage;
+    @Nullable private File mSavedStateFile;
+    @Nullable private File mBackupDataFile;
+    @Nullable private File mNewStateFile;
+    @Nullable private ParcelFileDescriptor mSavedState;
+    @Nullable private ParcelFileDescriptor mBackupData;
+    @Nullable private ParcelFileDescriptor mNewState;
+    // Indicates whether there was any data to be backed up, i.e. the queue was not empty
+    // and at least one of the packages had data. Used to avoid updating current token for
+    // empty backups.
+    private boolean mHasDataToBackup;
+    private boolean mNonIncremental;
+
+    /**
+     * This {@link ConditionVariable} is used to signal that the cancel operation has been
+     * received by the task and that no more transport calls will be made. Anyone can call {@link
+     * ConditionVariable#block()} to wait for these conditions to hold true, but there should only
+     * be one place where {@link ConditionVariable#open()} is called. Also there should be no calls
+     * to {@link ConditionVariable#close()}, which means there is only one cancel per backup -
+     * subsequent calls to block will return immediately.
+     */
+    private final ConditionVariable mCancelAcknowledged = new ConditionVariable(false);
+
+    /**
+     * Set it to {@code true} and block on {@code mCancelAcknowledged} to wait for the cancellation.
+     * DO NOT set it to {@code false}.
+     */
+    private volatile boolean mCancelled = false;
+
+    /**
+     * If non-{@code null} there is a pending agent call being made. This call can be cancelled (and
+     * control returned to this task) with {@link RemoteCall#cancel()}.
+     */
+    @Nullable private volatile RemoteCall mPendingCall;
+
+    @VisibleForTesting
+    public KeyValueBackupTask(
+            UserBackupManagerService backupManagerService,
+            TransportClient transportClient,
+            String transportDirName,
+            List<String> queue,
+            @Nullable DataChangedJournal journal,
+            KeyValueBackupReporter reporter,
+            OnTaskFinishedListener taskFinishedListener,
+            List<String> pendingFullBackups,
+            boolean userInitiated,
+            boolean nonIncremental) {
+        mBackupManagerService = backupManagerService;
+        mPackageManager = backupManagerService.getPackageManager();
+        mTransportClient = transportClient;
+        mOriginalQueue = queue;
+        // We need to retain the original queue contents in case of transport failure
+        mQueue = new ArrayList<>(queue);
+        mJournal = journal;
+        mReporter = reporter;
+        mTaskFinishedListener = taskFinishedListener;
+        mPendingFullBackups = pendingFullBackups;
+        mUserInitiated = userInitiated;
+        mNonIncremental = nonIncremental;
+        mAgentTimeoutParameters =
+                Objects.requireNonNull(
+                        backupManagerService.getAgentTimeoutParameters(),
+                        "Timeout parameters cannot be null");
+        mStateDirectory = new File(backupManagerService.getBaseStateDir(), transportDirName);
+        mDataDirectory = mBackupManagerService.getDataDir();
+        mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
+        mQueueLock = mBackupManagerService.getQueueLock();
+        mBlankStateFile = new File(mStateDirectory, BLANK_STATE_FILE_NAME);
+        mUserId = backupManagerService.getUserId();
+    }
+
+    private void registerTask() {
+        mBackupManagerService.putOperation(
+                mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
+    }
+
+    private void unregisterTask() {
+        mBackupManagerService.removeOperation(mCurrentOpToken);
+    }
+
+    @Override
+    public void run() {
+        Process.setThreadPriority(THREAD_PRIORITY);
+
+        mHasDataToBackup = false;
+
+        Set<String> backedUpApps = new HashSet<>();
+        int status = BackupTransport.TRANSPORT_OK;
+        try {
+            startTask();
+            while (!mQueue.isEmpty() && !mCancelled) {
+                String packageName = mQueue.remove(0);
+                try {
+                    if (PM_PACKAGE.equals(packageName)) {
+                        backupPm();
+                    } else {
+                        backupPackage(packageName);
+                    }
+                    setSuccessState(packageName, true);
+                    backedUpApps.add(packageName);
+                } catch (AgentException e) {
+                    setSuccessState(packageName, false);
+                    if (e.isTransitory()) {
+                        // We try again this package in the next backup pass.
+                        mBackupManagerService.dataChangedImpl(packageName);
+                    }
+                }
+            }
+
+            informTransportOfUnchangedApps(backedUpApps);
+        } catch (TaskException e) {
+            if (e.isStateCompromised()) {
+                mBackupManagerService.resetBackupState(mStateDirectory);
+            }
+            revertTask();
+            status = e.getStatus();
+        }
+        finishTask(status);
+    }
+
+    /**
+     * Tell the transport about all of the packages which have successfully backed up but
+     * have not informed the framework that they have new data. This allows transports to
+     * differentiate between packages which are not backing data up due to an error and
+     * packages which are not backing up data because nothing has changed.
+     *
+     * The current implementation involves creating a state file when a backup succeeds,
+     * on subsequent runs the existence of the file indicates the backup ran successfully
+     * but there was no data. If a backup fails with an error, or if the package is not
+     * eligible for backup by the transport any more, the status file is removed and the
+     * "no data" message will not be sent to the transport until another successful data
+     * changed backup has succeeded.
+     *
+     * @param appsBackedUp The Set of apps backed up during this run so we can exclude them
+     *                     from the list of successfully backed up apps that we signal to
+     *                     the transport have no data.
+     */
+    private void informTransportOfUnchangedApps(Set<String> appsBackedUp) {
+        String[] succeedingPackages = getSucceedingPackages();
+        if (succeedingPackages == null) {
+            // Nothing is succeeding, so end early.
+            return;
+        }
+
+        int flags = BackupTransport.FLAG_DATA_NOT_CHANGED;
+        if (mUserInitiated) {
+            flags |= BackupTransport.FLAG_USER_INITIATED;
+        }
+
+        boolean noDataPackageEncountered = false;
+        try {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow("KVBT.informTransportOfEmptyBackups()");
+
+            for (String packageName : succeedingPackages) {
+                if (appsBackedUp.contains(packageName)) {
+                    Log.v(TAG, "Skipping package which was backed up this time :" + packageName);
+                    // Skip packages we backed up in this run.
+                    continue;
+                }
+
+                PackageInfo packageInfo;
+                try {
+                    packageInfo = mPackageManager.getPackageInfo(packageName, /* flags */ 0);
+                    if (!isEligibleForNoDataCall(packageInfo)) {
+                        // If the package isn't eligible any more we can forget about it and move
+                        // on.
+                        clearStatus(packageName);
+                        continue;
+                    }
+                } catch (PackageManager.NameNotFoundException e) {
+                    // If the package has been uninstalled we can forget about it and move on.
+                    clearStatus(packageName);
+                    continue;
+                }
+
+                sendNoDataChangedTo(transport, packageInfo, flags);
+                noDataPackageEncountered = true;
+            }
+
+            if (noDataPackageEncountered) {
+                // If we've notified the transport of an unchanged package we need to
+                // tell it that it's seen all of the unchanged packages. We do this by
+                // reporting the end sentinel package as unchanged.
+                PackageInfo endSentinal = new PackageInfo();
+                endSentinal.packageName = NO_DATA_END_SENTINEL;
+                sendNoDataChangedTo(transport, endSentinal, flags);
+            }
+        } catch (TransportNotAvailableException | RemoteException e) {
+            Log.e(TAG, "Could not inform transport of all unchanged apps", e);
+        }
+    }
+
+    /** Determine if a package is eligible to be backed up to the transport */
+    private boolean isEligibleForNoDataCall(PackageInfo packageInfo) {
+        return AppBackupUtils.appIsKeyValueOnly(packageInfo)
+                && AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(mTransportClient,
+                packageInfo.packageName, mPackageManager, mUserId);
+    }
+
+    /** Send the "no data changed" message to a transport for a specific package */
+    private void sendNoDataChangedTo(IBackupTransport transport, PackageInfo packageInfo, int flags)
+            throws RemoteException {
+        ParcelFileDescriptor pfd;
+        try {
+            pfd = ParcelFileDescriptor.open(mBlankStateFile, MODE_READ_ONLY | MODE_CREATE);
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "Unable to find blank state file, aborting unchanged apps signal.");
+            return;
+        }
+        try {
+            int result = transport.performBackup(packageInfo, pfd, flags);
+            if (result == BackupTransport.TRANSPORT_ERROR
+                    || result == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
+                Log.w(
+                        TAG,
+                        "Aborting informing transport of unchanged apps, transport" + " errored");
+                return;
+            }
+
+            transport.finishBackup();
+        } finally {
+            IoUtils.closeQuietly(pfd);
+        }
+    }
+
+    /** Get the list of package names which are marked as having previously succeeded */
+    private String[] getSucceedingPackages() {
+        File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ false);
+        if (stateDirectory == null) {
+            // getSuccessStateFileFor logs when we can't use the state area
+            return null;
+        }
+
+        return stateDirectory.list();
+    }
+
+    /** Sets the indicator that a package backup is succeeding */
+    private void setSuccessState(String packageName, boolean success) {
+        File successStateFile = getSuccessStateFileFor(packageName);
+        if (successStateFile == null) {
+            // The error will have been logged by getSuccessStateFileFor().
+            return;
+        }
+
+        if (successStateFile.exists() != success) {
+            // If there's been a change of state
+            if (!success) {
+                // Clear the status if we're now failing
+                clearStatus(packageName, successStateFile);
+                return;
+            }
+
+            // For succeeding packages we want the file
+            try {
+                if (!successStateFile.createNewFile()) {
+                    Log.w(TAG, "Unable to permanently record success for " + packageName);
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to permanently record success for " + packageName, e);
+            }
+        }
+    }
+
+    /** Clear the status file for a specific package */
+    private void clearStatus(String packageName) {
+        File successStateFile = getSuccessStateFileFor(packageName);
+        if (successStateFile == null) {
+            // The error will have been logged by getSuccessStateFileFor().
+            return;
+        }
+        clearStatus(packageName, successStateFile);
+    }
+
+    /** Clear the status file for a package once we have the File representation */
+    private void clearStatus(String packageName, File successStateFile) {
+        if (successStateFile.exists()) {
+            if (!successStateFile.delete()) {
+                Log.w(TAG, "Unable to remove status file for " + packageName);
+            }
+        }
+    }
+
+    /** Get the backup state file for a package **/
+    private File getSuccessStateFileFor(String packageName) {
+        File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ true);
+        return stateDirectory == null ? null : new File(stateDirectory, packageName);
+    }
+
+    /** The top level directory for success state files */
+    private File getTopLevelSuccessStateDirectory(boolean createIfMissing) {
+        File directory = new File(mStateDirectory, SUCCESS_STATE_SUBDIR);
+        if (!directory.exists() && createIfMissing && !directory.mkdirs()) {
+            Log.e(TAG, "Unable to create backing-up state directory");
+            return null;
+        }
+        return directory;
+    }
+
+    /** Returns transport status. */
+    private int sendDataToTransport(@Nullable PackageInfo packageInfo)
+            throws AgentException, TaskException {
+        try {
+            return sendDataToTransport();
+        } catch (IOException e) {
+            mReporter.onAgentDataError(packageInfo.packageName, e);
+            throw TaskException.causedBy(e);
+        }
+    }
+
+    @Override
+    public void execute() {}
+
+    @Override
+    public void operationComplete(long unusedResult) {}
+
+    private void startTask() throws TaskException {
+        if (mBackupManagerService.isBackupOperationInProgress()) {
+            mReporter.onSkipBackup();
+            throw TaskException.create();
+        }
+
+        // Unfortunately full backup task constructor registers the task with BMS, so we have to
+        // create it here instead of in our constructor.
+        mFullBackupTask = createFullBackupTask(mPendingFullBackups);
+        registerTask();
+
+        if (mQueue.isEmpty() && mPendingFullBackups.isEmpty()) {
+            mReporter.onEmptyQueueAtStart();
+            return;
+        }
+        // We only backup PM if it was explicitly in the queue or if it's incremental.
+        boolean backupPm = mQueue.remove(PM_PACKAGE) || !mNonIncremental;
+        if (backupPm) {
+            mQueue.add(0, PM_PACKAGE);
+        } else {
+            mReporter.onSkipPm();
+        }
+
+        mReporter.onQueueReady(mQueue);
+        File pmState = new File(mStateDirectory, PM_PACKAGE);
+        try {
+            IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.startTask()");
+            String transportName = transport.name();
+            if (transportName.contains("EncryptedLocalTransport")) {
+                // Temporary code for EiTF POC. Only supports non-incremental backups.
+                mNonIncremental = true;
+            }
+
+            mReporter.onTransportReady(transportName);
+
+            // If we haven't stored PM metadata yet, we must initialize the transport.
+            if (pmState.length() <= 0) {
+                mReporter.onInitializeTransport(transportName);
+                mBackupManagerService.resetBackupState(mStateDirectory);
+                int status = transport.initializeDevice();
+                mReporter.onTransportInitialized(status);
+                if (status != BackupTransport.TRANSPORT_OK) {
+                    throw TaskException.stateCompromised();
+                }
+            }
+        } catch (TaskException e) {
+            throw e;
+        } catch (Exception e) {
+            mReporter.onInitializeTransportError(e);
+            throw TaskException.stateCompromised();
+        }
+    }
+
+    private PerformFullTransportBackupTask createFullBackupTask(List<String> packages) {
+        return new PerformFullTransportBackupTask(
+                mBackupManagerService,
+                mTransportClient,
+                /* fullBackupRestoreObserver */ null,
+                packages.toArray(new String[packages.size()]),
+                /* updateSchedule */ false,
+                /* runningJob */ null,
+                new CountDownLatch(1),
+                mReporter.getObserver(),
+                mReporter.getMonitor(),
+                mTaskFinishedListener,
+                mUserInitiated);
+    }
+
+    private void backupPm() throws TaskException {
+        mReporter.onStartPackageBackup(PM_PACKAGE);
+        mCurrentPackage = new PackageInfo();
+        mCurrentPackage.packageName = PM_PACKAGE;
+
+        try {
+            extractPmAgentData(mCurrentPackage);
+            int status = sendDataToTransport(mCurrentPackage);
+            cleanUpAgentForTransportStatus(status);
+        } catch (AgentException | TaskException e) {
+            mReporter.onExtractPmAgentDataError(e);
+            cleanUpAgentForError(e);
+            // PM agent failure is task failure.
+            throw TaskException.stateCompromised(e);
+        }
+    }
+
+    private void backupPackage(String packageName) throws AgentException, TaskException {
+        mReporter.onStartPackageBackup(packageName);
+        mCurrentPackage = getPackageForBackup(packageName);
+
+        try {
+            extractAgentData(mCurrentPackage);
+            int status = sendDataToTransport(mCurrentPackage);
+            cleanUpAgentForTransportStatus(status);
+        } catch (AgentException | TaskException e) {
+            cleanUpAgentForError(e);
+            throw e;
+        }
+    }
+
+    private PackageInfo getPackageForBackup(String packageName) throws AgentException {
+        final PackageInfo packageInfo;
+        try {
+            packageInfo =
+                    mPackageManager.getPackageInfoAsUser(
+                            packageName, PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+        } catch (PackageManager.NameNotFoundException e) {
+            mReporter.onAgentUnknown(packageName);
+            throw AgentException.permanent(e);
+        }
+        ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+        if (!AppBackupUtils.appIsEligibleForBackup(applicationInfo, mUserId)) {
+            mReporter.onPackageNotEligibleForBackup(packageName);
+            throw AgentException.permanent();
+        }
+        if (AppBackupUtils.appGetsFullBackup(packageInfo)) {
+            mReporter.onPackageEligibleForFullBackup(packageName);
+            throw AgentException.permanent();
+        }
+        if (AppBackupUtils.appIsStopped(applicationInfo)) {
+            mReporter.onPackageStopped(packageName);
+            throw AgentException.permanent();
+        }
+        return packageInfo;
+    }
+
+    private IBackupAgent bindAgent(PackageInfo packageInfo) throws AgentException {
+        String packageName = packageInfo.packageName;
+        final IBackupAgent agent;
+        try {
+            agent =
+                    mBackupManagerService.bindToAgentSynchronous(
+                            packageInfo.applicationInfo, BACKUP_MODE_INCREMENTAL);
+            if (agent == null) {
+                mReporter.onAgentError(packageName);
+                throw AgentException.transitory();
+            }
+        } catch (SecurityException e) {
+            mReporter.onBindAgentError(packageName, e);
+            throw AgentException.transitory(e);
+        }
+        return agent;
+    }
+
+    private void finishTask(int status) {
+        // Mark packages that we couldn't backup as pending backup.
+        for (String packageName : mQueue) {
+            mBackupManagerService.dataChangedImpl(packageName);
+        }
+
+        // If backup succeeded, we just invalidated this journal. If not, we've already re-enqueued
+        // the packages and also don't need the journal.
+        if (mJournal != null && !mJournal.delete()) {
+            mReporter.onJournalDeleteFailed(mJournal);
+        }
+
+        String callerLogString = "KVBT.finishTask()";
+        String transportName = null;
+
+        // If the backup data was not empty, we succeeded and this is the first time
+        // we've done a backup, we can record the current backup dataset token.
+        long currentToken = mBackupManagerService.getCurrentToken();
+        if (mHasDataToBackup && (status == BackupTransport.TRANSPORT_OK) && (currentToken == 0)) {
+            try {
+                IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString);
+                transportName = transport.name();
+                mBackupManagerService.setCurrentToken(transport.getCurrentRestoreSet());
+                mBackupManagerService.writeRestoreTokens();
+            } catch (Exception e) {
+                // This will be recorded the next time we succeed.
+                mReporter.onSetCurrentTokenError(e);
+            }
+        }
+
+        synchronized (mQueueLock) {
+            mBackupManagerService.setBackupRunning(false);
+            if (status == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
+                mReporter.onTransportNotInitialized(transportName);
+                try {
+                    triggerTransportInitializationLocked();
+                } catch (Exception e) {
+                    mReporter.onPendingInitializeTransportError(e);
+                    status = BackupTransport.TRANSPORT_ERROR;
+                }
+            }
+        }
+
+        unregisterTask();
+        mReporter.onTaskFinished();
+
+        if (mCancelled) {
+            // We acknowledge the cancel as soon as we unregister the task, allowing other backups
+            // to be performed.
+            mCancelAcknowledged.open();
+        }
+
+        if (!mCancelled
+                && status == BackupTransport.TRANSPORT_OK
+                && mFullBackupTask != null
+                && !mPendingFullBackups.isEmpty()) {
+            mReporter.onStartFullBackup(mPendingFullBackups);
+            // The key-value backup has finished but not the overall backup. Full-backup task will:
+            // * Call mObserver.backupFinished() (which is called by mReporter below).
+            // * Call mTaskFinishedListener.onFinished().
+            // * Release the wakelock.
+            (new Thread(mFullBackupTask, "full-transport-requested")).start();
+            return;
+        }
+
+        if (mFullBackupTask != null) {
+            mFullBackupTask.unregisterTask();
+        }
+        mTaskFinishedListener.onFinished(callerLogString);
+        mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, status));
+        mBackupManagerService.getWakelock().release();
+    }
+
+    private int getBackupFinishedStatus(boolean cancelled, int transportStatus) {
+        if (cancelled) {
+            return BackupManager.ERROR_BACKUP_CANCELLED;
+        }
+        switch (transportStatus) {
+            case BackupTransport.TRANSPORT_OK:
+            case BackupTransport.TRANSPORT_QUOTA_EXCEEDED:
+            case BackupTransport.TRANSPORT_PACKAGE_REJECTED:
+                return BackupManager.SUCCESS;
+            case BackupTransport.TRANSPORT_NOT_INITIALIZED:
+            case BackupTransport.TRANSPORT_ERROR:
+            default:
+                return BackupManager.ERROR_TRANSPORT_ABORTED;
+        }
+    }
+
+    @GuardedBy("mQueueLock")
+    private void triggerTransportInitializationLocked() throws Exception {
+        IBackupTransport transport =
+                mTransportClient.connectOrThrow("KVBT.triggerTransportInitializationLocked");
+        mBackupManagerService.getPendingInits().add(transport.name());
+        deletePmStateFile();
+        mBackupManagerService.backupNow();
+    }
+
+    /** Removes PM state, triggering initialization in the next key-value task. */
+    private void deletePmStateFile() {
+        new File(mStateDirectory, PM_PACKAGE).delete();
+    }
+
+    /** Same as {@link #extractAgentData(PackageInfo)}, but only for PM package. */
+    private void extractPmAgentData(PackageInfo packageInfo) throws AgentException, TaskException {
+        Preconditions.checkArgument(packageInfo.packageName.equals(PM_PACKAGE));
+        BackupAgent pmAgent = mBackupManagerService.makeMetadataAgent();
+        mAgent = IBackupAgent.Stub.asInterface(pmAgent.onBind());
+        extractAgentData(packageInfo, mAgent);
+    }
+
+    /**
+     * Binds to the agent and extracts its backup data. If this method returns, the data in {@code
+     * mBackupData} is ready to be sent to the transport, otherwise it will throw.
+     *
+     * <p>This method leaves agent resources (agent binder, files and file-descriptors) opened that
+     * need to be cleaned up after terminating, either successfully or exceptionally. This clean-up
+     * can be done with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link
+     * #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to
+     * the transport or not. It's the caller responsibility to do the clean-up or delegate it.
+     */
+    private void extractAgentData(PackageInfo packageInfo) throws AgentException, TaskException {
+        mBackupManagerService.setWorkSource(new WorkSource(packageInfo.applicationInfo.uid));
+        try {
+            mAgent = bindAgent(packageInfo);
+            extractAgentData(packageInfo, mAgent);
+        } finally {
+            mBackupManagerService.setWorkSource(null);
+        }
+    }
+
+    /**
+     * Calls agent {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor,
+     * ParcelFileDescriptor, long, IBackupCallback, int)} and waits for the result. If this method
+     * returns, the data in {@code mBackupData} is ready to be sent to the transport, otherwise it
+     * will throw.
+     *
+     * <p>This method creates files and file-descriptors for the agent that need to be deleted and
+     * closed after terminating, either successfully or exceptionally. This clean-up can be done
+     * with methods {@link #cleanUpAgentForTransportStatus(int)} and {@link
+     * #cleanUpAgentForError(BackupException)}, depending on whether data was successfully sent to
+     * the transport or not. It's the caller responsibility to do the clean-up or delegate it.
+     */
+    private void extractAgentData(PackageInfo packageInfo, IBackupAgent agent)
+            throws AgentException, TaskException {
+        String packageName = packageInfo.packageName;
+        mReporter.onExtractAgentData(packageName);
+
+        mSavedStateFile = new File(mStateDirectory, packageName);
+        mBackupDataFile = new File(mDataDirectory, packageName + STAGING_FILE_SUFFIX);
+        mNewStateFile = new File(mStateDirectory, packageName + NEW_STATE_FILE_SUFFIX);
+        mReporter.onAgentFilesReady(mBackupDataFile);
+
+        boolean callingAgent = false;
+        final RemoteResult agentResult;
+        try {
+            File savedStateFileForAgent = (mNonIncremental) ? mBlankStateFile : mSavedStateFile;
+            // MODE_CREATE to make an empty file if necessary
+            mSavedState =
+                    ParcelFileDescriptor.open(savedStateFileForAgent, MODE_READ_ONLY | MODE_CREATE);
+            mBackupData =
+                    ParcelFileDescriptor.open(
+                            mBackupDataFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
+            mNewState =
+                    ParcelFileDescriptor.open(
+                            mNewStateFile, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
+
+            // TODO (b/120424138): Remove once the system user is migrated to use the per-user CE
+            // directory. Per-user CE directories are managed by vold.
+            if (mUserId == UserHandle.USER_SYSTEM) {
+                if (!SELinux.restorecon(mBackupDataFile)) {
+                    mReporter.onRestoreconFailed(mBackupDataFile);
+                }
+            }
+
+            IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.extractAgentData()");
+            long quota = transport.getBackupQuota(packageName, /* isFullBackup */ false);
+            int transportFlags = transport.getTransportFlags();
+
+            callingAgent = true;
+            agentResult =
+                    remoteCall(
+                            callback ->
+                                    agent.doBackup(
+                                            mSavedState,
+                                            mBackupData,
+                                            mNewState,
+                                            quota,
+                                            callback,
+                                            transportFlags),
+                            mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis(),
+                            "doBackup()");
+        } catch (Exception e) {
+            mReporter.onCallAgentDoBackupError(packageName, callingAgent, e);
+            if (callingAgent) {
+                throw AgentException.transitory(e);
+            } else {
+                throw TaskException.create();
+            }
+        }
+        checkAgentResult(packageInfo, agentResult);
+    }
+
+    private void checkAgentResult(PackageInfo packageInfo, RemoteResult result)
+            throws AgentException, TaskException {
+        if (result == RemoteResult.FAILED_THREAD_INTERRUPTED) {
+            // Not an explicit cancel, we need to flag it.
+            mCancelled = true;
+            mReporter.onAgentCancelled(packageInfo);
+            throw TaskException.create();
+        }
+        if (result == RemoteResult.FAILED_CANCELLED) {
+            mReporter.onAgentCancelled(packageInfo);
+            throw TaskException.create();
+        }
+        if (result == RemoteResult.FAILED_TIMED_OUT) {
+            mReporter.onAgentTimedOut(packageInfo);
+            throw AgentException.transitory();
+        }
+        Preconditions.checkState(result.isPresent());
+        long resultCode = result.get();
+        if (resultCode == BackupAgent.RESULT_ERROR) {
+            mReporter.onAgentResultError(packageInfo);
+            throw AgentException.transitory();
+        }
+        Preconditions.checkState(resultCode == BackupAgent.RESULT_SUCCESS);
+    }
+
+    private void agentFail(IBackupAgent agent, String message) {
+        try {
+            agent.fail(message);
+        } catch (Exception e) {
+            mReporter.onFailAgentError(mCurrentPackage.packageName);
+        }
+    }
+
+    // SHA-1 a byte array and return the result in hex
+    private String SHA1Checksum(byte[] input) {
+        final byte[] checksum;
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            checksum = md.digest(input);
+        } catch (NoSuchAlgorithmException e) {
+            mReporter.onDigestError(e);
+            return "00";
+        }
+
+        StringBuilder string = new StringBuilder(checksum.length * 2);
+        for (byte item : checksum) {
+            string.append(Integer.toHexString(item));
+        }
+        return string.toString();
+    }
+
+    private void writeWidgetPayloadIfAppropriate(FileDescriptor fd, String pkgName)
+            throws IOException {
+        byte[] widgetState = AppWidgetBackupBridge.getWidgetState(pkgName, mUserId);
+        File widgetFile = new File(mStateDirectory, pkgName + "_widget");
+        boolean priorStateExists = widgetFile.exists();
+        if (!priorStateExists && widgetState == null) {
+            return;
+        }
+        mReporter.onWriteWidgetData(priorStateExists, widgetState);
+
+        // if the new state is not null, we might need to compare checksums to
+        // determine whether to update the widget blob in the archive.  If the
+        // widget state *is* null, we know a priori at this point that we simply
+        // need to commit a deletion for it.
+        String newChecksum = null;
+        if (widgetState != null) {
+            newChecksum = SHA1Checksum(widgetState);
+            if (priorStateExists) {
+                final String priorChecksum;
+                try (
+                        FileInputStream fin = new FileInputStream(widgetFile);
+                        DataInputStream in = new DataInputStream(fin)
+                ) {
+                    priorChecksum = in.readUTF();
+                }
+                if (Objects.equals(newChecksum, priorChecksum)) {
+                    // Same checksum => no state change => don't rewrite the widget data
+                    return;
+                }
+            }
+        } // else widget state *became* empty, so we need to commit a deletion
+
+        BackupDataOutput out = new BackupDataOutput(fd);
+        if (widgetState != null) {
+            try (
+                    FileOutputStream fout = new FileOutputStream(widgetFile);
+                    DataOutputStream stateOut = new DataOutputStream(fout)
+            ) {
+                stateOut.writeUTF(newChecksum);
+            }
+
+            out.writeEntityHeader(KEY_WIDGET_STATE, widgetState.length);
+            out.writeEntityData(widgetState, widgetState.length);
+        } else {
+            // Widget state for this app has been removed; commit a deletion
+            out.writeEntityHeader(KEY_WIDGET_STATE, -1);
+            widgetFile.delete();
+        }
+    }
+
+    /** Returns transport status. */
+    private int sendDataToTransport() throws AgentException, TaskException, IOException {
+        Preconditions.checkState(mBackupData != null);
+        checkBackupData(mCurrentPackage.applicationInfo, mBackupDataFile);
+
+        String packageName = mCurrentPackage.packageName;
+        writeWidgetPayloadIfAppropriate(mBackupData.getFileDescriptor(), packageName);
+
+        boolean nonIncremental = mSavedStateFile.length() == 0;
+        int status = transportPerformBackup(mCurrentPackage, mBackupDataFile, nonIncremental);
+        handleTransportStatus(status, packageName, mBackupDataFile.length());
+        return status;
+    }
+
+    private int transportPerformBackup(
+            PackageInfo packageInfo, File backupDataFile, boolean nonIncremental)
+            throws TaskException {
+        String packageName = packageInfo.packageName;
+        long size = backupDataFile.length();
+        if (size <= 0) {
+            mReporter.onEmptyData(packageInfo);
+            return BackupTransport.TRANSPORT_OK;
+        }
+
+        mHasDataToBackup = true;
+
+        int status;
+        try (ParcelFileDescriptor backupData =
+                ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow("KVBT.transportPerformBackup()");
+            mReporter.onTransportPerformBackup(packageName);
+            int flags = getPerformBackupFlags(mUserInitiated, nonIncremental);
+
+            status = transport.performBackup(packageInfo, backupData, flags);
+            if (status == BackupTransport.TRANSPORT_OK) {
+                status = transport.finishBackup();
+            } else if (status == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
+                mReporter.onTransportNotInitialized(transport.name());
+            }
+        } catch (Exception e) {
+            mReporter.onPackageBackupTransportError(packageName, e);
+            throw TaskException.causedBy(e);
+        }
+
+        if (nonIncremental && status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
+            mReporter.onPackageBackupNonIncrementalAndNonIncrementalRequired(packageName);
+            throw TaskException.create();
+        }
+
+        return status;
+    }
+
+    private void handleTransportStatus(int status, String packageName, long size)
+            throws TaskException, AgentException {
+        if (status == BackupTransport.TRANSPORT_OK) {
+            mReporter.onPackageBackupComplete(packageName, size);
+            return;
+        }
+        if (status == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
+            mReporter.onPackageBackupNonIncrementalRequired(mCurrentPackage);
+            // Immediately retry the current package.
+            mQueue.add(0, packageName);
+            return;
+        }
+        if (status == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
+            mReporter.onPackageBackupRejected(packageName);
+            throw AgentException.permanent();
+        }
+        if (status == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
+            mReporter.onPackageBackupQuotaExceeded(packageName);
+            agentDoQuotaExceeded(mAgent, packageName, size);
+            throw AgentException.permanent();
+        }
+        // Any other error here indicates a transport-level failure.
+        mReporter.onPackageBackupTransportFailure(packageName);
+        throw TaskException.forStatus(status);
+    }
+
+    private void agentDoQuotaExceeded(@Nullable IBackupAgent agent, String packageName, long size) {
+        if (agent != null) {
+            try {
+                IBackupTransport transport =
+                        mTransportClient.connectOrThrow("KVBT.agentDoQuotaExceeded()");
+                long quota = transport.getBackupQuota(packageName, false);
+                remoteCall(
+                        callback -> agent.doQuotaExceeded(size, quota, callback),
+                        mAgentTimeoutParameters.getQuotaExceededTimeoutMillis(),
+                        "doQuotaExceeded()");
+            } catch (Exception e) {
+                mReporter.onAgentDoQuotaExceededError(e);
+            }
+        }
+    }
+
+    /**
+     * For system apps and pseudo-apps never throws. For regular apps throws {@link AgentException}
+     * if {@code backupDataFile} has any protected keys, also crashing the app.
+     */
+    private void checkBackupData(@Nullable ApplicationInfo applicationInfo, File backupDataFile)
+            throws IOException, AgentException {
+        if (applicationInfo == null || (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            // System apps and pseudo-apps can write what they want.
+            return;
+        }
+        try (ParcelFileDescriptor backupData =
+                ParcelFileDescriptor.open(backupDataFile, MODE_READ_ONLY)) {
+            BackupDataInput backupDataInput = new BackupDataInput(backupData.getFileDescriptor());
+            while (backupDataInput.readNextHeader()) {
+                String key = backupDataInput.getKey();
+                if (key != null && key.charAt(0) >= 0xff00) {
+                    mReporter.onAgentIllegalKey(mCurrentPackage, key);
+                    // Crash them if they wrote any protected keys.
+                    agentFail(mAgent, "Illegal backup key: " + key);
+                    throw AgentException.permanent();
+                }
+                backupDataInput.skipEntityData();
+            }
+        }
+    }
+
+    private int getPerformBackupFlags(boolean userInitiated, boolean nonIncremental) {
+        int userInitiatedFlag = userInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
+        int incrementalFlag =
+                nonIncremental
+                        ? BackupTransport.FLAG_NON_INCREMENTAL
+                        : BackupTransport.FLAG_INCREMENTAL;
+        return userInitiatedFlag | incrementalFlag;
+    }
+
+    /**
+     * Cancels this task.
+     *
+     * <p>After this method returns this task won't be registered in {@link BackupManagerService}
+     * anymore, which means there will be no backups running unless there is a racy request
+     * coming from another thread in between. As a consequence there will be no more calls to the
+     * transport originated from this task.
+     *
+     * <p>If this method is executed while an agent is performing a backup, we will stop waiting for
+     * it, disregard its backup data and finalize the task. However, if this method is executed in
+     * between agent calls, the backup data of the last called agent will be sent to
+     * the transport and we will not consider the next agent (nor the rest of the queue), proceeding
+     * to finalize the backup.
+     *
+     * <p>Note: This method is inherently racy since there are no guarantees about how much of the
+     * task will be executed after you made the call.
+     *
+     * @param cancelAll MUST be {@code true}. Will be removed.
+     */
+    @Override
+    public void handleCancel(boolean cancelAll) {
+        // This is called in a thread different from the one that executes method run().
+        Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task");
+        markCancel();
+        waitCancel();
+    }
+
+    /** Marks this task as cancelled and tries to stop any ongoing agent call. */
+    @VisibleForTesting
+    public void markCancel() {
+        mReporter.onCancel();
+        mCancelled = true;
+        RemoteCall pendingCall = mPendingCall;
+        if (pendingCall != null) {
+            pendingCall.cancel();
+        }
+    }
+
+    /** Waits for this task to be cancelled after call to {@link #markCancel()}. */
+    @VisibleForTesting
+    public void waitCancel() {
+        mCancelAcknowledged.block();
+    }
+
+    private void revertTask() {
+        mReporter.onRevertTask();
+        long delay;
+        try {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow("KVBT.revertTask()");
+            delay = transport.requestBackupTime();
+        } catch (Exception e) {
+            mReporter.onTransportRequestBackupTimeError(e);
+            // Use the scheduler's default.
+            delay = 0;
+        }
+        KeyValueBackupJob.schedule(mBackupManagerService.getUserId(),
+                mBackupManagerService.getContext(), delay, mBackupManagerService.getConstants());
+
+        for (String packageName : mOriginalQueue) {
+            mBackupManagerService.dataChangedImpl(packageName);
+        }
+    }
+
+    /**
+     * Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} for exceptional
+     * case.
+     *
+     * <p>Note: Declaring exception parameter so that the caller only calls this when an exception
+     * is thrown.
+     */
+    private void cleanUpAgentForError(BackupException exception) {
+        cleanUpAgent(StateTransaction.DISCARD_NEW);
+    }
+
+    /**
+     * Cleans up agent resources opened by {@link #extractAgentData(PackageInfo)} according to
+     * transport status returned in {@link #sendDataToTransport(PackageInfo)}.
+     */
+    private void cleanUpAgentForTransportStatus(int status) {
+        switch (status) {
+            case BackupTransport.TRANSPORT_OK:
+                cleanUpAgent(StateTransaction.COMMIT_NEW);
+                break;
+            case BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED:
+                cleanUpAgent(StateTransaction.DISCARD_ALL);
+                break;
+            default:
+                // All other transport statuses are properly converted to agent or task exceptions.
+                throw new AssertionError();
+        }
+    }
+
+    private void cleanUpAgent(@StateTransaction int stateTransaction) {
+        applyStateTransaction(stateTransaction);
+        if (mBackupDataFile != null) {
+            mBackupDataFile.delete();
+        }
+        mBlankStateFile.delete();
+        mSavedStateFile = null;
+        mBackupDataFile = null;
+        mNewStateFile = null;
+        tryCloseFileDescriptor(mSavedState, "old state");
+        tryCloseFileDescriptor(mBackupData, "backup data");
+        tryCloseFileDescriptor(mNewState, "new state");
+        mSavedState = null;
+        mBackupData = null;
+        mNewState = null;
+
+        // For PM metadata (for which applicationInfo is null) there is no agent-bound state.
+        if (mCurrentPackage.applicationInfo != null) {
+            mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo);
+        }
+        mAgent = null;
+    }
+
+    private void applyStateTransaction(@StateTransaction int stateTransaction) {
+        switch (stateTransaction) {
+            case StateTransaction.COMMIT_NEW:
+                mNewStateFile.renameTo(mSavedStateFile);
+                break;
+            case StateTransaction.DISCARD_NEW:
+                if (mNewStateFile != null) {
+                    mNewStateFile.delete();
+                }
+                break;
+            case StateTransaction.DISCARD_ALL:
+                mSavedStateFile.delete();
+                mNewStateFile.delete();
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown state transaction " + stateTransaction);
+        }
+    }
+
+    private void tryCloseFileDescriptor(@Nullable Closeable closeable, String logName) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (IOException e) {
+                mReporter.onCloseFileDescriptorError(logName);
+            }
+        }
+    }
+
+    private RemoteResult remoteCall(
+            RemoteCallable<IBackupCallback> remoteCallable, long timeoutMs, String logIdentifier)
+            throws RemoteException {
+        mPendingCall = new RemoteCall(mCancelled, remoteCallable, timeoutMs);
+        RemoteResult result = mPendingCall.call();
+        mReporter.onRemoteCallReturned(result, logIdentifier);
+        mPendingCall = null;
+        return result;
+    }
+
+    @IntDef({
+        StateTransaction.COMMIT_NEW,
+        StateTransaction.DISCARD_NEW,
+        StateTransaction.DISCARD_ALL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface StateTransaction {
+        int COMMIT_NEW = 0;
+        int DISCARD_NEW = 1;
+        int DISCARD_ALL = 2;
+    }
+}
diff --git a/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
new file mode 100644
index 0000000..ec56e1e
--- /dev/null
+++ b/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -0,0 +1,2958 @@
+/*
+ * 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.createUserBackupManagerServiceAndRunTasks;
+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.messagesInLooper;
+import static com.android.server.backup.testing.TestUtils.uncheck;
+import static com.android.server.backup.testing.TestUtils.waitUntil;
+import static com.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.Utils.isFileNonEmpty;
+import static com.android.server.backup.testing.Utils.oneTimeIterable;
+import static com.android.server.backup.testing.Utils.transferStreamedData;
+
+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.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.shadow.api.Shadow.extract;
+import static org.testng.Assert.expectThrows;
+import static org.testng.Assert.fail;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.util.Collections.emptyList;
+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.IBackupCallback;
+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.content.pm.PackageManagerInternal;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+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.LocalServices;
+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.UserBackupManagerService;
+import com.android.server.backup.internal.BackupHandler;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.remote.RemoteCall;
+import com.android.server.backup.testing.PackageData;
+import com.android.server.backup.testing.TestUtils.ThrowingRunnable;
+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.testing.shadows.FrameworkShadowLooper;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+import com.android.server.testing.shadows.ShadowEventLog;
+import com.android.server.testing.shadows.ShadowSystemServiceRegistry;
+
+import com.google.common.truth.IterableSubject;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+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.concurrent.TimeoutException;
+import java.util.stream.Stream;
+
+// TODO: Test agents timing out
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            FrameworkShadowLooper.class,
+            ShadowApplicationPackageManager.class,
+            ShadowBackupDataInput.class,
+            ShadowBackupDataOutput.class,
+            ShadowEventLog.class,
+            ShadowQueuedWork.class,
+            ShadowSystemServiceRegistry.class
+        })
+@Presubmit
+public class KeyValueBackupTaskTest  {
+    private static final PackageData PACKAGE_1 = keyValuePackage(1);
+    private static final PackageData PACKAGE_2 = keyValuePackage(2);
+    private static final String BACKUP_AGENT_SHARED_PREFS_SYNCHRONIZER_CLASS =
+            "android.app.backup.BackupAgent$SharedPrefsSynchronizer";
+    private static final int USER_ID = 10;
+
+    @Mock private TransportManager mTransportManager;
+    @Mock private DataChangedJournal mOldJournal;
+    @Mock private IBackupObserver mObserver;
+    @Mock private IBackupManagerMonitor mMonitor;
+    @Mock private OnTaskFinishedListener mListener;
+    @Mock private PackageManagerInternal mPackageManagerInternal;
+    private UserBackupManagerService mBackupManagerService;
+    private TransportData mTransport;
+    private ShadowLooper mShadowBackupLooper;
+    private Handler mBackupHandler;
+    private UserBackupManagerService.BackupWakeLock mWakeLock;
+    private KeyValueBackupReporter mReporter;
+    private PackageManager mPackageManager;
+    private ShadowPackageManager mShadowPackageManager;
+    private FakeIBackupManager mBackupManager;
+    private File mBaseStateDir;
+    private File mDataDir;
+    private Application mApplication;
+    private Looper mMainLooper;
+    private FrameworkShadowLooper mShadowMainLooper;
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTransport = backupTransport();
+
+        mApplication = RuntimeEnvironment.application;
+        mContext = mApplication;
+
+        mMainLooper = Looper.getMainLooper();
+        mShadowMainLooper = extract(mMainLooper);
+
+        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();
+
+        mPackageManager = mApplication.getPackageManager();
+        mShadowPackageManager = shadowOf(mPackageManager);
+
+        mWakeLock = createBackupWakeLock(mApplication);
+        mBackupManager = spy(FakeIBackupManager.class);
+
+        // Needed to be able to use a real BMS instead of a mock
+        setUpBinderCallerAndApplicationAsSystem(mApplication);
+        mBackupManagerService =
+                spy(
+                        createUserBackupManagerServiceAndRunTasks(
+                                USER_ID, mContext, mBaseStateDir, mDataDir, mTransportManager));
+        setUpBackupManagerServiceBasics(
+                mBackupManagerService,
+                mApplication,
+                mTransportManager,
+                mPackageManager,
+                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();
+        mReporter = spy(new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor));
+
+        when(mPackageManagerInternal.getApplicationEnabledState(any(), anyInt()))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ShadowBackupDataInput.reset();
+        ShadowApplicationPackageManager.reset();
+    }
+
+    @Test
+    public void testRunTask_whenQueueEmpty_updatesBookkeeping() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(mBackupManagerService.getCurrentToken()).thenReturn(0L);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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, 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, 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, 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, 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, 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());
+    }
+
+    /** Do not update backup token if the backup queue was empty */
+    @Test
+    public void testRunTask_whenQueueEmptyOnFirstBackup_doesNotUpdateCurrentToken()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true);
+        mBackupManagerService.setCurrentToken(0L);
+        when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L);
+
+        runTask(task);
+
+        assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(0L);
+    }
+
+    @Test
+    public void testRunTask_whenOnePackageAndTransportUnavailable() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport.unavailable());
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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, 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, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(mWakeLock.isHeld()).isFalse();
+    }
+
+    @Test
+    public void testRunTask_whenOnePackage_cleansUpPmFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PM_PACKAGE);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsTransportErrorForPm_cleansUpPmFiles()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PM_PACKAGE)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED);
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PM_PACKAGE);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsTransportErrorForPm_resetsBackupState()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PM_PACKAGE)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_PACKAGE_REJECTED);
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile());
+    }
+
+    @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, 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, 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, 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);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task =
+                createKeyValueBackupTask(transportMock, 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);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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, 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, 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, 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, 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_whenPackageUnknown() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        // Not calling setUpAgent() for PACKAGE_1
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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_whenFirstPackageUnknown_callsTransportForSecondPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        // Not calling setUpAgent() for PACKAGE_1
+        setUpAgentWithData(PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+
+        runTask(task);
+
+        verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenPackageNotEligibleForBackup() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_1.backupNotAllowed());
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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_whenFirstPackageNotEligibleForBackup_callsTransportForSecondPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentsWithData(PACKAGE_1.backupNotAllowed(), PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+
+        runTask(task);
+
+        verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenPackageDoesFullBackup() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        PackageData packageData = fullBackupPackage(1);
+        AgentMock agentMock = setUpAgentWithData(packageData);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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_whenFirstPackageDoesFullBackup_callsTransportForSecondPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        PackageData packageData = fullBackupPackage(1);
+        setUpAgentsWithData(packageData, PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, packageData, PACKAGE_2);
+
+        runTask(task);
+
+        verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenPackageIsStopped() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_1.stopped());
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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_whenFirstPackageIsStopped_callsTransportForSecondPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentsWithData(PACKAGE_1.stopped(), PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+
+        runTask(task);
+
+        verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @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.getUid(0) == PACKAGE_1.uid));
+                    verify(mBackupManagerService, never()).setWorkSource(null);
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        // More verifications inside agent call above
+        verify(mBackupManagerService).setWorkSource(null);
+    }
+
+    /**
+     * Agent unavailable means {@link
+     * UserBackupManagerService#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, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).setWorkSource(null);
+        verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenSecondAgentUnavailable_commitsFirstAgentState() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        setUpAgent(PACKAGE_2.unavailable());
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("newState".getBytes());
+    }
+
+    @Test
+    public void testRunTask_whenNonIncrementalAndAgentUnavailable() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1.unavailable());
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).setWorkSource(null);
+        verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @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, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).setWorkSource(null);
+        verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenNonIncrementalAndBindToAgentThrowsSecurityException()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        doThrow(SecurityException.class)
+                .when(mBackupManagerService)
+                .bindToAgentSynchronous(argThat(applicationInfo(PACKAGE_1)), anyInt());
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).setWorkSource(null);
+        verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
+        verify(mObserver).backupFinished(BackupManager.SUCCESS);
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @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, 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, 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, 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_revertsTask() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.getBackupQuota(PACKAGE_1.packageName, false))
+                .thenThrow(DeadObjectException.class);
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertTaskReverted(transportMock, PACKAGE_1);
+    }
+
+    /**
+     * For local agents the exception is thrown in our stack, before {@link RemoteCall} has a chance
+     * to complete cleanly.
+     */
+    // TODO: When RemoteCall spins up a new thread the assertions on this method should be the same
+    // as the methods below (non-local call).
+    @Test
+    public void testRunTask_whenLocalAgentOnBackupThrows_setsNullWorkSource() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).setWorkSource(null);
+    }
+
+    @Test
+    public void testRunTask_whenLocalAgentOnBackupThrows_reportsCorrectly() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
+        verify(mObserver).backupFinished(SUCCESS);
+        verify(mReporter)
+                .onCallAgentDoBackupError(
+                        eq(PACKAGE_1.packageName), eq(true), any(RuntimeException.class));
+        assertEventLogged(
+                EventLogTags.BACKUP_AGENT_FAILURE,
+                PACKAGE_1.packageName,
+                new RuntimeException().toString());
+    }
+
+    @Test
+    public void testRunTask_whenLocalAgentOnBackupThrows_doesNotUpdateBookkeping()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenAgentOnBackupThrows_reportsCorrectly() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        remoteAgentOnBackupThrows(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mReporter).onAgentResultError(argThat(packageInfo(PACKAGE_1)));
+    }
+
+    @Test
+    public void testRunTask_whenAgentOnBackupThrows_updatesBookkeeping() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        remoteAgentOnBackupThrows(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertBackupPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenAgentOnBackupThrows_doesNotCallTransport() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        remoteAgentOnBackupThrows(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(transportMock.transport, never())
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenAgentOnBackupThrows_updatesFilesAndCleansUp() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        remoteAgentOnBackupThrows(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    throw new RuntimeException();
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("oldState".getBytes());
+        assertCleansUpFilesAndAgent(mTransport, 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, 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, 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<AgentMock> agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2);
+        BackupAgent agent1 = agentMocks.get(0).agent;
+        BackupAgent agent2 = agentMocks.get(1).agent;
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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, 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, PACKAGE_1);
+
+        runTask(task);
+
+        verify(agentMock.agentBinder).fail(any());
+        verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1)));
+    }
+
+    @Test
+    public void testRunTask_whenAgentUsesProhibitedKey_updatesFilesAndCleansUp() 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, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("oldState".getBytes());
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @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, 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, 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, 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<AgentMock> 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, 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, 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, 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, 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, PACKAGE_1);
+
+        runTask(task);
+
+        assertBackupNotPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenAgentDoesNotWriteData_updatesFilesAndCleansUp() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    // No-op
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1))).isEqualTo(new byte[0]);
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @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, 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_whenPmAgentWritesData_callsTransportPerformBackupWithAgentData()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        Path backupDataPath = createTemporaryFile();
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PM_PACKAGE)), any(), anyInt()))
+                .then(copyBackupDataTo(backupDataPath));
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        agentOnBackupDo(
+                pmAgent,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key1", "data1".getBytes());
+                    writeData(dataOutput, "key2", "data2".getBytes());
+                    writeState(newState, "newState".getBytes());
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PM_PACKAGE)), any(), anyInt());
+        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, PACKAGE_1);
+
+        runTask(task);
+
+        // First for PM, then for the package
+        verify(transportMock.transport, times(2)).finishBackup();
+    }
+
+    @Test
+    public void testRunTask_whenFinishBackupSucceeds_updatesFilesAndCleansUp() 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, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("newState".getBytes());
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenFinishBackupSucceedsForPm_cleansUp() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        agentOnBackupDo(
+                pmAgent,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PM_PACKAGE)))
+                .isEqualTo("newState".getBytes());
+        assertCleansUpFiles(mTransport, PM_PACKAGE);
+        // We don't unbind PM
+        verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE)));
+    }
+
+    @Test
+    public void testRunTask_whenFinishBackupSucceedsForPm_doesNotUnbindPm() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        when(transportMock.transport.finishBackup()).thenReturn(BackupTransport.TRANSPORT_OK);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        agentOnBackupDo(
+                pmAgent,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                });
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE)));
+    }
+
+    @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, 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, 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, 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, PACKAGE_1);
+
+        runTask(task);
+
+        // Called only for PM
+        verify(transportMock.transport, times(1)).finishBackup();
+    }
+
+    @Test
+    public void testRunTask_whenTransportRejectsPackage_updatesFilesAndCleansUp() 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, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("oldState".getBytes());
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @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, 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, 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, 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, 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, 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_callsAgentOnQuotaExceeded()
+            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, PACKAGE_1);
+
+        runTask(task);
+
+        InOrder inOrder = inOrder(agentMock.agent, mBackupManagerService);
+        inOrder.verify(agentMock.agent).onQuotaExceeded(anyLong(), eq(1234L));
+        inOrder.verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1)));
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsQuotaExceeded_updatesBookkeeping()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertBackupNotPendingFor(PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsQuotaExceeded_notifiesAndLogs() 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, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mObserver)
+                .onResult(PACKAGE_1.packageName, BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
+        verify(mObserver).backupFinished(SUCCESS);
+        assertEventLogged(EventLogTags.BACKUP_QUOTA_EXCEEDED, PACKAGE_1.packageName);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsQuotaExceeded_cleansUpFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_QUOTA_EXCEEDED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_cleansUpFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(isFileNonEmpty(getStateFile(mTransport, PACKAGE_1))).isFalse();
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_reportsCorrectly() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mReporter).onPackageBackupTransportFailure(PACKAGE_1.packageName);
+        verify(mReporter).onTransportNotInitialized(mTransport.transportName);
+        verify(mReporter).onBackupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitializedForPm_reportsCorrectly()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PM_PACKAGE)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mReporter).onPackageBackupTransportFailure(PM_PACKAGE.packageName);
+        verify(mReporter).onTransportNotInitialized(mTransport.transportName);
+        verify(mReporter).onBackupFinished(BackupManager.ERROR_TRANSPORT_ABORTED);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_doesNotCallSecondAgent()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+
+        runTask(task);
+
+        verify(agentMock.agent, never()).onBackup(any(), any(), any());
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_revertsTask() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertTaskReverted(transportMock, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_triggersTransportInitialization()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(mBackupManagerService.getPendingInits()).contains(mTransport.transportName);
+        verify(mBackupManagerService).backupNow();
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitialized_cleansUpPmStateFile()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PM_PACKAGE), "pmState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.exists(getStateFile(mTransport, PM_PACKAGE))).isFalse();
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsNotInitializedForPm_cleansUpPmStateFile()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PM_PACKAGE)), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PM_PACKAGE), "pmState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.exists(getStateFile(mTransport, PM_PACKAGE))).isFalse();
+    }
+
+    @Test
+    public void
+            testRunTask_whenTransportReturnsNotInitializedAndThrowsWhenQueryingName_reportsCorrectly()
+                    throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.performBackup(any(), any(), anyInt()))
+                .thenReturn(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        // First one is in startTask(), second is in finishTask(), the third is the one we want.
+        when(transportMock.transport.name())
+                .thenReturn(mTransport.transportName)
+                .thenReturn(mTransport.transportName)
+                .thenThrow(DeadObjectException.class);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mReporter).onPendingInitializeTransportError(any(DeadObjectException.class));
+        verify(mReporter).onBackupFinished(ERROR_TRANSPORT_ABORTED);
+    }
+
+    @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, 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, 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, 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, PACKAGE_1);
+
+        runTask(task);
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_FAILURE, PACKAGE_1.packageName);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsError_revertsTask() 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, PACKAGE_1);
+
+        runTask(task);
+
+        assertTaskReverted(transportMock, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenTransportReturnsError_updatesFilesAndCleansUp() 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, PACKAGE_1);
+        Files.write(getStateFile(mTransport, PACKAGE_1), "oldState".getBytes());
+
+        runTask(task);
+
+        assertThat(Files.readAllBytes(getStateFile(mTransport, PACKAGE_1)))
+                .isEqualTo("oldState".getBytes());
+        assertCleansUpFilesAndAgent(mTransport, PACKAGE_1);
+    }
+
+    @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, 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_reportsCorrectly() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException());
+        when(mBackupManagerService.makeMetadataAgent()).thenReturn(pmAgent);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, 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());
+    }
+
+    @Test
+    public void testRunTask_whenPmAgentFails_revertsTask() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException());
+        doReturn(pmAgent).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertTaskReverted(transportMock, PACKAGE_1);
+    }
+
+    @Test
+    public void testRunTask_whenPmAgentFails_cleansUpFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException());
+        doReturn(pmAgent).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PM_PACKAGE);
+    }
+
+    @Test
+    public void testRunTask_whenPmAgentFails_resetsBackupState() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        BackupAgent pmAgent = createThrowingPmAgent(new RuntimeException());
+        doReturn(pmAgent).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        runTask(task);
+
+        verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile());
+    }
+
+    @Test
+    public void testRunTask_whenMarkCancelDuringPmOnBackup_resetsBackupState() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        agentOnBackupDo(
+                pmAgent, (oldState, dataOutput, newState) -> runInWorkerThread(task::markCancel));
+
+        runTask(task);
+
+        verify(mBackupManagerService).resetBackupState(getStateDirectory(mTransport).toFile());
+    }
+
+    @Test
+    public void testRunTask_whenMarkCancelDuringPmOnBackup_cleansUpFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgent(PACKAGE_1);
+        BackupAgent pmAgent = spy(createPmAgent());
+        doReturn(forward(pmAgent)).when(mBackupManagerService).makeMetadataAgent();
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        agentOnBackupDo(
+                pmAgent, (oldState, dataOutput, newState) -> runInWorkerThread(task::markCancel));
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PM_PACKAGE);
+    }
+
+    @Test
+    public void testRunTask_whenBackupRunning_doesNotThrow() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(mBackupManagerService.isBackupOperationInProgress()).thenReturn(true);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock);
+
+        runTask(task);
+    }
+
+    @Test
+    public void testRunTask_whenReadingBackupDataThrows_reportsCorrectly() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        // We don't validate PM's data, so it will only throw in PACKAGE_1
+        ShadowBackupDataInput.throwInNextHeaderRead();
+
+        runTask(task);
+
+        verify(mReporter).onAgentDataError(eq(PACKAGE_1.packageName), any());
+    }
+
+    @Test
+    public void testRunTask_whenReadingBackupDataThrows_doesNotCallTransport() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        ShadowBackupDataInput.throwInNextHeaderRead();
+
+        runTask(task);
+
+        verify(transportMock.transport, never())
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenReadingBackupDataThrows_doesNotCallSecondAgent() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        ShadowBackupDataInput.throwInNextHeaderRead();
+
+        runTask(task);
+
+        verify(agentMock.agent, never()).onBackup(any(), any(), any());
+    }
+
+    @Test
+    public void testRunTask_whenReadingBackupDataThrows_cleansUpAndRevertsTask() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentsWithData(PACKAGE_1, PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        ShadowBackupDataInput.throwInNextHeaderRead();
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PACKAGE_2);
+        assertTaskReverted(transportMock, PACKAGE_1, PACKAGE_2);
+    }
+
+    @Test
+    public void testRunTask_whenMarkCancelDuringAgentOnBackup_cleansUpFiles() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                    runInWorkerThread(task::markCancel);
+                });
+
+        runTask(task);
+
+        assertCleansUpFiles(mTransport, PACKAGE_1);
+    }
+
+    @Test
+    public void
+            testRunTask_whenMarkCancelDuringFirstAgentOnBackup_doesNotCallTransportAfterWaitCancel()
+                    throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        setUpAgentsWithData(PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                    runInWorkerThread(task::markCancel);
+                });
+
+        ConditionVariable taskFinished = runTaskAsync(task);
+
+        verifyAndUnblockAgentCalls(2);
+        task.waitCancel();
+        reset(transportMock.transport);
+        taskFinished.block();
+        verifyZeroInteractions(transportMock.transport);
+    }
+
+    @Test
+    public void testRunTask_whenMarkCancelDuringAgentOnBackup_doesNotCallTransportForPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                    runInWorkerThread(task::markCancel);
+                });
+
+        ConditionVariable taskFinished = runTaskAsync(task);
+
+        verifyAndUnblockAgentCalls(2);
+        taskFinished.block();
+        // For PM
+        verify(transportMock.transport, times(1)).finishBackup();
+        verify(transportMock.transport, never())
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_whenMarkCancelDuringTransportPerformBackup_callsTransportForPackage()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenAnswer(
+                        invocation -> {
+                            runInWorkerThread(task::markCancel);
+                            return BackupTransport.TRANSPORT_OK;
+                        });
+
+        ConditionVariable taskFinished = runTaskAsync(task);
+
+        verifyAndUnblockAgentCalls(2);
+        taskFinished.block();
+        InOrder inOrder = inOrder(transportMock.transport);
+        inOrder.verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+        inOrder.verify(transportMock.transport).finishBackup();
+    }
+
+    @Test
+    public void
+            testRunTask_whenMarkCancelDuringSecondAgentOnBackup_callsTransportForFirstPackageButNotForSecond()
+                    throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        AgentMock agentMock = setUpAgent(PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                    runInWorkerThread(task::markCancel);
+                });
+
+        ConditionVariable taskFinished = runTaskAsync(task);
+
+        verifyAndUnblockAgentCalls(3);
+        taskFinished.block();
+        InOrder inOrder = inOrder(transportMock.transport);
+        inOrder.verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+        inOrder.verify(transportMock.transport).finishBackup();
+        verify(transportMock.transport, never())
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @Test
+    public void
+            testRunTask_whenMarkCancelDuringTransportPerformBackupForFirstPackage_callsTransportForFirstPackageButNotForSecond()
+                    throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentsWithData(PACKAGE_1, PACKAGE_2);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        when(transportMock.transport.performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(), anyInt()))
+                .thenAnswer(
+                        invocation -> {
+                            runInWorkerThread(task::markCancel);
+                            return BackupTransport.TRANSPORT_OK;
+                        });
+
+        ConditionVariable taskFinished = runTaskAsync(task);
+
+        verifyAndUnblockAgentCalls(2);
+        taskFinished.block();
+        InOrder inOrder = inOrder(transportMock.transport);
+        inOrder.verify(transportMock.transport)
+                .performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt());
+        inOrder.verify(transportMock.transport).finishBackup();
+        verify(transportMock.transport, never())
+                .performBackup(argThat(packageInfo(PACKAGE_2)), any(), anyInt());
+    }
+
+    @Test
+    public void testRunTask_afterMarkCancel_doesNotCallAgentOrTransport() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        AgentMock agentMock = setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        task.markCancel();
+
+        runTask(task);
+
+        verify(agentMock.agent, never()).onBackup(any(), any(), any());
+        verify(transportMock.transport, never()).performBackup(any(), any(), anyInt());
+        verify(transportMock.transport, never()).finishBackup();
+    }
+
+    @Test
+    public void testWaitCancel_afterCancelledTaskFinished_returns() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        task.markCancel();
+        runTask(task);
+
+        task.waitCancel();
+    }
+
+    @Test
+    public void testWaitCancel_whenMarkCancelDuringAgentOnBackup_unregistersTask()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        AgentMock agentMock = setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        agentOnBackupDo(
+                agentMock,
+                (oldState, dataOutput, newState) -> {
+                    writeData(dataOutput, "key", "data".getBytes());
+                    writeState(newState, "newState".getBytes());
+                    runInWorkerThread(task::markCancel);
+                });
+        ConditionVariable taskFinished = runTaskAsync(task);
+        verifyAndUnblockAgentCalls(1);
+        boolean backupInProgressDuringBackup = mBackupManagerService.isBackupOperationInProgress();
+        assertThat(backupInProgressDuringBackup).isTrue();
+        verifyAndUnblockAgentCalls(1);
+
+        task.waitCancel();
+
+        boolean backupInProgressAfterWaitCancel =
+                mBackupManagerService.isBackupOperationInProgress();
+        assertThat(backupInProgressDuringBackup).isTrue();
+        assertThat(backupInProgressAfterWaitCancel).isFalse();
+        taskFinished.block();
+    }
+
+    @Test
+    public void testMarkCancel_afterTaskFinished_returns() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        runTask(task);
+
+        task.markCancel();
+    }
+
+    @Test
+    public void testHandleCancel_callsMarkCancelAndWaitCancel() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = spy(createKeyValueBackupTask(transportMock, PACKAGE_1));
+        doNothing().when(task).waitCancel();
+
+        task.handleCancel(true);
+
+        InOrder inOrder = inOrder(task);
+        inOrder.verify(task).markCancel();
+        inOrder.verify(task).waitCancel();
+    }
+
+    @Test
+    public void testHandleCancel_whenCancelAllFalse_throws() throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        setUpAgentWithData(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+
+        expectThrows(IllegalArgumentException.class, () -> task.handleCancel(false));
+    }
+
+    /** Do not update backup token if no data was moved. */
+    @Test
+    public void testRunTask_whenNoDataToBackupOnFirstBackup_doesNotUpdateCurrentToken()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        mBackupManagerService.setCurrentToken(0L);
+        when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L);
+        // Set up agent with no data.
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1);
+
+        runTask(task);
+
+        assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(0L);
+    }
+
+    /** Do not inform transport of an empty backup if the app hasn't backed up before */
+    @Test
+    public void testRunTask_whenNoDataToBackupOnFirstBackup_doesNotTellTransportOfBackup()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        mBackupManagerService.setCurrentToken(0L);
+        when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L);
+        setUpAgent(PACKAGE_1);
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1);
+
+        runTask(task);
+
+        verify(transportMock.transport, never())
+                .performBackup(
+                        argThat(packageInfo(PACKAGE_1)), any(ParcelFileDescriptor.class), anyInt());
+    }
+
+    /** Let the transport know if there are no changes for a KV backed-up package. */
+    @Test
+    public void testRunTask_whenBackupHasCompletedAndThenNoDataChanges_transportGetsNotified()
+            throws Exception {
+        TransportMock transportMock = setUpInitializedTransport(mTransport);
+        when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L);
+        when(transportMock.transport.isAppEligibleForBackup(
+                        argThat(packageInfo(PACKAGE_1)), eq(false)))
+                .thenReturn(true);
+        when(transportMock.transport.isAppEligibleForBackup(
+                        argThat(packageInfo(PACKAGE_2)), eq(false)))
+                .thenReturn(true);
+        setUpAgentWithData(PACKAGE_1);
+        setUpAgentWithData(PACKAGE_2);
+
+        PackageInfo endSentinel = new PackageInfo();
+        endSentinel.packageName = KeyValueBackupTask.NO_DATA_END_SENTINEL;
+
+        // Perform First Backup run, which should backup both packages
+        KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2);
+        runTask(task);
+        InOrder order = Mockito.inOrder(transportMock.transport);
+        order.verify(transportMock.transport)
+                .performBackup(
+                        argThat(packageInfo(PACKAGE_1)),
+                        any(),
+                        eq(BackupTransport.FLAG_NON_INCREMENTAL));
+        order.verify(transportMock.transport).finishBackup();
+        order.verify(transportMock.transport)
+                .performBackup(
+                        argThat(packageInfo(PACKAGE_2)),
+                        any(),
+                        eq(BackupTransport.FLAG_NON_INCREMENTAL));
+        order.verify(transportMock.transport).finishBackup();
+
+        // Run again with new data for package 1, but nothing new for package 2
+        task = createKeyValueBackupTask(transportMock, PACKAGE_1);
+        runTask(task);
+
+        // Now for the second run we performed one incremental backup (package 1) and
+        // made one "no change" call (package 2) before sending the end sentinel.
+        order.verify(transportMock.transport)
+                .performBackup(
+                        argThat(packageInfo(PACKAGE_1)),
+                        any(),
+                        eq(BackupTransport.FLAG_INCREMENTAL));
+        order.verify(transportMock.transport).finishBackup();
+        order.verify(transportMock.transport)
+                .performBackup(
+                        argThat(packageInfo(PACKAGE_2)),
+                        any(),
+                        eq(BackupTransport.FLAG_DATA_NOT_CHANGED));
+        order.verify(transportMock.transport).finishBackup();
+        order.verify(transportMock.transport)
+                .performBackup(
+                        argThat(packageInfo(endSentinel)),
+                        any(),
+                        eq(BackupTransport.FLAG_DATA_NOT_CHANGED));
+        order.verify(transportMock.transport).finishBackup();
+        order.verifyNoMoreInteractions();
+    }
+
+    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 ConditionVariable runTaskAsync(KeyValueBackupTask task) {
+        return runInWorkerThreadAsync(task::run);
+    }
+
+    private static ConditionVariable runInWorkerThreadAsync(ThrowingRunnable runnable) {
+        ConditionVariable finished = new ConditionVariable(false);
+        new Thread(
+                        () -> {
+                            uncheck(runnable);
+                            finished.open();
+                        },
+                        "test-worker-thread")
+                .start();
+        return finished;
+    }
+
+    private static void runInWorkerThread(ThrowingRunnable runnable) {
+        runInWorkerThreadAsync(runnable).block();
+    }
+
+    /**
+     * If you have kicked-off the task with {@link #runTaskAsync(KeyValueBackupTask)}, call this to
+     * unblock the task thread that will be waiting for the agent's {@link
+     * IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor, ParcelFileDescriptor, long,
+     * IBackupCallback, int)}.
+     *
+     * @param times The number of {@link IBackupAgent#doBackup(ParcelFileDescriptor,
+     *     ParcelFileDescriptor, ParcelFileDescriptor, long, IBackupCallback, int)} calls. Remember
+     *     to count PM calls.
+     */
+    private void verifyAndUnblockAgentCalls(int times)
+            throws InterruptedException, TimeoutException {
+        // HACK: IBackupAgent.doBackup() posts a runnable to the front of the main-thread queue and
+        // immediately waits for its execution. In Robolectric, if we are in the main-thread this
+        // runnable is executed inline (this is called unpaused looper), that's why when we run the
+        // task in the main-thread (runTask() as opposed to runTaskAsync()) we don't need to call
+        // this method. However, if we are not in the main-thread nobody executes the runnable for
+        // us, thus IBackupAgent code will be stuck waiting for someone to execute the runnable.
+        // This method waits for that *specific* runnable, identifying it via class name, and then
+        // idles the main looper (for 0 seconds because it's posted at the front of the queue),
+        // which executes the method.
+        for (int i = 0; i < times; i++) {
+            waitUntil(() -> messagesInLooper(mMainLooper, this::isSharedPrefsSynchronizer) > 0);
+            mShadowMainLooper.idle();
+        }
+    }
+
+    private boolean isSharedPrefsSynchronizer(@Nullable Message message) {
+        String className = BACKUP_AGENT_SHARED_PREFS_SYNCHRONIZER_CLASS;
+        return message != null
+                && message.getCallback() != null
+                && className.equals(message.getCallback().getClass().getName());
+    }
+
+    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<AgentMock> setUpAgents(PackageData... packageNames) {
+        return Stream.of(packageNames).map(this::setUpAgent).collect(toList());
+    }
+
+    private AgentMock setUpAgent(PackageData packageData) {
+        try {
+            String packageName = packageData.packageName;
+            mPackageManager.setApplicationEnabledSetting(
+                    packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
+            PackageInfo packageInfo = getPackageInfo(packageData);
+            mShadowPackageManager.installPackage(packageInfo);
+            ShadowApplicationPackageManager.addInstalledPackage(packageName, packageInfo);
+            mContext.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<AgentMock> 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(
+            TransportMock transportMock, PackageData... packages) {
+        return createKeyValueBackupTask(transportMock, false, packages);
+    }
+
+    private KeyValueBackupTask createKeyValueBackupTask(
+            TransportMock transportMock, boolean nonIncremental, PackageData... packages) {
+        List<String> queue =
+                Stream.of(packages).map(packageData -> packageData.packageName).collect(toList());
+        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,
+                        transportMock.transportClient,
+                        transportMock.transportData.transportDirName,
+                        queue,
+                        mOldJournal,
+                        mReporter,
+                        mListener,
+                        emptyList(),
+                        /* userInitiated */ false,
+                        nonIncremental);
+        mBackupManager.setUp(mBackupHandler, task);
+        return task;
+    }
+
+    private PackageManagerBackupAgent createPmAgent() {
+        PackageManagerBackupAgent pmAgent =
+                new PackageManagerBackupAgent(mApplication.getPackageManager(), USER_ID);
+        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> 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 PackageInfo} whose package name is {@code packageData.packageName}. */
+    private static ArgumentMatcher<PackageInfo> packageInfo(PackageInfo 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 && packageInfo.packageName.equals(packageInfo.packageName);
+    }
+
+    /** Matches {@link ApplicationInfo} whose package name is {@code packageData.packageName}. */
+    private static ArgumentMatcher<ApplicationInfo> applicationInfo(PackageData packageData) {
+        return applicationInfo ->
+                applicationInfo != null
+                        && packageData.packageName.equals(applicationInfo.packageName);
+    }
+
+    private static ArgumentMatcher<BackupDataOutput> 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:
+     *
+     * <ul>
+     *   <li>The transport being initialized with {@link IBackupTransport#initializeDevice()}
+     *   <li>{@link UserBackupManagerService#resetBackupState(File)} being called, which will:
+     *       <ul>
+     *         <li>Reset processed packages journal.
+     *         <li>Reset current token to 0.
+     *         <li>Delete state files.
+     *         <li>Mark data changed for every key-value participant.
+     *       </ul>
+     * </ul>
+     */
+    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
+     * UserBackupManagerService#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}.
+     *
+     * <p>Note that for throwing agents this will simulate a local agent (the exception will be
+     * thrown in our stack), use {@link #remoteAgentOnBackupThrows(AgentMock, BackupAgentOnBackup)}
+     * if you want to simulate a remote agent.
+     */
+    private static void agentOnBackupDo(AgentMock agentMock, BackupAgentOnBackup function)
+            throws Exception {
+        agentOnBackupDo(
+                agentMock.agent,
+                (oldState, dataOutput, newState) -> {
+                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                    transferStreamedData(
+                            new FileInputStream(oldState.getFileDescriptor()), outputStream);
+                    agentMock.oldState = outputStream.toByteArray();
+                    agentMock.oldStateHistory.add(agentMock.oldState);
+                    function.onBackup(oldState, dataOutput, newState);
+                });
+    }
+
+    /**
+     * Implements {@code function} for {@link BackupAgent#onBackup(ParcelFileDescriptor,
+     * BackupDataOutput, ParcelFileDescriptor)} of {@code agentMock}.
+     *
+     * @see #agentOnBackupDo(AgentMock, BackupAgentOnBackup)
+     * @see #remoteAgentOnBackupThrows(AgentMock, BackupAgentOnBackup)
+     */
+    private static void agentOnBackupDo(BackupAgent backupAgent, BackupAgentOnBackup function)
+            throws IOException {
+        doAnswer(function).when(backupAgent).onBackup(any(), any(), any());
+    }
+
+    /**
+     * Use this method to simulate a remote agent throwing. We catch the exception thrown, thus
+     * simulating a one-way call. It also populates {@link AgentMock#oldState}.
+     *
+     * @param agentMock The Agent mock.
+     * @param function A function that throws, otherwise the test will fail.
+     */
+    // TODO: Remove when RemoteCall spins up a dedicated thread for calls
+    private static void remoteAgentOnBackupThrows(AgentMock agentMock, BackupAgentOnBackup function)
+            throws Exception {
+        agentOnBackupDo(agentMock, function);
+        doAnswer(
+                        invocation -> {
+                            try {
+                                invocation.callRealMethod();
+                                fail("Agent method expected to throw");
+                            } catch (RuntimeException e) {
+                                // This silences the exception just like a one-way call would, the
+                                // normal completion via IBackupCallback binder still happens, check
+                                // finally() block of IBackupAgent.doBackup().
+                            }
+                            return null;
+                        })
+                .when(agentMock.agentBinder)
+                .doBackup(any(), any(), any(), anyLong(), any(), anyInt());
+    }
+
+    /**
+     * 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<Integer> 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<Integer> 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 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<String> packages = (journal == null) ? emptyList() : journal.getPackages();
+        assertThat(packages).doesNotContain(packageName);
+    }
+
+    private void assertTaskReverted(TransportMock transportMock, PackageData... packages)
+            throws RemoteException, IOException {
+        verify(transportMock.transport).requestBackupTime();
+        assertBackupPendingFor(packages);
+        assertThat(KeyValueBackupJob.isScheduled(mBackupManagerService.getUserId())).isTrue();
+    }
+
+    private void assertBackupPendingFor(PackageData... packages) throws IOException {
+        for (PackageData packageData : packages) {
+            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... packages) throws IOException {
+        for (PackageData packageData : packages) {
+            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);
+            // Also verifying BMS is never called since for some cases the package wouldn't be
+            // pending for other reasons (for example it's not eligible for backup). Regardless of
+            // these reasons, we shouldn't mark them as pending backup (call dataChangedImpl()).
+            verify(mBackupManagerService, never()).dataChangedImpl(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);
+    }
+
+    private void assertCleansUpFilesAndAgent(TransportData transport, PackageData packageData) {
+        assertCleansUpFiles(transport, packageData);
+        verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(packageData)));
+    }
+
+    private void assertCleansUpFiles(TransportData transport, PackageData packageData) {
+        assertThat(Files.exists(getTemporaryStateFile(transport, packageData))).isFalse();
+        assertThat(Files.exists(getStagingFile(packageData))).isFalse();
+    }
+
+    /**
+     * Put conditions that should *always* be true after task execution.
+     *
+     * <p>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> {
+        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<byte[]> 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, USER_ID);
+            mException = exception;
+        }
+
+        @Override
+        public void onBackup(
+                ParcelFileDescriptor oldState,
+                BackupDataOutput data,
+                ParcelFileDescriptor newState) {
+            throw mException;
+        }
+    }
+}
diff --git a/com/android/server/backup/keyvalue/TaskException.java b/com/android/server/backup/keyvalue/TaskException.java
new file mode 100644
index 0000000..08d2895
--- /dev/null
+++ b/com/android/server/backup/keyvalue/TaskException.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.app.backup.BackupTransport;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * The key-value backup task has failed, no more packages will be processed and we shouldn't attempt
+ * any more backups now. These can be caused by transport failures (as opposed to agent failures).
+ *
+ * @see KeyValueBackupTask
+ * @see AgentException
+ */
+class TaskException extends BackupException {
+    private static final int DEFAULT_STATUS = BackupTransport.TRANSPORT_ERROR;
+
+    static TaskException stateCompromised() {
+        return new TaskException(/* stateCompromised */ true, DEFAULT_STATUS);
+    }
+
+    static TaskException stateCompromised(Exception cause) {
+        if (cause instanceof TaskException) {
+            TaskException exception = (TaskException) cause;
+            return new TaskException(cause, /* stateCompromised */ true, exception.getStatus());
+        }
+        return new TaskException(cause, /* stateCompromised */ true, DEFAULT_STATUS);
+    }
+
+    static TaskException forStatus(int status) {
+        Preconditions.checkArgument(
+                status != BackupTransport.TRANSPORT_OK, "Exception based on TRANSPORT_OK");
+        return new TaskException(/* stateCompromised */ false, status);
+    }
+
+    static TaskException causedBy(Exception cause) {
+        if (cause instanceof TaskException) {
+            return (TaskException) cause;
+        }
+        return new TaskException(cause, /* stateCompromised */ false, DEFAULT_STATUS);
+    }
+
+    static TaskException create() {
+        return new TaskException(/* stateCompromised */ false, DEFAULT_STATUS);
+    }
+
+    private final boolean mStateCompromised;
+    private final int mStatus;
+
+    private TaskException(Exception cause, boolean stateCompromised, int status) {
+        super(cause);
+        mStateCompromised = stateCompromised;
+        mStatus = status;
+    }
+
+    private TaskException(boolean stateCompromised, int status) {
+        mStateCompromised = stateCompromised;
+        mStatus = status;
+    }
+
+    boolean isStateCompromised() {
+        return mStateCompromised;
+    }
+
+    int getStatus() {
+        return mStatus;
+    }
+}
diff --git a/com/android/server/backup/keyvalue/TaskExceptionTest.java b/com/android/server/backup/keyvalue/TaskExceptionTest.java
new file mode 100644
index 0000000..3698b79
--- /dev/null
+++ b/com/android/server/backup/keyvalue/TaskExceptionTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.backup.BackupTransport;
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TaskExceptionTest {
+    @Test
+    public void testStateCompromised() {
+        TaskException exception = TaskException.stateCompromised();
+
+        assertThat(exception.isStateCompromised()).isTrue();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void testStateCompromised_whenCauseInstanceOfTaskException() {
+        Exception cause = TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+
+        TaskException exception = TaskException.stateCompromised(cause);
+
+        assertThat(exception.isStateCompromised()).isTrue();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+
+    @Test
+    public void testStateCompromised_whenCauseNotInstanceOfTaskException() {
+        Exception cause = new IOException();
+
+        TaskException exception = TaskException.stateCompromised(cause);
+
+        assertThat(exception.isStateCompromised()).isTrue();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+
+    @Test
+    public void testForStatus_whenTransportOk_throws() {
+        expectThrows(
+                IllegalArgumentException.class,
+                () -> TaskException.forStatus(BackupTransport.TRANSPORT_OK));
+    }
+
+    @Test
+    public void testForStatus_whenTransportNotInitialized() {
+        TaskException exception =
+                TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+
+        assertThat(exception.isStateCompromised()).isFalse();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+    }
+
+    @Test
+    public void testCausedBy_whenCauseInstanceOfTaskException_returnsCause() {
+        Exception cause = TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+
+        TaskException exception = TaskException.causedBy(cause);
+
+        assertThat(exception).isEqualTo(cause);
+    }
+
+    @Test
+    public void testCausedBy_whenCauseNotInstanceOfTaskException() {
+        Exception cause = new IOException();
+
+        TaskException exception = TaskException.causedBy(cause);
+
+        assertThat(exception).isNotEqualTo(cause);
+        assertThat(exception.isStateCompromised()).isFalse();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+        assertThat(exception.getCause()).isEqualTo(cause);
+    }
+
+    @Test
+    public void testCreate() {
+        TaskException exception = TaskException.create();
+
+        assertThat(exception.isStateCompromised()).isFalse();
+        assertThat(exception.getStatus()).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+    }
+
+    @Test
+    public void testIsStateCompromised_whenStateCompromised_returnsTrue() {
+        TaskException taskException = TaskException.stateCompromised();
+
+        boolean stateCompromised = taskException.isStateCompromised();
+
+        assertThat(stateCompromised).isTrue();
+    }
+
+    @Test
+    public void testIsStateCompromised_whenCreatedWithCreate_returnsFalse() {
+        TaskException taskException = TaskException.create();
+
+        boolean stateCompromised = taskException.isStateCompromised();
+
+        assertThat(stateCompromised).isFalse();
+    }
+
+    @Test
+    public void testGetStatus_whenStatusIsTransportPackageRejected() {
+        TaskException taskException =
+                TaskException.forStatus(BackupTransport.TRANSPORT_PACKAGE_REJECTED);
+
+        int status = taskException.getStatus();
+
+        assertThat(status).isEqualTo(BackupTransport.TRANSPORT_PACKAGE_REJECTED);
+    }
+
+    @Test
+    public void testGetStatus_whenStatusIsTransportNotInitialized() {
+        TaskException taskException =
+                TaskException.forStatus(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+
+        int status = taskException.getStatus();
+
+        assertThat(status).isEqualTo(BackupTransport.TRANSPORT_NOT_INITIALIZED);
+    }
+}
diff --git a/com/android/server/backup/params/AdbBackupParams.java b/com/android/server/backup/params/AdbBackupParams.java
new file mode 100644
index 0000000..5c1ba24
--- /dev/null
+++ b/com/android/server/backup/params/AdbBackupParams.java
@@ -0,0 +1,47 @@
+/*
+ * 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.params;
+
+import android.os.ParcelFileDescriptor;
+
+public class AdbBackupParams extends AdbParams {
+
+    public boolean includeApks;
+    public boolean includeObbs;
+    public boolean includeShared;
+    public boolean doWidgets;
+    public boolean allApps;
+    public boolean includeSystem;
+    public boolean doCompress;
+    public boolean includeKeyValue;
+    public String[] packages;
+
+    public AdbBackupParams(ParcelFileDescriptor output, boolean saveApks, boolean saveObbs,
+            boolean saveShared, boolean alsoWidgets, boolean doAllApps, boolean doSystem,
+            boolean compress, boolean doKeyValue, String[] pkgList) {
+        fd = output;
+        includeApks = saveApks;
+        includeObbs = saveObbs;
+        includeShared = saveShared;
+        doWidgets = alsoWidgets;
+        allApps = doAllApps;
+        includeSystem = doSystem;
+        doCompress = compress;
+        includeKeyValue = doKeyValue;
+        packages = pkgList;
+    }
+}
diff --git a/com/android/server/backup/params/AdbParams.java b/com/android/server/backup/params/AdbParams.java
new file mode 100644
index 0000000..392f40d
--- /dev/null
+++ b/com/android/server/backup/params/AdbParams.java
@@ -0,0 +1,38 @@
+/*
+ * 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.params;
+
+import android.app.backup.IFullBackupRestoreObserver;
+import android.os.ParcelFileDescriptor;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Parameters used by adbBackup() and adbRestore().
+ */
+public class AdbParams {
+
+    public ParcelFileDescriptor fd;
+    public final AtomicBoolean latch;
+    public IFullBackupRestoreObserver observer;
+    public String curPassword;     // filled in by the confirmation step
+    public String encryptPassword;
+
+    AdbParams() {
+        latch = new AtomicBoolean(false);
+    }
+}
diff --git a/com/android/server/backup/params/AdbRestoreParams.java b/com/android/server/backup/params/AdbRestoreParams.java
new file mode 100644
index 0000000..0142fe0
--- /dev/null
+++ b/com/android/server/backup/params/AdbRestoreParams.java
@@ -0,0 +1,26 @@
+/*
+ * 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.params;
+
+import android.os.ParcelFileDescriptor;
+
+public class AdbRestoreParams extends AdbParams {
+
+    public AdbRestoreParams(ParcelFileDescriptor input) {
+        fd = input;
+    }
+}
diff --git a/com/android/server/backup/params/BackupParams.java b/com/android/server/backup/params/BackupParams.java
new file mode 100644
index 0000000..2ba8ec1
--- /dev/null
+++ b/com/android/server/backup/params/BackupParams.java
@@ -0,0 +1,53 @@
+/*
+ * 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.params;
+
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IBackupObserver;
+
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
+
+import java.util.ArrayList;
+
+public class BackupParams {
+
+    public TransportClient transportClient;
+    public String dirName;
+    public ArrayList<String> kvPackages;
+    public ArrayList<String> fullPackages;
+    public IBackupObserver observer;
+    public IBackupManagerMonitor monitor;
+    public OnTaskFinishedListener listener;
+    public boolean userInitiated;
+    public boolean nonIncrementalBackup;
+
+    public BackupParams(TransportClient transportClient, String dirName,
+            ArrayList<String> kvPackages, ArrayList<String> fullPackages, IBackupObserver observer,
+            IBackupManagerMonitor monitor, OnTaskFinishedListener listener, boolean userInitiated,
+            boolean nonIncrementalBackup) {
+        this.transportClient = transportClient;
+        this.dirName = dirName;
+        this.kvPackages = kvPackages;
+        this.fullPackages = fullPackages;
+        this.observer = observer;
+        this.monitor = monitor;
+        this.listener = listener;
+        this.userInitiated = userInitiated;
+        this.nonIncrementalBackup = nonIncrementalBackup;
+    }
+}
diff --git a/com/android/server/backup/params/ClearParams.java b/com/android/server/backup/params/ClearParams.java
new file mode 100644
index 0000000..dc3bba0
--- /dev/null
+++ b/com/android/server/backup/params/ClearParams.java
@@ -0,0 +1,37 @@
+/*
+ * 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.params;
+
+import android.content.pm.PackageInfo;
+
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
+
+public class ClearParams {
+    public TransportClient transportClient;
+    public PackageInfo packageInfo;
+    public OnTaskFinishedListener listener;
+
+    public ClearParams(
+            TransportClient transportClient,
+            PackageInfo packageInfo,
+            OnTaskFinishedListener listener) {
+        this.transportClient = transportClient;
+        this.packageInfo = packageInfo;
+        this.listener = listener;
+    }
+}
diff --git a/com/android/server/backup/params/ClearRetryParams.java b/com/android/server/backup/params/ClearRetryParams.java
new file mode 100644
index 0000000..41b5641
--- /dev/null
+++ b/com/android/server/backup/params/ClearRetryParams.java
@@ -0,0 +1,27 @@
+/*
+ * 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.params;
+
+public class ClearRetryParams {
+    public String transportName;
+    public String packageName;
+
+    public ClearRetryParams(String transportName, String packageName) {
+        this.transportName = transportName;
+        this.packageName = packageName;
+    }
+}
diff --git a/com/android/server/backup/params/RestoreGetSetsParams.java b/com/android/server/backup/params/RestoreGetSetsParams.java
new file mode 100644
index 0000000..914e9ea
--- /dev/null
+++ b/com/android/server/backup/params/RestoreGetSetsParams.java
@@ -0,0 +1,43 @@
+/*
+ * 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.params;
+
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IRestoreObserver;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.restore.ActiveRestoreSession;
+import com.android.server.backup.transport.TransportClient;
+
+public class RestoreGetSetsParams {
+    public final TransportClient transportClient;
+    public final ActiveRestoreSession session;
+    public final IRestoreObserver observer;
+    public final IBackupManagerMonitor monitor;
+    public final OnTaskFinishedListener listener;
+
+    public RestoreGetSetsParams(TransportClient _transportClient, ActiveRestoreSession _session,
+            IRestoreObserver _observer, IBackupManagerMonitor _monitor,
+            OnTaskFinishedListener _listener) {
+        transportClient = _transportClient;
+        session = _session;
+        observer = _observer;
+        monitor = _monitor;
+        listener = _listener;
+    }
+}
diff --git a/com/android/server/backup/params/RestoreParams.java b/com/android/server/backup/params/RestoreParams.java
new file mode 100644
index 0000000..a6fea6c
--- /dev/null
+++ b/com/android/server/backup/params/RestoreParams.java
@@ -0,0 +1,151 @@
+/*
+ * 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.params;
+
+import android.annotation.Nullable;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IRestoreObserver;
+import android.content.pm.PackageInfo;
+
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
+
+import java.util.Map;
+import java.util.Set;
+
+public class RestoreParams {
+    public final TransportClient transportClient;
+    public final IRestoreObserver observer;
+    public final IBackupManagerMonitor monitor;
+    public final long token;
+    @Nullable public final PackageInfo packageInfo;
+    public final int pmToken; // in post-install restore, the PM's token for this transaction
+    public final boolean isSystemRestore;
+    @Nullable public final String[] filterSet;
+    public final OnTaskFinishedListener listener;
+
+    /**
+     * No kill after restore.
+     */
+    public static RestoreParams createForSinglePackage(
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long token,
+            PackageInfo packageInfo,
+            OnTaskFinishedListener listener) {
+        return new RestoreParams(
+                transportClient,
+                observer,
+                monitor,
+                token,
+                packageInfo,
+                /* pmToken */ 0,
+                /* isSystemRestore */ false,
+                /* filterSet */ null,
+                listener);
+    }
+
+    /**
+     * Kill after restore.
+     */
+    public static RestoreParams createForRestoreAtInstall(
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long token,
+            String packageName,
+            int pmToken,
+            OnTaskFinishedListener listener) {
+        String[] filterSet = {packageName};
+        return new RestoreParams(
+                transportClient,
+                observer,
+                monitor,
+                token,
+                /* packageInfo */ null,
+                pmToken,
+                /* isSystemRestore */ false,
+                filterSet,
+                listener);
+    }
+
+    /**
+     * This is the form that Setup Wizard or similar restore UXes use.
+     */
+    public static RestoreParams createForRestoreAll(
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long token,
+            OnTaskFinishedListener listener) {
+        return new RestoreParams(
+                transportClient,
+                observer,
+                monitor,
+                token,
+                /* packageInfo */ null,
+                /* pmToken */ 0,
+                /* isSystemRestore */ true,
+                /* filterSet */ null,
+                listener);
+    }
+
+    /**
+     * Caller specifies whether is considered a system-level restore.
+     */
+    public static RestoreParams createForRestorePackages(
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long token,
+            String[] filterSet,
+            boolean isSystemRestore,
+            OnTaskFinishedListener listener) {
+        return new RestoreParams(
+                transportClient,
+                observer,
+                monitor,
+                token,
+                /* packageInfo */ null,
+                /* pmToken */ 0,
+                isSystemRestore,
+                filterSet,
+                listener);
+    }
+
+    private RestoreParams(
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long token,
+            @Nullable PackageInfo packageInfo,
+            int pmToken,
+            boolean isSystemRestore,
+            @Nullable String[] filterSet,
+            OnTaskFinishedListener listener) {
+        this.transportClient = transportClient;
+        this.observer = observer;
+        this.monitor = monitor;
+        this.token = token;
+        this.packageInfo = packageInfo;
+        this.pmToken = pmToken;
+        this.isSystemRestore = isSystemRestore;
+        this.filterSet = filterSet;
+        this.listener = listener;
+    }
+}
diff --git a/com/android/server/backup/remote/FutureBackupCallback.java b/com/android/server/backup/remote/FutureBackupCallback.java
new file mode 100644
index 0000000..1ea4249
--- /dev/null
+++ b/com/android/server/backup/remote/FutureBackupCallback.java
@@ -0,0 +1,39 @@
+/*
+ * 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.remote;
+
+import android.app.backup.IBackupCallback;
+import android.os.RemoteException;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * An implementation of {@link IBackupCallback} that completes the {@link CompletableFuture}
+ * provided in the constructor with a present {@link RemoteResult}.
+ */
+public class FutureBackupCallback extends IBackupCallback.Stub {
+    private final CompletableFuture<RemoteResult> mFuture;
+
+    FutureBackupCallback(CompletableFuture<RemoteResult> future) {
+        mFuture = future;
+    }
+
+    @Override
+    public void operationComplete(long result) throws RemoteException {
+        mFuture.complete(RemoteResult.of(result));
+    }
+}
diff --git a/com/android/server/backup/remote/FutureBackupCallbackTest.java b/com/android/server/backup/remote/FutureBackupCallbackTest.java
new file mode 100644
index 0000000..5ac26f4
--- /dev/null
+++ b/com/android/server/backup/remote/FutureBackupCallbackTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.remote;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.CompletableFuture;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class FutureBackupCallbackTest {
+    @Test
+    public void testOperationComplete_completesFuture() throws Exception {
+        CompletableFuture<RemoteResult> future = new CompletableFuture<>();
+        FutureBackupCallback callback = new FutureBackupCallback(future);
+
+        callback.operationComplete(7);
+
+        assertThat(future.get()).isEqualTo(RemoteResult.of(7));
+    }
+}
diff --git a/com/android/server/backup/remote/RemoteCall.java b/com/android/server/backup/remote/RemoteCall.java
new file mode 100644
index 0000000..3af9e1d
--- /dev/null
+++ b/com/android/server/backup/remote/RemoteCall.java
@@ -0,0 +1,149 @@
+/*
+ * 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.remote;
+
+import android.annotation.WorkerThread;
+import android.app.backup.IBackupCallback;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A wrapper that encapsulates an outbound call from the system process, converting an asynchronous
+ * operation into a synchronous operation with time-out and cancellation built-in. This was built to
+ * be able to call one-way binder methods that accept a {@link IBackupCallback} as a callback and
+ * handle the result inline.
+ *
+ * <p>Create one {@link RemoteCall} object providing the actual call in the form of a {@link
+ * RemoteCallable} that accepts a {@link IBackupCallback}. Perform the call by calling {@link
+ * #call()}, at which point {@link RemoteCall} will execute the callable providing an implementation
+ * of the callback that communicates the result back to this object. Even if the call returns
+ * straight away (which is the case for one-way methods) the method will only return when either the
+ * callback is called, time-out happens, or someone calls {@link #cancel()}.
+ *
+ * <p>This class was designed to have the method {@link #call()} called only once.
+ */
+// TODO: Kick-off callable in dedicated thread (because of local calls, which are synchronous)
+public class RemoteCall {
+    /**
+     * Creates a {@link RemoteCall} object with {@code callable} and {@code timeoutMs} and calls
+     * {@link #call()} on it immediately after.
+     *
+     * <p>Note that you won't be able to cancel the call, to do that construct an object regularly
+     * first, then use {@link #call()}.
+     *
+     * @see #RemoteCall(RemoteCallable, long)
+     * @see #call()
+     */
+    public static RemoteResult execute(RemoteCallable<IBackupCallback> callable, long timeoutMs)
+            throws RemoteException {
+        return new RemoteCall(callable, timeoutMs).call();
+    }
+
+    private final RemoteCallable<IBackupCallback> mCallable;
+    private final CompletableFuture<RemoteResult> mFuture;
+    private final long mTimeoutMs;
+
+    /**
+     * Creates a new {@link RemoteCall} object for a given callable.
+     *
+     * @param callable A function that signals its completion by calling {@link
+     *     IBackupCallback#operationComplete(long)} on the object provided as a parameter.
+     * @param timeoutMs The time in milliseconds after which {@link #call()} will return with {@link
+     *     RemoteResult#FAILED_TIMED_OUT} if the callable hasn't completed and no one canceled. The
+     *     time starts to be counted in {@link #call()}.
+     */
+    public RemoteCall(RemoteCallable<IBackupCallback> callable, long timeoutMs) {
+        this(false, callable, timeoutMs);
+    }
+
+    /**
+     * Same as {@link #RemoteCall(RemoteCallable, long)} but with parameter {@code cancelled}.
+     *
+     * @param cancelled Whether the call has already been canceled. It has the same effect of
+     *     calling {@link #cancel()} before {@link #call()}.
+     * @see #RemoteCall(RemoteCallable, long)
+     */
+    public RemoteCall(boolean cancelled, RemoteCallable<IBackupCallback> callable, long timeoutMs) {
+        mCallable = callable;
+        mTimeoutMs = timeoutMs;
+        mFuture = new CompletableFuture<>();
+        if (cancelled) {
+            cancel();
+        }
+    }
+
+    /**
+     * Kicks-off the callable provided in the constructor and blocks before returning, waiting for
+     * the first of these to happen:
+     *
+     * <ul>
+     *   <li>The callback passed to {@link RemoteCallable} is called with the result. We return a
+     *       present {@link RemoteResult} with the result.
+     *   <li>Time-out happens. We return {@link RemoteResult#FAILED_TIMED_OUT}.
+     *   <li>Someone calls {@link #cancel()} on this object. We return {@link
+     *       RemoteResult#FAILED_CANCELLED}.
+     * </ul>
+     *
+     * <p>This method can't be called from the main thread and was designed to be called only once.
+     *
+     * @return A {@link RemoteResult} with the result of the operation.
+     * @throws RemoteException If the callable throws it.
+     */
+    @WorkerThread
+    public RemoteResult call() throws RemoteException {
+        // If called on the main-thread we would never get a time-out != 0
+        Preconditions.checkState(
+                !Looper.getMainLooper().isCurrentThread(), "Can't call call() on main thread");
+
+        if (!mFuture.isDone()) {
+            if (mTimeoutMs == 0L) {
+                timeOut();
+            } else {
+                Handler.getMain().postDelayed(this::timeOut, mTimeoutMs);
+                mCallable.call(new FutureBackupCallback(mFuture));
+            }
+        }
+        try {
+            return mFuture.get();
+        } catch (InterruptedException e) {
+            return RemoteResult.FAILED_THREAD_INTERRUPTED;
+        } catch (ExecutionException e) {
+            throw new IllegalStateException("Future unexpectedly completed with an exception");
+        }
+    }
+
+    /**
+     * Attempts to cancel the operation. It will only be successful if executed before the callback
+     * is called and before the time-out.
+     *
+     * <p>This method can be called from any thread, any time, including the same thread that called
+     * {@link #call()} (which is obviously only possible if the former is called before the latter).
+     */
+    public void cancel() {
+        mFuture.complete(RemoteResult.FAILED_CANCELLED);
+    }
+
+    private void timeOut() {
+        mFuture.complete(RemoteResult.FAILED_TIMED_OUT);
+    }
+}
diff --git a/com/android/server/backup/remote/RemoteCallTest.java b/com/android/server/backup/remote/RemoteCallTest.java
new file mode 100644
index 0000000..7ec2a4e
--- /dev/null
+++ b/com/android/server/backup/remote/RemoteCallTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.remote;
+
+import static com.android.server.backup.testing.TestUtils.runToEndOfTasks;
+import static com.android.server.backup.testing.TestUtils.uncheck;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.backup.IBackupCallback;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.testing.TestUtils.ThrowingRunnable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class RemoteCallTest {
+    /** A {@link RemoteCallable} that calls the callback immediately. */
+    private final RemoteCallable<IBackupCallback> IMMEDIATE_CALLABLE =
+            callback -> callback.operationComplete(0);
+
+    @Mock private RemoteCallable<IBackupCallback> mCallable;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testCall_whenCancelledAndImmediateCallableAndTimeOut0_returnsCancel()
+            throws Exception {
+        RemoteCall remoteCall = new RemoteCall(true, IMMEDIATE_CALLABLE, 0);
+
+        RemoteResult result = runInWorkerThread(remoteCall::call);
+
+        assertThat(result).isEqualTo(RemoteResult.FAILED_CANCELLED);
+    }
+
+    @Test
+    public void testCall_whenCancelledAndImmediateCallableAndTimeOut0_doesNotCallCallable()
+            throws Exception {
+        RemoteCall remoteCall = new RemoteCall(true, IMMEDIATE_CALLABLE, 0);
+
+        runInWorkerThread(remoteCall::call);
+
+        verify(mCallable, never()).call(any());
+    }
+
+    @Test
+    public void testCall_whenImmediateCallableAndTimeOut0AndCancelIsCalledBeforeCall_returnsCancel()
+            throws Exception {
+        RemoteCall remoteCall = new RemoteCall(IMMEDIATE_CALLABLE, 0);
+        remoteCall.cancel();
+
+        RemoteResult result = runInWorkerThread(remoteCall::call);
+
+        assertThat(result).isEqualTo(RemoteResult.FAILED_CANCELLED);
+    }
+
+    @Test
+    public void
+            testCall_whenImmediateCallableAndTimeOut0AndCancelIsCalledBeforeCall_doesNotCallCallable()
+                    throws Exception {
+        RemoteCall remoteCall = new RemoteCall(IMMEDIATE_CALLABLE, 0);
+        remoteCall.cancel();
+
+        runInWorkerThread(remoteCall::call);
+
+        verify(mCallable, never()).call(any());
+    }
+
+    @Test
+    public void testCall_whenImmediateCallableAndTimeOut0_returnsTimeOut() throws Exception {
+        RemoteCall remoteCall = new RemoteCall(IMMEDIATE_CALLABLE, 0);
+
+        RemoteResult result = runInWorkerThread(remoteCall::call);
+
+        assertThat(result).isEqualTo(RemoteResult.FAILED_TIMED_OUT);
+    }
+
+    @Test
+    public void testCall_whenTimeOut0_doesNotCallCallable() throws Exception {
+        RemoteCall remoteCall = new RemoteCall(mCallable, 0);
+
+        runInWorkerThread(remoteCall::call);
+
+        verify(mCallable, never()).call(any());
+    }
+
+    @Test
+    public void testCall_whenTimesOutBeforeCallbackIsCalled_returnsTimeOut() throws Exception {
+        ConditionVariable scheduled = new ConditionVariable(false);
+        RemoteCall remoteCall =
+                new RemoteCall(
+                        callback -> {
+                            postDelayed(
+                                    Handler.getMain(), () -> callback.operationComplete(0), 1000);
+                            scheduled.open();
+                        },
+                        500);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        // Method runToEndOfTasks() will execute what was posted to the main handler, which is the
+        // completion of the callback and the time-out (that was scheduled by RemoteCall). But to be
+        // able to execute everything we have to ensure that runToEndOfTasks() is called *after*
+        // everything has been scheduled, that's why we use the condition variable scheduled, that
+        // is set to true (i.e. opened) when everything is scheduled, allowing us to run the tasks.
+        scheduled.block();
+        runToEndOfTasks(Looper.getMainLooper());
+        assertThat(result.get()).isEqualTo(RemoteResult.FAILED_TIMED_OUT);
+    }
+
+    @Test
+    public void testCall_whenTimesOutBeforeCancelIsCalled_returnsTimeOut() throws Exception {
+        ConditionVariable scheduled = new ConditionVariable(false);
+        RemoteCall remoteCall = new RemoteCall(callback -> scheduled.open(), 500);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        scheduled.block();
+        runToEndOfTasks(Looper.getMainLooper());
+        remoteCall.cancel();
+        assertThat(result.get()).isEqualTo(RemoteResult.FAILED_TIMED_OUT);
+    }
+
+    @Test
+    public void testCall_whenCallbackIsCalledBeforeTimeOut_returnsResult() throws Exception {
+        ConditionVariable scheduled = new ConditionVariable(false);
+        RemoteCall remoteCall =
+                new RemoteCall(
+                        callback -> {
+                            postDelayed(
+                                    Handler.getMain(), () -> callback.operationComplete(3), 500);
+                            scheduled.open();
+                        },
+                        1000);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        scheduled.block();
+        runToEndOfTasks(Looper.getMainLooper());
+        assertThat(result.get()).isEqualTo(RemoteResult.of(3));
+    }
+
+    @Test
+    public void testCall_whenCallbackIsCalledBeforeCancel_returnsResult() throws Exception {
+        CompletableFuture<IBackupCallback> callbackFuture = new CompletableFuture<>();
+        RemoteCall remoteCall = new RemoteCall(callbackFuture::complete, 1000);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        // callbackFuture.get() will return when callable is executed (i.e. inside
+        // remoteCall.call()), at which point we can complete it.
+        IBackupCallback callback = callbackFuture.get();
+        callback.operationComplete(3);
+        remoteCall.cancel();
+        assertThat(result.get()).isEqualTo(RemoteResult.of(3));
+    }
+
+    @Test
+    public void testCall_whenCancelIsCalledBeforeCallbackButAfterCall_returnsCancel()
+            throws Exception {
+        CompletableFuture<IBackupCallback> callbackFuture = new CompletableFuture<>();
+        RemoteCall remoteCall = new RemoteCall(callbackFuture::complete, 1000);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        IBackupCallback callback = callbackFuture.get();
+        remoteCall.cancel();
+        callback.operationComplete(3);
+        assertThat(result.get()).isEqualTo(RemoteResult.FAILED_CANCELLED);
+    }
+
+    @Test
+    public void testCall_whenCancelIsCalledBeforeTimeOutButAfterCall_returnsCancel()
+            throws Exception {
+        ConditionVariable scheduled = new ConditionVariable(false);
+        RemoteCall remoteCall = new RemoteCall(callback -> scheduled.open(), 1000);
+
+        Future<RemoteResult> result = runInWorkerThreadAsync(remoteCall::call);
+
+        scheduled.block();
+        remoteCall.cancel();
+        runToEndOfTasks(Looper.getMainLooper());
+        assertThat(result.get()).isEqualTo(RemoteResult.FAILED_CANCELLED);
+    }
+
+    @Test
+    public void testExecute_whenCallbackIsCalledBeforeTimeout_returnsResult() throws Exception {
+        RemoteResult result =
+                runInWorkerThread(
+                        () -> RemoteCall.execute(callback -> callback.operationComplete(3), 1000));
+
+        assertThat(result.get()).isEqualTo(3);
+    }
+
+    @Test
+    public void testExecute_whenTimesOutBeforeCallback_returnsTimeOut() throws Exception {
+        ConditionVariable scheduled = new ConditionVariable(false);
+
+        Future<RemoteResult> result =
+                runInWorkerThreadAsync(
+                        () ->
+                                RemoteCall.execute(
+                                        callback -> {
+                                            postDelayed(
+                                                    Handler.getMain(),
+                                                    () -> callback.operationComplete(0),
+                                                    1000);
+                                            scheduled.open();
+                                        },
+                                        500));
+
+        scheduled.block();
+        runToEndOfTasks(Looper.getMainLooper());
+        assertThat(result.get()).isEqualTo(RemoteResult.FAILED_TIMED_OUT);
+    }
+
+    private static <T> Future<T> runInWorkerThreadAsync(Callable<T> supplier) {
+        CompletableFuture<T> future = new CompletableFuture<>();
+        new Thread(() -> future.complete(uncheck(supplier)), "test-worker-thread").start();
+        return future;
+    }
+
+    private static <T> T runInWorkerThread(Callable<T> supplier) throws Exception {
+        return runInWorkerThreadAsync(supplier).get();
+    }
+
+    /** Unchecked version of {@link Handler#postDelayed(Runnable, long)}. */
+    private static void postDelayed(Handler handler, ThrowingRunnable runnable, long delayMillis) {
+        handler.postDelayed(() -> uncheck(runnable), delayMillis);
+    }
+}
diff --git a/com/android/server/backup/remote/RemoteCallable.java b/com/android/server/backup/remote/RemoteCallable.java
new file mode 100644
index 0000000..d2671ff
--- /dev/null
+++ b/com/android/server/backup/remote/RemoteCallable.java
@@ -0,0 +1,28 @@
+/*
+ * 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.remote;
+
+import android.os.RemoteException;
+
+/**
+ * Implementations should perform a remote call in {@link #call(Object)}, possibly throwing {@link
+ * RemoteException}.
+ */
+@FunctionalInterface
+public interface RemoteCallable<T> {
+    void call(T input) throws RemoteException;
+}
diff --git a/com/android/server/backup/remote/RemoteResult.java b/com/android/server/backup/remote/RemoteResult.java
new file mode 100644
index 0000000..63c79db
--- /dev/null
+++ b/com/android/server/backup/remote/RemoteResult.java
@@ -0,0 +1,117 @@
+/*
+ * 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.remote;
+
+import android.annotation.IntDef;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents the result of a {@link RemoteCall}. It can be either {@link #FAILED_TIMED_OUT}, {@link
+ * #FAILED_CANCELLED}, {@link #FAILED_THREAD_INTERRUPTED} or a successful result, in which case
+ * {@link #get()} returns its value.
+ *
+ * <p>Use {@link #isPresent()} to check for successful result, or direct identity comparison to
+ * check for specific failures, like {@code result == RemoteResult.FAILED_CANCELLED}.
+ */
+public class RemoteResult {
+    public static final RemoteResult FAILED_TIMED_OUT = new RemoteResult(Type.FAILED_TIMED_OUT, 0);
+    public static final RemoteResult FAILED_CANCELLED = new RemoteResult(Type.FAILED_CANCELLED, 0);
+    public static final RemoteResult FAILED_THREAD_INTERRUPTED =
+            new RemoteResult(Type.FAILED_THREAD_INTERRUPTED, 0);
+
+    public static RemoteResult of(long value) {
+        return new RemoteResult(Type.SUCCESS, value);
+    }
+
+    @Type private final int mType;
+    private final long mValue;
+
+    private RemoteResult(@Type int type, long value) {
+        mType = type;
+        mValue = value;
+    }
+
+    public boolean isPresent() {
+        return mType == Type.SUCCESS;
+    }
+
+    /**
+     * Returns the value of this result.
+     *
+     * @throws IllegalStateException in case this is not a successful result.
+     */
+    public long get() {
+        Preconditions.checkState(isPresent(), "Can't obtain value of failed result");
+        return mValue;
+    }
+
+    @Override
+    public String toString() {
+        return "RemoteResult{" + toStringDescription() + "}";
+    }
+
+    private String toStringDescription() {
+        switch (mType) {
+            case Type.SUCCESS:
+                return Long.toString(mValue);
+            case Type.FAILED_TIMED_OUT:
+                return "FAILED_TIMED_OUT";
+            case Type.FAILED_CANCELLED:
+                return "FAILED_CANCELLED";
+            case Type.FAILED_THREAD_INTERRUPTED:
+                return "FAILED_THREAD_INTERRUPTED";
+            default:
+                throw new AssertionError("Unknown type");
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof RemoteResult)) {
+            return false;
+        }
+        RemoteResult that = (RemoteResult) o;
+        return mType == that.mType && mValue == that.mValue;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mValue);
+    }
+
+    @IntDef({
+        Type.SUCCESS,
+        Type.FAILED_TIMED_OUT,
+        Type.FAILED_CANCELLED,
+        Type.FAILED_THREAD_INTERRUPTED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface Type {
+        int SUCCESS = 0;
+        int FAILED_TIMED_OUT = 1;
+        int FAILED_CANCELLED = 2;
+        int FAILED_THREAD_INTERRUPTED = 3;
+    }
+}
diff --git a/com/android/server/backup/remote/RemoteResultTest.java b/com/android/server/backup/remote/RemoteResultTest.java
new file mode 100644
index 0000000..b9a77fb
--- /dev/null
+++ b/com/android/server/backup/remote/RemoteResultTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.remote;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class RemoteResultTest {
+    @Test
+    public void testIsPresent_whenNonFailedResult_returnsTrue() {
+        RemoteResult result = RemoteResult.of(3);
+
+        boolean isPresent = result.isPresent();
+
+        assertThat(isPresent).isTrue();
+    }
+
+    @Test
+    public void testIsPresent_whenTimeOutResult_returnsFalse() {
+        boolean timeOutIsPresent = RemoteResult.FAILED_TIMED_OUT.isPresent();
+
+        assertThat(timeOutIsPresent).isFalse();
+    }
+
+    @Test
+    public void testIsPresent_whenCancelledResult_returnsFalse() {
+        boolean cancelledIsPresent = RemoteResult.FAILED_CANCELLED.isPresent();
+
+        assertThat(cancelledIsPresent).isFalse();
+    }
+
+    @Test
+    public void testIsPresent_whenThreadInterruptedResult_returnsFalse() {
+        boolean threadInterruptedIsPresent = RemoteResult.FAILED_THREAD_INTERRUPTED.isPresent();
+
+        assertThat(threadInterruptedIsPresent).isFalse();
+    }
+
+    @Test
+    public void testGet_whenSuccessfulResult_returnsValue() {
+        RemoteResult result = RemoteResult.of(7);
+
+        long value = result.get();
+
+        assertThat(value).isEqualTo(7);
+    }
+
+    @Test
+    public void testGet_whenFailedResult_throws() {
+        RemoteResult result = RemoteResult.FAILED_TIMED_OUT;
+
+        expectThrows(IllegalStateException.class, result::get);
+    }
+
+    @Test
+    public void testToString() {
+        assertThat(RemoteResult.of(3).toString()).isEqualTo("RemoteResult{3}");
+        assertThat(RemoteResult.FAILED_TIMED_OUT.toString())
+                .isEqualTo("RemoteResult{FAILED_TIMED_OUT}");
+        assertThat(RemoteResult.FAILED_CANCELLED.toString())
+                .isEqualTo("RemoteResult{FAILED_CANCELLED}");
+        assertThat(RemoteResult.FAILED_THREAD_INTERRUPTED.toString())
+                .isEqualTo("RemoteResult{FAILED_THREAD_INTERRUPTED}");
+    }
+
+    @Test
+    public void testEquals() {
+        assertThat(RemoteResult.of(3).equals(RemoteResult.of(3))).isTrue();
+        assertThat(RemoteResult.of(3).equals(RemoteResult.of(7))).isFalse();
+        assertThat(RemoteResult.of(-1).equals(RemoteResult.of(1))).isFalse();
+        assertThat(RemoteResult.of(Long.MAX_VALUE).equals(RemoteResult.of(-1)))
+                .isFalse();
+        assertThat(RemoteResult.of(3).equals(RemoteResult.FAILED_TIMED_OUT)).isFalse();
+        assertThat(RemoteResult.of(3).equals("3")).isFalse();
+        assertThat(RemoteResult.of(3).equals(null)).isFalse();
+        assertThat(RemoteResult.FAILED_TIMED_OUT.equals(RemoteResult.FAILED_TIMED_OUT)).isTrue();
+        assertThat(RemoteResult.FAILED_TIMED_OUT.equals(RemoteResult.FAILED_CANCELLED)).isFalse();
+    }
+
+    /** @see Object#hashCode() */
+    @Test
+    public void testHashCode() {
+        RemoteResult result3 = RemoteResult.of(3);
+        assertThat(result3.hashCode()).isEqualTo(result3.hashCode());
+        assertThat(result3.hashCode()).isEqualTo(RemoteResult.of(3).hashCode());
+        assertThat(RemoteResult.FAILED_TIMED_OUT.hashCode())
+                .isEqualTo(RemoteResult.FAILED_TIMED_OUT.hashCode());
+        assertThat(RemoteResult.FAILED_CANCELLED.hashCode())
+                .isEqualTo(RemoteResult.FAILED_CANCELLED.hashCode());
+        assertThat(RemoteResult.FAILED_THREAD_INTERRUPTED.hashCode())
+                .isEqualTo(RemoteResult.FAILED_THREAD_INTERRUPTED.hashCode());
+    }
+}
diff --git a/com/android/server/backup/remote/ServiceBackupCallback.java b/com/android/server/backup/remote/ServiceBackupCallback.java
new file mode 100644
index 0000000..bfc97ae
--- /dev/null
+++ b/com/android/server/backup/remote/ServiceBackupCallback.java
@@ -0,0 +1,43 @@
+/*
+ * 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.remote;
+
+import android.app.backup.IBackupCallback;
+import android.app.backup.IBackupManager;
+import android.os.RemoteException;
+
+import com.android.server.backup.UserBackupManagerService;
+
+/**
+ * An implementation of {@link IBackupCallback} that routes the result to {@link
+ * UserBackupManagerService} via {@link IBackupManager#opComplete(int, long)} passing the token
+ * provided in the constructor.
+ */
+public class ServiceBackupCallback extends IBackupCallback.Stub {
+    private final IBackupManager mBackupManager;
+    private final int mToken;
+
+    public ServiceBackupCallback(IBackupManager backupManager, int token) {
+        mBackupManager = backupManager;
+        mToken = token;
+    }
+
+    @Override
+    public void operationComplete(long result) throws RemoteException {
+        mBackupManager.opComplete(mToken, result);
+    }
+}
diff --git a/com/android/server/backup/remote/ServiceBackupCallbackTest.java b/com/android/server/backup/remote/ServiceBackupCallbackTest.java
new file mode 100644
index 0000000..38a54da
--- /dev/null
+++ b/com/android/server/backup/remote/ServiceBackupCallbackTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.remote;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.backup.IBackupManager;
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class ServiceBackupCallbackTest {
+    @Test
+    public void testOperationComplete_callsBackupManagerOpComplete() throws Exception {
+        IBackupManager backupManager = mock(IBackupManager.class);
+        ServiceBackupCallback callback = new ServiceBackupCallback(backupManager, 0x68e);
+
+        callback.operationComplete(7);
+
+        verify(backupManager).opComplete(0x68e, 7);
+    }
+}
diff --git a/com/android/server/backup/restore/ActiveRestoreSession.java b/com/android/server/backup/restore/ActiveRestoreSession.java
new file mode 100644
index 0000000..5a57cdc
--- /dev/null
+++ b/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -0,0 +1,452 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_GET_RESTORE_SETS;
+import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.app.backup.RestoreSet;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Slog;
+
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.params.RestoreGetSetsParams;
+import com.android.server.backup.params.RestoreParams;
+import com.android.server.backup.transport.TransportClient;
+
+import java.util.function.BiFunction;
+
+/**
+ * Restore session.
+ */
+public class ActiveRestoreSession extends IRestoreSession.Stub {
+    private static final String TAG = "RestoreSession";
+
+    private final TransportManager mTransportManager;
+    private final String mTransportName;
+    private final UserBackupManagerService mBackupManagerService;
+    private final int mUserId;
+    @Nullable private final String mPackageName;
+    public RestoreSet[] mRestoreSets = null;
+    boolean mEnded = false;
+    boolean mTimedOut = false;
+
+    public ActiveRestoreSession(
+            UserBackupManagerService backupManagerService,
+            @Nullable String packageName,
+            String transportName) {
+        mBackupManagerService = backupManagerService;
+        mPackageName = packageName;
+        mTransportManager = backupManagerService.getTransportManager();
+        mTransportName = transportName;
+        mUserId = backupManagerService.getUserId();
+    }
+
+    public void markTimedOut() {
+        mTimedOut = true;
+    }
+
+    // --- Binder interface ---
+    public synchronized int getAvailableRestoreSets(IRestoreObserver observer,
+            IBackupManagerMonitor monitor) {
+        mBackupManagerService.getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP,
+                "getAvailableRestoreSets");
+        if (observer == null) {
+            throw new IllegalArgumentException("Observer must not be null");
+        }
+
+        if (mEnded) {
+            throw new IllegalStateException("Restore session already ended");
+        }
+
+        if (mTimedOut) {
+            Slog.i(TAG, "Session already timed out");
+            return -1;
+        }
+
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            TransportClient transportClient =
+                    mTransportManager.getTransportClient(
+                                    mTransportName, "RestoreSession.getAvailableRestoreSets()");
+            if (transportClient == null) {
+                Slog.w(TAG, "Null transport client getting restore sets");
+                return -1;
+            }
+
+            // We know we're doing legit work now, so halt the timeout
+            // until we're done.  It gets started again when the result
+            // comes in.
+            mBackupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+
+            UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
+            wakelock.acquire();
+
+            // Prevent lambda from leaking 'this'
+            TransportManager transportManager = mTransportManager;
+            OnTaskFinishedListener listener = caller -> {
+                    transportManager.disposeOfTransportClient(transportClient, caller);
+                    wakelock.release();
+            };
+            Message msg = mBackupManagerService.getBackupHandler().obtainMessage(
+                    MSG_RUN_GET_RESTORE_SETS,
+                    new RestoreGetSetsParams(transportClient, this, observer, monitor, listener));
+            mBackupManagerService.getBackupHandler().sendMessage(msg);
+            return 0;
+        } catch (Exception e) {
+            Slog.e(TAG, "Error in getAvailableRestoreSets", e);
+            return -1;
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    public synchronized int restoreAll(long token, IRestoreObserver observer,
+            IBackupManagerMonitor monitor) {
+        mBackupManagerService.getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP,
+                "performRestore");
+
+        if (DEBUG) {
+            Slog.d(TAG, "restoreAll token=" + Long.toHexString(token)
+                    + " observer=" + observer);
+        }
+
+        if (mEnded) {
+            throw new IllegalStateException("Restore session already ended");
+        }
+
+        if (mTimedOut) {
+            Slog.i(TAG, "Session already timed out");
+            return -1;
+        }
+
+        if (mRestoreSets == null) {
+            Slog.e(TAG, "Ignoring restoreAll() with no restore set");
+            return -1;
+        }
+
+        if (mPackageName != null) {
+            Slog.e(TAG, "Ignoring restoreAll() on single-package session");
+            return -1;
+        }
+
+        if (!mTransportManager.isTransportRegistered(mTransportName)) {
+            Slog.e(TAG, "Transport " + mTransportName + " not registered");
+            return -1;
+        }
+
+        synchronized (mBackupManagerService.getQueueLock()) {
+            for (int i = 0; i < mRestoreSets.length; i++) {
+                if (token == mRestoreSets[i].token) {
+                    long oldId = Binder.clearCallingIdentity();
+                    try {
+                        return sendRestoreToHandlerLocked(
+                                (transportClient, listener) ->
+                                        RestoreParams.createForRestoreAll(
+                                                transportClient,
+                                                observer,
+                                                monitor,
+                                                token,
+                                                listener),
+                                "RestoreSession.restoreAll()");
+                    } finally {
+                        Binder.restoreCallingIdentity(oldId);
+                    }
+                }
+            }
+        }
+
+        Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
+        return -1;
+    }
+
+    // Restores of more than a single package are treated as 'system' restores
+    public synchronized int restorePackages(long token, @Nullable IRestoreObserver observer,
+            @NonNull String[] packages, @Nullable IBackupManagerMonitor monitor) {
+        mBackupManagerService.getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.BACKUP,
+                "performRestore");
+
+        if (DEBUG) {
+            StringBuilder b = new StringBuilder(128);
+            b.append("restorePackages token=");
+            b.append(Long.toHexString(token));
+            b.append(" observer=");
+            if (observer == null) {
+                b.append("null");
+            } else {
+                b.append(observer.toString());
+            }
+            b.append(" monitor=");
+            if (monitor == null) {
+                b.append("null");
+            } else {
+                b.append(monitor.toString());
+            }
+            b.append(" packages=");
+            if (packages == null) {
+                b.append("null");
+            } else {
+                b.append('{');
+                boolean first = true;
+                for (String s : packages) {
+                    if (!first) {
+                        b.append(", ");
+                    } else {
+                        first = false;
+                    }
+                    b.append(s);
+                }
+                b.append('}');
+            }
+            Slog.d(TAG, b.toString());
+        }
+
+        if (mEnded) {
+            throw new IllegalStateException("Restore session already ended");
+        }
+
+        if (mTimedOut) {
+            Slog.i(TAG, "Session already timed out");
+            return -1;
+        }
+
+        if (mRestoreSets == null) {
+            Slog.e(TAG, "Ignoring restoreAll() with no restore set");
+            return -1;
+        }
+
+        if (mPackageName != null) {
+            Slog.e(TAG, "Ignoring restoreAll() on single-package session");
+            return -1;
+        }
+
+        if (!mTransportManager.isTransportRegistered(mTransportName)) {
+            Slog.e(TAG, "Transport " + mTransportName + " not registered");
+            return -1;
+        }
+
+        synchronized (mBackupManagerService.getQueueLock()) {
+            for (int i = 0; i < mRestoreSets.length; i++) {
+                if (token == mRestoreSets[i].token) {
+                    long oldId = Binder.clearCallingIdentity();
+                    try {
+                        return sendRestoreToHandlerLocked(
+                                (transportClient, listener) ->
+                                        RestoreParams.createForRestorePackages(
+                                                transportClient,
+                                                observer,
+                                                monitor,
+                                                token,
+                                                packages,
+                                                /* isSystemRestore */ packages.length > 1,
+                                                listener),
+                                "RestoreSession.restorePackages(" + packages.length + " packages)");
+                    } finally {
+                        Binder.restoreCallingIdentity(oldId);
+                    }
+                }
+            }
+        }
+
+        Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
+        return -1;
+    }
+
+    public synchronized int restorePackage(String packageName, IRestoreObserver observer,
+            IBackupManagerMonitor monitor) {
+        if (DEBUG) {
+            Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer
+                    + "monitor=" + monitor);
+        }
+
+        if (mEnded) {
+            throw new IllegalStateException("Restore session already ended");
+        }
+
+        if (mTimedOut) {
+            Slog.i(TAG, "Session already timed out");
+            return -1;
+        }
+
+        if (mPackageName != null) {
+            if (!mPackageName.equals(packageName)) {
+                Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName
+                        + " on session for package " + mPackageName);
+                return -1;
+            }
+        }
+
+        final PackageInfo app;
+        try {
+            app = mBackupManagerService.getPackageManager().getPackageInfoAsUser(
+                    packageName, 0, mUserId);
+        } catch (NameNotFoundException nnf) {
+            Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName);
+            return -1;
+        }
+
+        // If the caller is not privileged and is not coming from the target
+        // app's uid, throw a permission exception back to the caller.
+        int perm = mBackupManagerService.getContext().checkPermission(
+                android.Manifest.permission.BACKUP,
+                Binder.getCallingPid(), Binder.getCallingUid());
+        if ((perm == PackageManager.PERMISSION_DENIED) &&
+                (app.applicationInfo.uid != Binder.getCallingUid())) {
+            Slog.w(TAG, "restorePackage: bad packageName=" + packageName
+                    + " or calling uid=" + Binder.getCallingUid());
+            throw new SecurityException("No permission to restore other packages");
+        }
+
+        if (!mTransportManager.isTransportRegistered(mTransportName)) {
+            Slog.e(TAG, "Transport " + mTransportName + " not registered");
+            return -1;
+        }
+
+        // So far so good; we're allowed to try to restore this package.
+        long oldId = Binder.clearCallingIdentity();
+        try {
+            // Check whether there is data for it in the current dataset, falling back
+            // to the ancestral dataset if not.
+            long token = mBackupManagerService.getAvailableRestoreToken(packageName);
+            if (DEBUG) {
+                Slog.v(TAG, "restorePackage pkg=" + packageName
+                        + " token=" + Long.toHexString(token));
+            }
+
+            // If we didn't come up with a place to look -- no ancestral dataset and
+            // the app has never been backed up from this device -- there's nothing
+            // to do but return failure.
+            if (token == 0) {
+                if (DEBUG) {
+                    Slog.w(TAG, "No data available for this package; not restoring");
+                }
+                return -1;
+            }
+
+            return sendRestoreToHandlerLocked(
+                    (transportClient, listener) ->
+                            RestoreParams.createForSinglePackage(
+                                    transportClient,
+                                    observer,
+                                    monitor,
+                                    token,
+                                    app,
+                                    listener),
+                    "RestoreSession.restorePackage(" + packageName + ")");
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    public void setRestoreSets(RestoreSet[] restoreSets) {
+        mRestoreSets = restoreSets;
+    }
+
+    /**
+     * Returns 0 if operation sent or -1 otherwise.
+     */
+    private int sendRestoreToHandlerLocked(
+            BiFunction<TransportClient, OnTaskFinishedListener, RestoreParams> restoreParamsBuilder,
+            String callerLogString) {
+        TransportClient transportClient =
+                mTransportManager.getTransportClient(mTransportName, callerLogString);
+        if (transportClient == null) {
+            Slog.e(TAG, "Transport " + mTransportName + " got unregistered");
+            return -1;
+        }
+
+        // Stop the session timeout until we finalize the restore
+        Handler backupHandler = mBackupManagerService.getBackupHandler();
+        backupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+
+        UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
+        wakelock.acquire();
+        if (MORE_DEBUG) {
+            Slog.d(TAG, callerLogString);
+        }
+
+        // Prevent lambda from leaking 'this'
+        TransportManager transportManager = mTransportManager;
+        OnTaskFinishedListener listener = caller -> {
+                transportManager.disposeOfTransportClient(transportClient, caller);
+                wakelock.release();
+        };
+        Message msg = backupHandler.obtainMessage(MSG_RUN_RESTORE);
+        msg.obj = restoreParamsBuilder.apply(transportClient, listener);
+        backupHandler.sendMessage(msg);
+        return 0;
+    }
+
+    // Posted to the handler to tear down a restore session in a cleanly synchronized way
+    public class EndRestoreRunnable implements Runnable {
+
+        UserBackupManagerService mBackupManager;
+        ActiveRestoreSession mSession;
+
+        public EndRestoreRunnable(UserBackupManagerService manager, ActiveRestoreSession session) {
+            mBackupManager = manager;
+            mSession = session;
+        }
+
+        public void run() {
+            // clean up the session's bookkeeping
+            synchronized (mSession) {
+                mSession.mEnded = true;
+            }
+
+            // clean up the BackupManagerImpl side of the bookkeeping
+            // and cancel any pending timeout message
+            mBackupManager.clearRestoreSession(mSession);
+        }
+    }
+
+    public synchronized void endRestoreSession() {
+        if (DEBUG) {
+            Slog.d(TAG, "endRestoreSession");
+        }
+
+        if (mTimedOut) {
+            Slog.i(TAG, "Session already timed out");
+            return;
+        }
+
+        if (mEnded) {
+            throw new IllegalStateException("Restore session already ended");
+        }
+
+        mBackupManagerService.getBackupHandler().post(
+                new EndRestoreRunnable(mBackupManagerService, this));
+    }
+}
diff --git a/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/com/android/server/backup/restore/ActiveRestoreSessionTest.java
new file mode 100644
index 0000000..3fc421d
--- /dev/null
+++ b/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -0,0 +1,595 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.testing.BackupManagerServiceTestUtils.createBackupWakeLock;
+import static com.android.server.backup.testing.BackupManagerServiceTestUtils.setUpBackupManagerServiceBasics;
+import static com.android.server.backup.testing.BackupManagerServiceTestUtils.startBackupThread;
+import static com.android.server.backup.testing.TestUtils.assertEventLogged;
+import static com.android.server.backup.testing.TestUtils.assertEventNotLogged;
+import static com.android.server.backup.testing.TransportData.backupTransport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.AdditionalMatchers.aryEq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import android.app.Application;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IRestoreObserver;
+import android.app.backup.IRestoreSession;
+import android.app.backup.RestoreSet;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.EventLogTags;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.internal.BackupHandler;
+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.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowEventLog;
+import com.android.server.testing.shadows.ShadowPerformUnifiedRestoreTask;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowBinder;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.util.ArrayDeque;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowApplicationPackageManager.class,
+            ShadowBinder.class,
+            ShadowEventLog.class,
+            ShadowPerformUnifiedRestoreTask.class
+        })
+@Presubmit
+public class ActiveRestoreSessionTest {
+    private static final String PACKAGE_1 = "com.example.package1";
+    private static final String PACKAGE_2 = "com.example.package2";
+    private static final long TOKEN_1 = 1L;
+    private static final long TOKEN_2 = 2L;
+
+    @Mock private UserBackupManagerService mBackupManagerService;
+    @Mock private TransportManager mTransportManager;
+    @Mock private IRestoreObserver mObserver;
+    @Mock private IBackupManagerMonitor mMonitor;
+    private ShadowLooper mShadowBackupLooper;
+    private ShadowApplication mShadowApplication;
+    private UserBackupManagerService.BackupWakeLock mWakeLock;
+    private TransportData mTransport;
+    private RestoreSet mRestoreSet1;
+    private RestoreSet mRestoreSet2;
+    private ShadowPackageManager mShadowPackageManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mTransport = backupTransport();
+
+        mRestoreSet1 = new RestoreSet("name1", "device1", TOKEN_1);
+        mRestoreSet2 = new RestoreSet("name2", "device2", TOKEN_2);
+
+        Application application = RuntimeEnvironment.application;
+        mShadowApplication = shadowOf(application);
+
+        mShadowPackageManager = shadowOf(application.getPackageManager());
+
+        HandlerThread handlerThread = startBackupThread(null);
+        Looper backupLooper = handlerThread.getLooper();
+        mShadowBackupLooper = shadowOf(backupLooper);
+
+        Handler mainHandler = new Handler(Looper.getMainLooper());
+        BackupAgentTimeoutParameters agentTimeoutParameters =
+                new BackupAgentTimeoutParameters(mainHandler, application.getContentResolver());
+        agentTimeoutParameters.start();
+
+        // We need to mock BMS timeout parameters before initializing the BackupHandler since
+        // the constructor of BackupHandler relies on it.
+        when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
+        BackupHandler backupHandler = new BackupHandler(mBackupManagerService, handlerThread);
+
+        mWakeLock = createBackupWakeLock(application);
+
+        // TODO: Migrate to use spy(createInitializedBackupManagerService())
+        setUpBackupManagerServiceBasics(
+                mBackupManagerService,
+                application,
+                mTransportManager,
+                application.getPackageManager(),
+                backupHandler,
+                mWakeLock,
+                agentTimeoutParameters);
+        when(mBackupManagerService.getPendingRestores()).thenReturn(new ArrayDeque<>());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ShadowApplicationPackageManager.reset();
+        ShadowPerformUnifiedRestoreTask.reset();
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets_withoutPermission() throws Exception {
+        mShadowApplication.denyPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        expectThrows(
+                SecurityException.class,
+                () -> restoreSession.getAvailableRestoreSets(mObserver, mMonitor));
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets_forNullObserver() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        expectThrows(
+                RuntimeException.class,
+                () -> restoreSession.getAvailableRestoreSets(null, mMonitor));
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets_whenTransportNotRegistered() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport.unregistered());
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpTransport(mTransport);
+        when(transportMock.transport.getAvailableRestoreSets())
+                .thenReturn(new RestoreSet[] {mRestoreSet1, mRestoreSet2});
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mObserver)
+                .restoreSetsAvailable(aryEq(new RestoreSet[] {mRestoreSet1, mRestoreSet2}));
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        assertThat(mWakeLock.isHeld()).isFalse();
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets_forEmptyRestoreSets() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpTransport(mTransport);
+        when(transportMock.transport.getAvailableRestoreSets()).thenReturn(new RestoreSet[0]);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mObserver).restoreSetsAvailable(aryEq(new RestoreSet[0]));
+        assertEventNotLogged(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+    }
+
+    @Test
+    public void testGetAvailableRestoreSets_forNullRestoreSets() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpTransport(mTransport);
+        when(transportMock.transport.getAvailableRestoreSets()).thenReturn(null);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mObserver).restoreSetsAvailable(isNull());
+        assertEventLogged(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        assertThat(mWakeLock.isHeld()).isFalse();
+    }
+
+    @Test
+    public void testRestoreAll() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        doCallRealMethod().when(mBackupManagerService).setRestoreInProgress(anyBoolean());
+        when(mBackupManagerService.isRestoreInProgress()).thenCallRealMethod();
+        TransportMock transportMock = setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        assertThat(mWakeLock.isHeld()).isFalse();
+        assertThat(mBackupManagerService.isRestoreInProgress()).isFalse();
+        // Verify it created the task properly
+        ShadowPerformUnifiedRestoreTask shadowTask =
+                ShadowPerformUnifiedRestoreTask.getLastCreated();
+        assertThat(shadowTask.isFullSystemRestore()).isTrue();
+        assertThat(shadowTask.getFilterSet()).isNull();
+        assertThat(shadowTask.getPackage()).isNull();
+    }
+
+    @Test
+    public void testRestoreAll_whenNoRestoreSets() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+
+        int result = restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestoreAll_whenSinglePackageSession() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(PACKAGE_1, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestoreAll_whenSessionEnded() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+        restoreSession.endRestoreSession();
+        mShadowBackupLooper.runToEndOfTasks();
+
+        expectThrows(
+                IllegalStateException.class,
+                () -> restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor));
+    }
+
+    @Test
+    public void testRestoreAll_whenTransportNotRegistered() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport.unregistered());
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestoreAll_whenRestoreInProgress_addsToPendingRestores() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        when(mBackupManagerService.isRestoreInProgress()).thenReturn(true);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restoreAll(TOKEN_1, mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        assertThat(mBackupManagerService.getPendingRestores()).hasSize(1);
+    }
+
+    @Test
+    public void testRestorePackages_for2Packages() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        TransportMock transportMock = setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restorePackages(TOKEN_1, mObserver,
+                new String[] {PACKAGE_1, PACKAGE_2}, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        assertThat(mWakeLock.isHeld()).isFalse();
+        assertThat(mBackupManagerService.isRestoreInProgress()).isFalse();
+        ShadowPerformUnifiedRestoreTask shadowTask =
+                ShadowPerformUnifiedRestoreTask.getLastCreated();
+        assertThat(shadowTask.getFilterSet()).asList().containsExactly(PACKAGE_1, PACKAGE_2);
+        assertThat(shadowTask.getPackage()).isNull();
+    }
+
+    @Test
+    public void testRestorePackages_for2Packages_createsSystemRestoreTask() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1, PACKAGE_2},
+                mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(ShadowPerformUnifiedRestoreTask.getLastCreated().isFullSystemRestore()).isTrue();
+    }
+
+    @Test
+    public void testRestorePackages_for1Package() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1}, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        ShadowPerformUnifiedRestoreTask shadowTask =
+                ShadowPerformUnifiedRestoreTask.getLastCreated();
+        assertThat(shadowTask.getFilterSet()).asList().containsExactly(PACKAGE_1);
+        assertThat(shadowTask.getPackage()).isNull();
+    }
+
+    @Test
+    public void testRestorePackages_for1Package_createsNonSystemRestoreTask() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1}, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(ShadowPerformUnifiedRestoreTask.getLastCreated().isFullSystemRestore())
+                .isFalse();
+    }
+
+    @Test
+    public void testRestorePackages_whenNoRestoreSets() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+
+        int result = restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1},
+                mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestorePackages_whenSinglePackageSession() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(PACKAGE_1, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_2},
+                mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestorePackages_whenSessionEnded() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+        restoreSession.endRestoreSession();
+        mShadowBackupLooper.runToEndOfTasks();
+
+        expectThrows(
+                IllegalStateException.class,
+                () ->
+                        restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1},
+                                mMonitor));
+    }
+
+    @Test
+    public void testRestorePackages_whenTransportNotRegistered() throws Exception {
+        mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
+        setUpTransport(mTransport.unregistered());
+        IRestoreSession restoreSession =
+                createActiveRestoreSessionWithRestoreSets(null, mTransport, mRestoreSet1);
+
+        int result = restoreSession.restorePackages(TOKEN_1, mObserver, new String[] {PACKAGE_1},
+                mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestorePackage_whenCallerIsPackage() throws Exception {
+        // No need for BACKUP permission in this case
+        mShadowApplication.denyPermissions(android.Manifest.permission.BACKUP);
+        ShadowBinder.setCallingUid(1);
+        setUpPackage(PACKAGE_1, /* uid */ 1);
+        when(mBackupManagerService.getAvailableRestoreToken(PACKAGE_1)).thenReturn(TOKEN_1);
+        TransportMock transportMock = setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+        verify(mTransportManager)
+                .disposeOfTransportClient(eq(transportMock.transportClient), any());
+        assertThat(mWakeLock.isHeld()).isFalse();
+        assertThat(mBackupManagerService.isRestoreInProgress()).isFalse();
+        ShadowPerformUnifiedRestoreTask shadowTask =
+                ShadowPerformUnifiedRestoreTask.getLastCreated();
+        assertThat(shadowTask.isFullSystemRestore()).isFalse();
+        assertThat(shadowTask.getFilterSet()).isNull();
+        assertThat(shadowTask.getPackage().packageName).isEqualTo(PACKAGE_1);
+    }
+
+    @Test
+    public void testRestorePackage_whenPackageNullWhenCreated() throws Exception {
+        ShadowBinder.setCallingUid(1);
+        setUpPackage(PACKAGE_1, /* uid */ 1);
+        when(mBackupManagerService.getAvailableRestoreToken(PACKAGE_1)).thenReturn(TOKEN_1);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+
+        int result = restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void testRestorePackage_whenCallerIsNotPackageAndPermissionGranted() throws Exception {
+        final int pid = 1211;
+        final int uid = 1;
+        mShadowApplication.grantPermissions(pid, uid, android.Manifest.permission.BACKUP);
+        ShadowBinder.setCallingPid(pid);
+        ShadowBinder.setCallingUid(uid);
+        setUpPackage(PACKAGE_1, /* uid */ 2);
+        when(mBackupManagerService.getAvailableRestoreToken(PACKAGE_1)).thenReturn(TOKEN_1);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        int result = restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(result).isEqualTo(0);
+    }
+
+    @Test
+    public void testRestorePackage_whenCallerIsNotPackageAndPermissionDenied() throws Exception {
+        final int pid = 1211;
+        final int uid = 1;
+        mShadowApplication.denyPermissions(pid, uid, android.Manifest.permission.BACKUP);
+        ShadowBinder.setCallingPid(pid);
+        ShadowBinder.setCallingUid(uid);
+        setUpPackage(PACKAGE_1, /* uid */ 2);
+        when(mBackupManagerService.getAvailableRestoreToken(PACKAGE_1)).thenReturn(TOKEN_1);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
+
+        expectThrows(
+                SecurityException.class,
+                () -> restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor));
+    }
+
+    @Test
+    public void testRestorePackage_whenPackageNotFound() throws Exception {
+        final int pid = 1211;
+        final int uid = 1;
+        mShadowApplication.grantPermissions(pid, uid, android.Manifest.permission.BACKUP);
+        ShadowBinder.setCallingPid(pid);
+        ShadowBinder.setCallingUid(uid);
+        setUpPackage(PACKAGE_1, /* uid */ 1);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+
+        int result = restoreSession.restorePackage(PACKAGE_2, mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    @Test
+    public void testRestorePackage_whenSessionEnded() throws Exception {
+        ShadowBinder.setCallingUid(1);
+        setUpPackage(PACKAGE_1, /* uid */ 1);
+        setUpTransport(mTransport);
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+        restoreSession.endRestoreSession();
+        mShadowBackupLooper.runToEndOfTasks();
+
+        expectThrows(
+                IllegalStateException.class,
+                () -> restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor));
+    }
+
+    @Test
+    public void testRestorePackage_whenTransportNotRegistered() throws Exception {
+        ShadowBinder.setCallingUid(1);
+        setUpPackage(PACKAGE_1, /* uid */ 1);
+        setUpTransport(mTransport.unregistered());
+        IRestoreSession restoreSession = createActiveRestoreSession(null, mTransport);
+
+        int result = restoreSession.restorePackage(PACKAGE_1, mObserver, mMonitor);
+
+        assertThat(result).isEqualTo(-1);
+    }
+
+    // TODO: Create a builder for PackageInfo/ApplicationInfo and unify usage with
+    //       TransportManagerTest
+    private void setUpPackage(String packageName, int uid) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.uid = uid;
+        mShadowPackageManager.installPackage(packageInfo);
+        ShadowApplicationPackageManager.addInstalledPackage(packageName, packageInfo);
+    }
+
+    private IRestoreSession createActiveRestoreSession(
+            String packageName, TransportData transport) {
+        return new ActiveRestoreSession(
+                mBackupManagerService, packageName, transport.transportName);
+    }
+
+    private IRestoreSession createActiveRestoreSessionWithRestoreSets(
+            String packageName, TransportData transport, RestoreSet... restoreSets)
+            throws RemoteException {
+        ActiveRestoreSession restoreSession =
+                new ActiveRestoreSession(
+                        mBackupManagerService, packageName, transport.transportName);
+        restoreSession.setRestoreSets(restoreSets);
+        return restoreSession;
+    }
+
+    private TransportMock setUpTransport(TransportData transport) throws Exception {
+        return TransportTestUtils.setUpTransport(mTransportManager, transport);
+    }
+}
diff --git a/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
new file mode 100644
index 0000000..376b618
--- /dev/null
+++ b/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
@@ -0,0 +1,86 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+
+import android.util.Slog;
+
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.UserBackupManagerService;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Used for synchronizing doRestoreFinished during adb restore.
+ */
+public class AdbRestoreFinishedLatch implements BackupRestoreTask {
+
+    private static final String TAG = "AdbRestoreFinishedLatch";
+    private UserBackupManagerService backupManagerService;
+    final CountDownLatch mLatch;
+    private final int mCurrentOpToken;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    public AdbRestoreFinishedLatch(UserBackupManagerService backupManagerService,
+            int currentOpToken) {
+        this.backupManagerService = backupManagerService;
+        mLatch = new CountDownLatch(1);
+        mCurrentOpToken = currentOpToken;
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+    }
+
+    void await() {
+        boolean latched = false;
+        long fullBackupAgentTimeoutMillis =
+                mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+        try {
+            latched = mLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Slog.w(TAG, "Interrupted!");
+        }
+    }
+
+    @Override
+    public void execute() {
+        // Unused
+    }
+
+    @Override
+    public void operationComplete(long result) {
+        if (MORE_DEBUG) {
+            Slog.w(TAG, "adb onRestoreFinished() complete");
+        }
+        mLatch.countDown();
+        backupManagerService.removeOperation(mCurrentOpToken);
+    }
+
+    @Override
+    public void handleCancel(boolean cancelAll) {
+        if (DEBUG) {
+            Slog.w(TAG, "adb onRestoreFinished() timed out");
+        }
+        mLatch.countDown();
+        backupManagerService.removeOperation(mCurrentOpToken);
+    }
+}
diff --git a/com/android/server/backup/restore/AdbRestoreFinishedRunnable.java b/com/android/server/backup/restore/AdbRestoreFinishedRunnable.java
new file mode 100644
index 0000000..184a6d0
--- /dev/null
+++ b/com/android/server/backup/restore/AdbRestoreFinishedRunnable.java
@@ -0,0 +1,33 @@
+package com.android.server.backup.restore;
+
+import android.app.IBackupAgent;
+import android.os.RemoteException;
+
+import com.android.server.backup.UserBackupManagerService;
+
+/**
+ * Runner that can be placed on a separate thread to do in-process invocation of the "restore
+ * finished" API asynchronously.  Used by adb restore.
+ */
+public class AdbRestoreFinishedRunnable implements Runnable {
+
+    private final IBackupAgent mAgent;
+    private final int mToken;
+    private final UserBackupManagerService mBackupManagerService;
+
+    AdbRestoreFinishedRunnable(IBackupAgent agent, int token,
+            UserBackupManagerService backupManagerService) {
+        mAgent = agent;
+        mToken = token;
+        mBackupManagerService = backupManagerService;
+    }
+
+    @Override
+    public void run() {
+        try {
+            mAgent.doRestoreFinished(mToken, mBackupManagerService.getBackupManagerBinder());
+        } catch (RemoteException e) {
+            // never happens; this is used only for local binder calls
+        }
+    }
+}
diff --git a/com/android/server/backup/restore/FullRestoreEngine.java b/com/android/server/backup/restore/FullRestoreEngine.java
new file mode 100644
index 0000000..82bed3b
--- /dev/null
+++ b/com/android/server/backup/restore/FullRestoreEngine.java
@@ -0,0 +1,697 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_RESTORE_WAIT;
+import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
+
+import android.app.ApplicationThreadConstants;
+import android.app.IBackupAgent;
+import android.app.backup.FullBackup;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IFullBackupRestoreObserver;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.FileMetadata;
+import com.android.server.backup.KeyValueAdbRestoreEngine;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.fullbackup.FullBackupObbConnection;
+import com.android.server.backup.utils.BytesReadListener;
+import com.android.server.backup.utils.FullBackupRestoreObserverUtils;
+import com.android.server.backup.utils.RestoreUtils;
+import com.android.server.backup.utils.TarBackupReader;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Full restore engine, used by both adb restore and transport-based full restore.
+ */
+public class FullRestoreEngine extends RestoreEngine {
+
+    private final UserBackupManagerService mBackupManagerService;
+    private final int mUserId;
+
+    // Task in charge of monitoring timeouts
+    private final BackupRestoreTask mMonitorTask;
+
+    private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
+
+    // Dedicated observer, if any
+    private IFullBackupRestoreObserver mObserver;
+
+    final IBackupManagerMonitor mMonitor;
+
+    // Where we're delivering the file data as we go
+    private IBackupAgent mAgent;
+
+    // Are we permitted to only deliver a specific package's metadata?
+    final PackageInfo mOnlyPackage;
+
+    final boolean mAllowApks;
+
+    // Which package are we currently handling data for?
+    private String mAgentPackage;
+
+    // Info for working with the target app process
+    private ApplicationInfo mTargetApp;
+
+    // Machinery for restoring OBBs
+    private FullBackupObbConnection mObbConnection = null;
+
+    // possible handling states for a given package in the restore dataset
+    private final HashMap<String, RestorePolicy> mPackagePolicies
+            = new HashMap<>();
+
+    // installer package names for each encountered app, derived from the manifests
+    private final HashMap<String, String> mPackageInstallers = new HashMap<>();
+
+    // Signatures for a given package found in its manifest file
+    private final HashMap<String, Signature[]> mManifestSignatures
+            = new HashMap<>();
+
+    // Packages we've already wiped data on when restoring their first file
+    private final HashSet<String> mClearedPackages = new HashSet<>();
+
+    // Working buffer
+    final byte[] mBuffer;
+
+    // Pipes for moving data
+    private ParcelFileDescriptor[] mPipes = null;
+    private final Object mPipesLock = new Object();
+
+    // Widget blob to be restored out-of-band
+    private byte[] mWidgetData = null;
+    private long mAppVersion;
+
+    final int mEphemeralOpToken;
+
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+    private final boolean mIsAdbRestore;
+    @GuardedBy("mPipesLock")
+    private boolean mPipesClosed;
+
+    public FullRestoreEngine(UserBackupManagerService backupManagerService,
+            BackupRestoreTask monitorTask, IFullBackupRestoreObserver observer,
+            IBackupManagerMonitor monitor, PackageInfo onlyPackage, boolean allowApks,
+            int ephemeralOpToken, boolean isAdbRestore) {
+        mBackupManagerService = backupManagerService;
+        mEphemeralOpToken = ephemeralOpToken;
+        mMonitorTask = monitorTask;
+        mObserver = observer;
+        mMonitor = monitor;
+        mOnlyPackage = onlyPackage;
+        mAllowApks = allowApks;
+        mBuffer = new byte[32 * 1024];
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+        mIsAdbRestore = isAdbRestore;
+        mUserId = backupManagerService.getUserId();
+    }
+
+    public IBackupAgent getAgent() {
+        return mAgent;
+    }
+
+    public byte[] getWidgetData() {
+        return mWidgetData;
+    }
+
+    public boolean restoreOneFile(InputStream instream, boolean mustKillAgent, byte[] buffer,
+            PackageInfo onlyPackage, boolean allowApks, int token, IBackupManagerMonitor monitor) {
+        if (!isRunning()) {
+            Slog.w(TAG, "Restore engine used after halting");
+            return false;
+        }
+
+        BytesReadListener bytesReadListener = bytesRead -> { };
+
+        TarBackupReader tarBackupReader = new TarBackupReader(instream,
+                bytesReadListener, monitor);
+
+        FileMetadata info;
+        try {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "Reading tar header for restoring file");
+            }
+            info = tarBackupReader.readTarHeaders();
+            if (info != null) {
+                if (MORE_DEBUG) {
+                    info.dump();
+                }
+
+                final String pkg = info.packageName;
+                if (!pkg.equals(mAgentPackage)) {
+                    // In the single-package case, it's a semantic error to expect
+                    // one app's data but see a different app's on the wire
+                    if (onlyPackage != null) {
+                        if (!pkg.equals(onlyPackage.packageName)) {
+                            Slog.w(TAG, "Expected data for " + onlyPackage + " but saw " + pkg);
+                            setResult(RestoreEngine.TRANSPORT_FAILURE);
+                            setRunning(false);
+                            return false;
+                        }
+                    }
+
+                    // okay, change in package; set up our various
+                    // bookkeeping if we haven't seen it yet
+                    if (!mPackagePolicies.containsKey(pkg)) {
+                        mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
+                    }
+
+                    // Clean up the previous agent relationship if necessary,
+                    // and let the observer know we're considering a new app.
+                    if (mAgent != null) {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Saw new package; finalizing old one");
+                        }
+                        // Now we're really done
+                        tearDownPipes();
+                        tearDownAgent(mTargetApp, mIsAdbRestore);
+                        mTargetApp = null;
+                        mAgentPackage = null;
+                    }
+                }
+
+                if (info.path.equals(BACKUP_MANIFEST_FILENAME)) {
+                    Signature[] signatures = tarBackupReader.readAppManifestAndReturnSignatures(
+                            info);
+                    // readAppManifestAndReturnSignatures() will have extracted the version from
+                    // the manifest, so we save it to use in adb key-value restore later.
+                    mAppVersion = info.version;
+                    PackageManagerInternal pmi = LocalServices.getService(
+                            PackageManagerInternal.class);
+                    RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy(
+                            mBackupManagerService.getPackageManager(), allowApks, info, signatures,
+                            pmi, mUserId);
+                    mManifestSignatures.put(info.packageName, signatures);
+                    mPackagePolicies.put(pkg, restorePolicy);
+                    mPackageInstallers.put(pkg, info.installerPackageName);
+                    // We've read only the manifest content itself at this point,
+                    // so consume the footer before looping around to the next
+                    // input file
+                    tarBackupReader.skipTarPadding(info.size);
+                    mObserver = FullBackupRestoreObserverUtils.sendOnRestorePackage(mObserver, pkg);
+                } else if (info.path.equals(BACKUP_METADATA_FILENAME)) {
+                    // Metadata blobs!
+                    tarBackupReader.readMetadata(info);
+
+                    // The following only exist because we want to keep refactoring as safe as
+                    // possible, without changing too much.
+                    // TODO: Refactor, so that there are no funny things like this.
+                    // This is read during TarBackupReader.readMetadata().
+                    mWidgetData = tarBackupReader.getWidgetData();
+                    // This can be nulled during TarBackupReader.readMetadata().
+                    monitor = tarBackupReader.getMonitor();
+
+                    tarBackupReader.skipTarPadding(info.size);
+                } else {
+                    // Non-manifest, so it's actual file data.  Is this a package
+                    // we're ignoring?
+                    boolean okay = true;
+                    RestorePolicy policy = mPackagePolicies.get(pkg);
+                    switch (policy) {
+                        case IGNORE:
+                            okay = false;
+                            break;
+
+                        case ACCEPT_IF_APK:
+                            // If we're in accept-if-apk state, then the first file we
+                            // see MUST be the apk.
+                            if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
+                                if (DEBUG) {
+                                    Slog.d(TAG, "APK file; installing");
+                                }
+                                // Try to install the app.
+                                String installerPackageName = mPackageInstallers.get(pkg);
+                                boolean isSuccessfullyInstalled = RestoreUtils.installApk(
+                                        instream, mBackupManagerService.getContext(),
+                                        mDeleteObserver, mManifestSignatures,
+                                        mPackagePolicies, info, installerPackageName,
+                                        bytesReadListener, mUserId);
+                                // good to go; promote to ACCEPT
+                                mPackagePolicies.put(pkg, isSuccessfullyInstalled
+                                        ? RestorePolicy.ACCEPT
+                                        : RestorePolicy.IGNORE);
+                                // At this point we've consumed this file entry
+                                // ourselves, so just strip the tar footer and
+                                // go on to the next file in the input stream
+                                tarBackupReader.skipTarPadding(info.size);
+                                return true;
+                            } else {
+                                // File data before (or without) the apk.  We can't
+                                // handle it coherently in this case so ignore it.
+                                mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
+                                okay = false;
+                            }
+                            break;
+
+                        case ACCEPT:
+                            if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
+                                if (DEBUG) {
+                                    Slog.d(TAG, "apk present but ACCEPT");
+                                }
+                                // we can take the data without the apk, so we
+                                // *want* to do so.  skip the apk by declaring this
+                                // one file not-okay without changing the restore
+                                // policy for the package.
+                                okay = false;
+                            }
+                            break;
+
+                        default:
+                            // Something has gone dreadfully wrong when determining
+                            // the restore policy from the manifest.  Ignore the
+                            // rest of this package's data.
+                            Slog.e(TAG, "Invalid policy from manifest");
+                            okay = false;
+                            mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
+                            break;
+                    }
+
+                    // Is it a *file* we need to drop or is it not a canonical path?
+                    if (!isRestorableFile(info) || !isCanonicalFilePath(info.path)) {
+                        okay = false;
+                    }
+
+                    // If the policy is satisfied, go ahead and set up to pipe the
+                    // data to the agent.
+                    if (MORE_DEBUG && okay && mAgent != null) {
+                        Slog.i(TAG, "Reusing existing agent instance");
+                    }
+                    if (okay && mAgent == null) {
+                        if (MORE_DEBUG) {
+                            Slog.d(TAG, "Need to launch agent for " + pkg);
+                        }
+
+                        try {
+                            mTargetApp =
+                                    mBackupManagerService.getPackageManager()
+                                            .getApplicationInfoAsUser(pkg, 0, mUserId);
+
+                            // If we haven't sent any data to this app yet, we probably
+                            // need to clear it first. Check that.
+                            if (!mClearedPackages.contains(pkg)) {
+                                // Apps with their own backup agents are responsible for coherently
+                                // managing a full restore.
+                                // In some rare cases they can't, especially in case of deferred
+                                // restore. In this case check whether this app should be forced to
+                                // clear up.
+                                // TODO: Fix this properly with manifest parameter.
+                                boolean forceClear = shouldForceClearAppDataOnFullRestore(
+                                        mTargetApp.packageName);
+                                if (mTargetApp.backupAgentName == null || forceClear) {
+                                    if (DEBUG) {
+                                        Slog.d(TAG,
+                                                "Clearing app data preparatory to full restore");
+                                    }
+                                    mBackupManagerService.clearApplicationDataBeforeRestore(pkg);
+                                } else {
+                                    if (MORE_DEBUG) {
+                                        Slog.d(TAG, "backup agent ("
+                                                + mTargetApp.backupAgentName + ") => no clear");
+                                    }
+                                }
+                                mClearedPackages.add(pkg);
+                            } else {
+                                if (MORE_DEBUG) {
+                                    Slog.d(TAG, "We've initialized this app already; no clear "
+                                            + "required");
+                                }
+                            }
+
+                            // All set; now set up the IPC and launch the agent
+                            setUpPipes();
+                            mAgent = mBackupManagerService.bindToAgentSynchronous(mTargetApp,
+                                    FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)
+                                            ? ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL
+                                            : ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL);
+                            mAgentPackage = pkg;
+                        } catch (IOException | NameNotFoundException e) {
+                            // fall through to error handling
+                        }
+
+                        if (mAgent == null) {
+                            Slog.e(TAG, "Unable to create agent for " + pkg);
+                            okay = false;
+                            tearDownPipes();
+                            mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
+                        }
+                    }
+
+                    // Sanity check: make sure we never give data to the wrong app.  This
+                    // should never happen but a little paranoia here won't go amiss.
+                    if (okay && !pkg.equals(mAgentPackage)) {
+                        Slog.e(TAG, "Restoring data for " + pkg
+                                + " but agent is for " + mAgentPackage);
+                        okay = false;
+                    }
+
+                    // At this point we have an agent ready to handle the full
+                    // restore data as well as a pipe for sending data to
+                    // that agent.  Tell the agent to start reading from the
+                    // pipe.
+                    if (okay) {
+                        boolean agentSuccess = true;
+                        long toCopy = info.size;
+                        final boolean isSharedStorage = pkg.equals(SHARED_BACKUP_AGENT_PACKAGE);
+                        final long timeout = isSharedStorage ?
+                                mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis() :
+                                mAgentTimeoutParameters.getRestoreAgentTimeoutMillis();
+                        try {
+                            mBackupManagerService.prepareOperationTimeout(token,
+                                    timeout,
+                                    mMonitorTask,
+                                    OP_TYPE_RESTORE_WAIT);
+
+                            if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) {
+                                if (DEBUG) {
+                                    Slog.d(TAG, "Restoring OBB file for " + pkg
+                                            + " : " + info.path);
+                                }
+                                mObbConnection.restoreObbFile(pkg, mPipes[0],
+                                        info.size, info.type, info.path, info.mode,
+                                        info.mtime, token,
+                                        mBackupManagerService.getBackupManagerBinder());
+                            } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) {
+                                // This is only possible during adb restore.
+                                // TODO: Refactor to clearly separate the flows.
+                                if (DEBUG) {
+                                    Slog.d(TAG, "Restoring key-value file for " + pkg
+                                            + " : " + info.path);
+                                }
+                                // Set the version saved from manifest entry.
+                                info.version = mAppVersion;
+                                KeyValueAdbRestoreEngine restoreEngine =
+                                        new KeyValueAdbRestoreEngine(
+                                                mBackupManagerService,
+                                                mBackupManagerService.getDataDir(), info, mPipes[0],
+                                                mAgent, token);
+                                new Thread(restoreEngine, "restore-key-value-runner").start();
+                            } else {
+                                if (MORE_DEBUG) {
+                                    Slog.d(TAG, "Invoking agent to restore file " + info.path);
+                                }
+                                // fire up the app's agent listening on the socket.  If
+                                // the agent is running in the system process we can't
+                                // just invoke it asynchronously, so we provide a thread
+                                // for it here.
+                                if (mTargetApp.processName.equals("system")) {
+                                    Slog.d(TAG, "system process agent - spinning a thread");
+                                    RestoreFileRunnable runner = new RestoreFileRunnable(
+                                            mBackupManagerService, mAgent, info, mPipes[0], token);
+                                    new Thread(runner, "restore-sys-runner").start();
+                                } else {
+                                    mAgent.doRestoreFile(mPipes[0], info.size, info.type,
+                                            info.domain, info.path, info.mode, info.mtime,
+                                            token, mBackupManagerService.getBackupManagerBinder());
+                                }
+                            }
+                        } catch (IOException e) {
+                            // couldn't dup the socket for a process-local restore
+                            Slog.d(TAG, "Couldn't establish restore");
+                            agentSuccess = false;
+                            okay = false;
+                        } catch (RemoteException e) {
+                            // whoops, remote entity went away.  We'll eat the content
+                            // ourselves, then, and not copy it over.
+                            Slog.e(TAG, "Agent crashed during full restore");
+                            agentSuccess = false;
+                            okay = false;
+                        }
+
+                        // Copy over the data if the agent is still good
+                        if (okay) {
+                            if (MORE_DEBUG) {
+                                Slog.v(TAG, "  copying to restore agent: " + toCopy + " bytes");
+                            }
+                            boolean pipeOkay = true;
+                            FileOutputStream pipe = new FileOutputStream(
+                                    mPipes[1].getFileDescriptor());
+                            while (toCopy > 0) {
+                                int toRead = (toCopy > buffer.length)
+                                        ? buffer.length : (int) toCopy;
+                                int nRead = instream.read(buffer, 0, toRead);
+                                if (nRead <= 0) {
+                                    break;
+                                }
+                                toCopy -= nRead;
+
+                                // send it to the output pipe as long as things
+                                // are still good
+                                if (pipeOkay) {
+                                    try {
+                                        pipe.write(buffer, 0, nRead);
+                                    } catch (IOException e) {
+                                        Slog.e(TAG, "Failed to write to restore pipe: "
+                                                + e.getMessage());
+                                        pipeOkay = false;
+                                    }
+                                }
+                            }
+
+                            // done sending that file!  Now we just need to consume
+                            // the delta from info.size to the end of block.
+                            tarBackupReader.skipTarPadding(info.size);
+
+                            // and now that we've sent it all, wait for the remote
+                            // side to acknowledge receipt
+                            agentSuccess = mBackupManagerService.waitUntilOperationComplete(token);
+                        }
+
+                        // okay, if the remote end failed at any point, deal with
+                        // it by ignoring the rest of the restore on it
+                        if (!agentSuccess) {
+                            Slog.w(TAG, "Agent failure restoring " + pkg + "; ending restore");
+                            mBackupManagerService.getBackupHandler().removeMessages(
+                                    MSG_RESTORE_OPERATION_TIMEOUT);
+                            tearDownPipes();
+                            tearDownAgent(mTargetApp, false);
+                            mAgent = null;
+                            mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
+
+                            // If this was a single-package restore, we halt immediately
+                            // with an agent error under these circumstances
+                            if (onlyPackage != null) {
+                                setResult(RestoreEngine.TARGET_FAILURE);
+                                setRunning(false);
+                                return false;
+                            }
+                        }
+                    }
+
+                    // Problems setting up the agent communication, an explicitly
+                    // dropped file, or an already-ignored package: skip to the
+                    // next stream entry by reading and discarding this file.
+                    if (!okay) {
+                        if (MORE_DEBUG) {
+                            Slog.d(TAG, "[discarding file content]");
+                        }
+                        long bytesToConsume = (info.size + 511) & ~511;
+                        while (bytesToConsume > 0) {
+                            int toRead = (bytesToConsume > buffer.length)
+                                    ? buffer.length : (int) bytesToConsume;
+                            long nRead = instream.read(buffer, 0, toRead);
+                            if (nRead <= 0) {
+                                break;
+                            }
+                            bytesToConsume -= nRead;
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            if (DEBUG) {
+                Slog.w(TAG, "io exception on restore socket read: " + e.getMessage());
+            }
+            setResult(RestoreEngine.TRANSPORT_FAILURE);
+            info = null;
+        }
+
+        // If we got here we're either running smoothly or we've finished
+        if (info == null) {
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "No [more] data for this package; tearing down");
+            }
+            tearDownPipes();
+            setRunning(false);
+            if (mustKillAgent) {
+                tearDownAgent(mTargetApp, mIsAdbRestore);
+            }
+        }
+        return (info != null);
+    }
+
+    private void setUpPipes() throws IOException {
+        synchronized (mPipesLock) {
+            mPipes = ParcelFileDescriptor.createPipe();
+            mPipesClosed = false;
+        }
+    }
+
+    private void tearDownPipes() {
+        // Teardown might arise from the inline restore processing or from the asynchronous
+        // timeout mechanism, and these might race.  Make sure we don't try to close and
+        // null out the pipes twice.
+        synchronized (mPipesLock) {
+            if (!mPipesClosed && mPipes != null) {
+                try {
+                    mPipes[0].close();
+                    mPipes[1].close();
+
+                    mPipesClosed = true;
+                } catch (IOException e) {
+                    Slog.w(TAG, "Couldn't close agent pipes", e);
+                }
+            }
+        }
+    }
+
+    private void tearDownAgent(ApplicationInfo app, boolean doRestoreFinished) {
+        if (mAgent != null) {
+            try {
+                // In the adb restore case, we do restore-finished here
+                if (doRestoreFinished) {
+                    final int token = mBackupManagerService.generateRandomIntegerToken();
+                    long fullBackupAgentTimeoutMillis =
+                            mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+                    final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch(
+                            mBackupManagerService, token);
+                    mBackupManagerService.prepareOperationTimeout(
+                            token, fullBackupAgentTimeoutMillis, latch, OP_TYPE_RESTORE_WAIT);
+                    if (mTargetApp.processName.equals("system")) {
+                        if (MORE_DEBUG) {
+                            Slog.d(TAG, "system agent - restoreFinished on thread");
+                        }
+                        Runnable runner = new AdbRestoreFinishedRunnable(mAgent, token,
+                                mBackupManagerService);
+                        new Thread(runner, "restore-sys-finished-runner").start();
+                    } else {
+                        mAgent.doRestoreFinished(token,
+                                mBackupManagerService.getBackupManagerBinder());
+                    }
+
+                    latch.await();
+                }
+
+                mBackupManagerService.tearDownAgentAndKill(app);
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Lost app trying to shut down");
+            }
+            mAgent = null;
+        }
+    }
+
+    void handleTimeout() {
+        tearDownPipes();
+        setResult(RestoreEngine.TARGET_FAILURE);
+        setRunning(false);
+    }
+
+    private static boolean isRestorableFile(FileMetadata info) {
+        if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) {
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Dropping cache file path " + info.path);
+            }
+            return false;
+        }
+
+        if (FullBackup.ROOT_TREE_TOKEN.equals(info.domain)) {
+            // It's possible this is "no-backup" dir contents in an archive stream
+            // produced on a device running a version of the OS that predates that
+            // API.  Respect the no-backup intention and don't let the data get to
+            // the app.
+            if (info.path.startsWith("no_backup/")) {
+                if (MORE_DEBUG) {
+                    Slog.i(TAG, "Dropping no_backup file path " + info.path);
+                }
+                return false;
+            }
+        }
+
+        // Otherwise we think this file is good to go
+        return true;
+    }
+
+    private static boolean isCanonicalFilePath(String path) {
+        if (path.contains("..") || path.contains("//")) {
+            if (MORE_DEBUG) {
+                Slog.w(TAG, "Dropping invalid path " + path);
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns whether the package is in the list of the packages for which clear app data should
+     * be called despite the fact that they have backup agent.
+     *
+     * <p>The list is read from {@link Settings.Secure#PACKAGES_TO_CLEAR_DATA_BEFORE_FULL_RESTORE}.
+     */
+    private boolean shouldForceClearAppDataOnFullRestore(String packageName) {
+        String packageListString = Settings.Secure.getStringForUser(
+                mBackupManagerService.getContext().getContentResolver(),
+                Settings.Secure.PACKAGES_TO_CLEAR_DATA_BEFORE_FULL_RESTORE,
+                mUserId);
+        if (TextUtils.isEmpty(packageListString)) {
+            return false;
+        }
+
+        List<String> packages = Arrays.asList(packageListString.split(";"));
+        return packages.contains(packageName);
+    }
+
+    void sendOnRestorePackage(String name) {
+        if (mObserver != null) {
+            try {
+                // TODO: use a more user-friendly name string
+                mObserver.onRestorePackage(name);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full restore observer went away: restorePackage");
+                mObserver = null;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/restore/FullRestoreEngineThread.java b/com/android/server/backup/restore/FullRestoreEngineThread.java
new file mode 100644
index 0000000..7075608
--- /dev/null
+++ b/com/android/server/backup/restore/FullRestoreEngineThread.java
@@ -0,0 +1,63 @@
+package com.android.server.backup.restore;
+
+import android.os.ParcelFileDescriptor;
+
+import libcore.io.IoUtils;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+class FullRestoreEngineThread implements Runnable {
+
+    FullRestoreEngine mEngine;
+    InputStream mEngineStream;
+    private final boolean mMustKillAgent;
+
+    FullRestoreEngineThread(FullRestoreEngine engine, ParcelFileDescriptor engineSocket) {
+        mEngine = engine;
+        engine.setRunning(true);
+        // We *do* want this FileInputStream to own the underlying fd, so that
+        // when we are finished with it, it closes this end of the pipe in a way
+        // that signals its other end.
+        mEngineStream = new FileInputStream(engineSocket.getFileDescriptor(), true);
+        // Tell it to be sure to leave the agent instance up after finishing
+        mMustKillAgent = false;
+    }
+
+    //for adb restore
+    FullRestoreEngineThread(FullRestoreEngine engine, InputStream inputStream) {
+        mEngine = engine;
+        engine.setRunning(true);
+        mEngineStream = inputStream;
+        // philippov: in adb agent is killed after restore.
+        mMustKillAgent = true;
+    }
+
+    public boolean isRunning() {
+        return mEngine.isRunning();
+    }
+
+    public int waitForResult() {
+        return mEngine.waitForResult();
+    }
+
+    @Override
+    public void run() {
+        try {
+            while (mEngine.isRunning()) {
+                mEngine.restoreOneFile(mEngineStream, mMustKillAgent, mEngine.mBuffer,
+                        mEngine.mOnlyPackage, mEngine.mAllowApks, mEngine.mEphemeralOpToken,
+                        mEngine.mMonitor);
+            }
+        } finally {
+            // Because mEngineStream adopted its underlying FD, this also
+            // closes this end of the pipe.
+            IoUtils.closeQuietly(mEngineStream);
+        }
+    }
+
+    public void handleTimeout() {
+        IoUtils.closeQuietly(mEngineStream);
+        mEngine.handleTimeout();
+    }
+}
diff --git a/com/android/server/backup/restore/PerformAdbRestoreTask.java b/com/android/server/backup/restore/PerformAdbRestoreTask.java
new file mode 100644
index 0000000..01b40fb
--- /dev/null
+++ b/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -0,0 +1,333 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
+import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_VERSION;
+
+import android.app.backup.IFullBackupRestoreObserver;
+import android.os.ParcelFileDescriptor;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.fullbackup.FullBackupObbConnection;
+import com.android.server.backup.utils.FullBackupRestoreObserverUtils;
+import com.android.server.backup.utils.PasswordUtils;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.InflaterInputStream;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PerformAdbRestoreTask implements Runnable {
+
+    private final UserBackupManagerService mBackupManagerService;
+    private final ParcelFileDescriptor mInputFile;
+    private final String mCurrentPassword;
+    private final String mDecryptPassword;
+    private final AtomicBoolean mLatchObject;
+    private final FullBackupObbConnection mObbConnection;
+
+    private IFullBackupRestoreObserver mObserver;
+
+    public PerformAdbRestoreTask(UserBackupManagerService backupManagerService,
+            ParcelFileDescriptor fd, String curPassword, String decryptPassword,
+            IFullBackupRestoreObserver observer, AtomicBoolean latch) {
+        this.mBackupManagerService = backupManagerService;
+        mInputFile = fd;
+        mCurrentPassword = curPassword;
+        mDecryptPassword = decryptPassword;
+        mObserver = observer;
+        mLatchObject = latch;
+        mObbConnection = new FullBackupObbConnection(backupManagerService);
+    }
+
+    @Override
+    public void run() {
+        Slog.i(TAG, "--- Performing full-dataset restore ---");
+        mObbConnection.establish();
+        mObserver = FullBackupRestoreObserverUtils.sendStartRestore(mObserver);
+
+        FileInputStream rawInStream = null;
+        try {
+            if (!mBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Backup password mismatch; aborting");
+                }
+                return;
+            }
+
+            rawInStream = new FileInputStream(mInputFile.getFileDescriptor());
+
+            InputStream tarInputStream = parseBackupFileHeaderAndReturnTarStream(rawInStream,
+                    mDecryptPassword);
+            if (tarInputStream == null) {
+                // There was an error reading the backup file, which is already handled and logged.
+                // Just abort.
+                return;
+            }
+
+            FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService, null,
+                    mObserver, null, null, true, 0 /*unused*/, true);
+            FullRestoreEngineThread mEngineThread = new FullRestoreEngineThread(mEngine,
+                    tarInputStream);
+            mEngineThread.run();
+
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "Done consuming input tarfile.");
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Unable to read restore input");
+        } finally {
+            try {
+                if (rawInStream != null) {
+                    rawInStream.close();
+                }
+                mInputFile.close();
+            } catch (IOException e) {
+                Slog.w(TAG, "Close of restore data pipe threw", e);
+                /* nothing we can do about this */
+            }
+            synchronized (mLatchObject) {
+                mLatchObject.set(true);
+                mLatchObject.notifyAll();
+            }
+            mObbConnection.tearDown();
+            mObserver = FullBackupRestoreObserverUtils.sendEndRestore(mObserver);
+            Slog.d(TAG, "Full restore pass complete.");
+            mBackupManagerService.getWakelock().release();
+        }
+    }
+
+    private static void readFullyOrThrow(InputStream in, byte[] buffer) throws IOException {
+        int offset = 0;
+        while (offset < buffer.length) {
+            int bytesRead = in.read(buffer, offset, buffer.length - offset);
+            if (bytesRead <= 0) {
+                throw new IOException("Couldn't fully read data");
+            }
+            offset += bytesRead;
+        }
+    }
+
+    @VisibleForTesting
+    public static InputStream parseBackupFileHeaderAndReturnTarStream(
+            InputStream rawInputStream,
+            String decryptPassword)
+            throws IOException {
+        // First, parse out the unencrypted/uncompressed header
+        boolean compressed = false;
+        InputStream preCompressStream = rawInputStream;
+
+        boolean okay = false;
+        final int headerLen = BACKUP_FILE_HEADER_MAGIC.length();
+        byte[] streamHeader = new byte[headerLen];
+        readFullyOrThrow(rawInputStream, streamHeader);
+        byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes(
+                "UTF-8");
+        if (Arrays.equals(magicBytes, streamHeader)) {
+            // okay, header looks good.  now parse out the rest of the fields.
+            String s = readHeaderLine(rawInputStream);
+            final int archiveVersion = Integer.parseInt(s);
+            if (archiveVersion <= BACKUP_FILE_VERSION) {
+                // okay, it's a version we recognize.  if it's version 1, we may need
+                // to try two different PBKDF2 regimes to compare checksums.
+                final boolean pbkdf2Fallback = (archiveVersion == 1);
+
+                s = readHeaderLine(rawInputStream);
+                compressed = (Integer.parseInt(s) != 0);
+                s = readHeaderLine(rawInputStream);
+                if (s.equals("none")) {
+                    // no more header to parse; we're good to go
+                    okay = true;
+                } else if (decryptPassword != null && decryptPassword.length() > 0) {
+                    preCompressStream = decodeAesHeaderAndInitialize(
+                            decryptPassword, s, pbkdf2Fallback,
+                            rawInputStream);
+                    if (preCompressStream != null) {
+                        okay = true;
+                    }
+                } else {
+                    Slog.w(TAG, "Archive is encrypted but no password given");
+                }
+            } else {
+                Slog.w(TAG, "Wrong header version: " + s);
+            }
+        } else {
+            Slog.w(TAG, "Didn't read the right header magic");
+        }
+
+        if (!okay) {
+            Slog.w(TAG, "Invalid restore data; aborting.");
+            return null;
+        }
+
+        // okay, use the right stream layer based on compression
+        return compressed ? new InflaterInputStream(preCompressStream) : preCompressStream;
+    }
+
+    private static String readHeaderLine(InputStream in) throws IOException {
+        int c;
+        StringBuilder buffer = new StringBuilder(80);
+        while ((c = in.read()) >= 0) {
+            if (c == '\n') {
+                break;   // consume and discard the newlines
+            }
+            buffer.append((char) c);
+        }
+        return buffer.toString();
+    }
+
+    private static InputStream attemptMasterKeyDecryption(String decryptPassword, String algorithm,
+            byte[] userSalt, byte[] ckSalt,
+            int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream,
+            boolean doLog) {
+        InputStream result = null;
+
+        try {
+            Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
+            SecretKey userKey = PasswordUtils
+                    .buildPasswordKey(algorithm, decryptPassword, userSalt,
+                            rounds);
+            byte[] IV = PasswordUtils.hexToByteArray(userIvHex);
+            IvParameterSpec ivSpec = new IvParameterSpec(IV);
+            c.init(Cipher.DECRYPT_MODE,
+                    new SecretKeySpec(userKey.getEncoded(), "AES"),
+                    ivSpec);
+            byte[] mkCipher = PasswordUtils.hexToByteArray(masterKeyBlobHex);
+            byte[] mkBlob = c.doFinal(mkCipher);
+
+            // first, the master key IV
+            int offset = 0;
+            int len = mkBlob[offset++];
+            IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
+            offset += len;
+            // then the master key itself
+            len = mkBlob[offset++];
+            byte[] mk = Arrays.copyOfRange(mkBlob,
+                    offset, offset + len);
+            offset += len;
+            // and finally the master key checksum hash
+            len = mkBlob[offset++];
+            byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
+                    offset, offset + len);
+
+            // now validate the decrypted master key against the checksum
+            byte[] calculatedCk = PasswordUtils.makeKeyChecksum(algorithm, mk, ckSalt,
+                    rounds);
+            if (Arrays.equals(calculatedCk, mkChecksum)) {
+                ivSpec = new IvParameterSpec(IV);
+                c.init(Cipher.DECRYPT_MODE,
+                        new SecretKeySpec(mk, "AES"),
+                        ivSpec);
+                // Only if all of the above worked properly will 'result' be assigned
+                result = new CipherInputStream(rawInStream, c);
+            } else if (doLog) {
+                Slog.w(TAG, "Incorrect password");
+            }
+        } catch (InvalidAlgorithmParameterException e) {
+            if (doLog) {
+                Slog.e(TAG, "Needed parameter spec unavailable!", e);
+            }
+        } catch (BadPaddingException e) {
+            // This case frequently occurs when the wrong password is used to decrypt
+            // the master key.  Use the identical "incorrect password" log text as is
+            // used in the checksum failure log in order to avoid providing additional
+            // information to an attacker.
+            if (doLog) {
+                Slog.w(TAG, "Incorrect password");
+            }
+        } catch (IllegalBlockSizeException e) {
+            if (doLog) {
+                Slog.w(TAG, "Invalid block size in master key");
+            }
+        } catch (NoSuchAlgorithmException e) {
+            if (doLog) {
+                Slog.e(TAG, "Needed decryption algorithm unavailable!");
+            }
+        } catch (NoSuchPaddingException e) {
+            if (doLog) {
+                Slog.e(TAG, "Needed padding mechanism unavailable!");
+            }
+        } catch (InvalidKeyException e) {
+            if (doLog) {
+                Slog.w(TAG, "Illegal password; aborting");
+            }
+        }
+
+        return result;
+    }
+
+    private static InputStream decodeAesHeaderAndInitialize(String decryptPassword,
+            String encryptionName,
+            boolean pbkdf2Fallback,
+            InputStream rawInStream) {
+        InputStream result = null;
+        try {
+            if (encryptionName.equals(PasswordUtils.ENCRYPTION_ALGORITHM_NAME)) {
+
+                String userSaltHex = readHeaderLine(rawInStream); // 5
+                byte[] userSalt = PasswordUtils.hexToByteArray(userSaltHex);
+
+                String ckSaltHex = readHeaderLine(rawInStream); // 6
+                byte[] ckSalt = PasswordUtils.hexToByteArray(ckSaltHex);
+
+                int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7
+                String userIvHex = readHeaderLine(rawInStream); // 8
+
+                String masterKeyBlobHex = readHeaderLine(rawInStream); // 9
+
+                // decrypt the master key blob
+                result = attemptMasterKeyDecryption(decryptPassword, PBKDF_CURRENT,
+                        userSalt, ckSalt, rounds, userIvHex, masterKeyBlobHex, rawInStream, false);
+                if (result == null && pbkdf2Fallback) {
+                    result = attemptMasterKeyDecryption(
+                            decryptPassword, PBKDF_FALLBACK, userSalt, ckSalt,
+                            rounds, userIvHex, masterKeyBlobHex, rawInStream, true);
+                }
+            } else {
+                Slog.w(TAG, "Unsupported encryption method: " + encryptionName);
+            }
+        } catch (NumberFormatException e) {
+            Slog.w(TAG, "Can't parse restore data header");
+        } catch (IOException e) {
+            Slog.w(TAG, "Can't read input header");
+        }
+
+        return result;
+    }
+}
diff --git a/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
new file mode 100644
index 0000000..12113fe
--- /dev/null
+++ b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -0,0 +1,1372 @@
+/*
+ * 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.restore;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
+import static com.android.server.backup.UserBackupManagerService.OP_TYPE_RESTORE_WAIT;
+import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
+import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE;
+import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_RESTORE_STEP;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
+import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.annotation.Nullable;
+import android.app.ApplicationThreadConstants;
+import android.app.IBackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
+import android.app.backup.IRestoreObserver;
+import android.app.backup.RestoreDescription;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.EventLog;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.AppWidgetBackupBridge;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.BackupUtils;
+import com.android.server.backup.PackageManagerBackupAgent;
+import com.android.server.backup.PackageManagerBackupAgent.Metadata;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.utils.AppBackupUtils;
+import com.android.server.backup.utils.BackupManagerMonitorUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class PerformUnifiedRestoreTask implements BackupRestoreTask {
+
+    private UserBackupManagerService backupManagerService;
+    private final int mUserId;
+    private final TransportManager mTransportManager;
+    // Transport client we're working with to do the restore
+    private final TransportClient mTransportClient;
+
+    // Where per-transport saved state goes
+    private File mStateDir;
+
+    // Restore observer; may be null
+    private IRestoreObserver mObserver;
+
+    // BackuoManagerMonitor; may be null
+    private IBackupManagerMonitor mMonitor;
+
+    // Token identifying the dataset to the transport
+    private long mToken;
+
+    // When this is a restore-during-install, this is the token identifying the
+    // operation to the Package Manager, and we must ensure that we let it know
+    // when we're finished.
+    private int mPmToken;
+
+    // When this is restore-during-install, we need to tell the package manager
+    // whether we actually launched the app, because this affects notifications
+    // around externally-visible state transitions.
+    private boolean mDidLaunch;
+
+    // Is this a whole-system restore, i.e. are we establishing a new ancestral
+    // dataset to base future restore-at-install operations from?
+    private boolean mIsSystemRestore;
+
+    // If this is a single-package restore, what package are we interested in?
+    private PackageInfo mTargetPackage;
+
+    // In all cases, the calculated list of packages that we are trying to restore
+    private List<PackageInfo> mAcceptSet;
+
+    // Our bookkeeping about the ancestral dataset
+    private PackageManagerBackupAgent mPmAgent;
+
+    // Currently-bound backup agent for restore + restoreFinished purposes
+    private IBackupAgent mAgent;
+
+    // What sort of restore we're doing now
+    private RestoreDescription mRestoreDescription;
+
+    // The package we're currently restoring
+    private PackageInfo mCurrentPackage;
+
+    // Widget-related data handled as part of this restore operation
+    private byte[] mWidgetData;
+
+    // Number of apps restored in this pass
+    private int mCount;
+
+    // When did we start?
+    private long mStartRealtime;
+
+    // State machine progress
+    private UnifiedRestoreState mState;
+
+    // How are things going?
+    private int mStatus;
+
+    // Done?
+    private boolean mFinished;
+
+    // When finished call listener
+    private final OnTaskFinishedListener mListener;
+
+    // Key/value: bookkeeping about staged data and files for agent access
+    private File mBackupDataName;
+    private File mStageName;
+    private File mNewStateName;
+    private ParcelFileDescriptor mBackupData;
+    private ParcelFileDescriptor mNewState;
+
+    private final int mEphemeralOpToken;
+    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
+
+    @VisibleForTesting
+    PerformUnifiedRestoreTask(UserBackupManagerService backupManagerService) {
+        mListener = null;
+        mAgentTimeoutParameters = null;
+        mTransportClient = null;
+        mTransportManager = null;
+        mEphemeralOpToken = 0;
+        mUserId = 0;
+        this.backupManagerService = backupManagerService;
+    }
+
+    // This task can assume that the wakelock is properly held for it and doesn't have to worry
+    // about releasing it.
+    public PerformUnifiedRestoreTask(
+            UserBackupManagerService backupManagerService,
+            TransportClient transportClient,
+            IRestoreObserver observer,
+            IBackupManagerMonitor monitor,
+            long restoreSetToken,
+            @Nullable PackageInfo targetPackage,
+            int pmToken,
+            boolean isFullSystemRestore,
+            @Nullable String[] filterSet,
+            OnTaskFinishedListener listener) {
+        this.backupManagerService = backupManagerService;
+        mUserId = backupManagerService.getUserId();
+        mTransportManager = backupManagerService.getTransportManager();
+        mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
+        mState = UnifiedRestoreState.INITIAL;
+        mStartRealtime = SystemClock.elapsedRealtime();
+
+        mTransportClient = transportClient;
+        mObserver = observer;
+        mMonitor = monitor;
+        mToken = restoreSetToken;
+        mPmToken = pmToken;
+        mTargetPackage = targetPackage;
+        mIsSystemRestore = isFullSystemRestore;
+        mFinished = false;
+        mDidLaunch = false;
+        mListener = listener;
+        mAgentTimeoutParameters = Objects.requireNonNull(
+                backupManagerService.getAgentTimeoutParameters(),
+                "Timeout parameters cannot be null");
+
+        if (targetPackage != null) {
+            // Single package restore
+            mAcceptSet = new ArrayList<>();
+            mAcceptSet.add(targetPackage);
+        } else {
+            // Everything possible, or a target set
+            if (filterSet == null) {
+                // We want everything and a pony
+                List<PackageInfo> apps =
+                        PackageManagerBackupAgent.getStorableApplications(
+                                backupManagerService.getPackageManager(), mUserId);
+                filterSet = packagesToNames(apps);
+                if (DEBUG) {
+                    Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps");
+                }
+            }
+
+            mAcceptSet = new ArrayList<>(filterSet.length);
+
+            // Pro tem, we insist on moving the settings provider package to last place.
+            // Keep track of whether it's in the list, and bump it down if so.  We also
+            // want to do the system package itself first if it's called for.
+            boolean hasSystem = false;
+            boolean hasSettings = false;
+            for (int i = 0; i < filterSet.length; i++) {
+                try {
+                    PackageManager pm = backupManagerService.getPackageManager();
+                    PackageInfo info = pm.getPackageInfoAsUser(filterSet[i], 0, mUserId);
+                    if (PLATFORM_PACKAGE_NAME.equals(info.packageName)) {
+                        hasSystem = true;
+                        continue;
+                    }
+                    if (SETTINGS_PACKAGE.equals(info.packageName)) {
+                        hasSettings = true;
+                        continue;
+                    }
+
+                    if (AppBackupUtils.appIsEligibleForBackup(info.applicationInfo, mUserId)) {
+                        mAcceptSet.add(info);
+                    }
+                } catch (NameNotFoundException e) {
+                    // requested package name doesn't exist; ignore it
+                }
+            }
+            if (hasSystem) {
+                try {
+                    mAcceptSet.add(0, backupManagerService.getPackageManager().getPackageInfoAsUser(
+                                    PLATFORM_PACKAGE_NAME, 0, mUserId));
+                } catch (NameNotFoundException e) {
+                    // won't happen; we know a priori that it's valid
+                }
+            }
+            if (hasSettings) {
+                try {
+                    mAcceptSet.add(backupManagerService.getPackageManager().getPackageInfoAsUser(
+                            SETTINGS_PACKAGE, 0, mUserId));
+                } catch (NameNotFoundException e) {
+                    // this one is always valid too
+                }
+            }
+        }
+
+        mAcceptSet = backupManagerService.filterUserFacingPackages(mAcceptSet);
+
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "Restore; accept set size is " + mAcceptSet.size());
+            for (PackageInfo info : mAcceptSet) {
+                Slog.v(TAG, "   " + info.packageName);
+            }
+        }
+    }
+
+    private String[] packagesToNames(List<PackageInfo> apps) {
+        final int N = apps.size();
+        String[] names = new String[N];
+        for (int i = 0; i < N; i++) {
+            names[i] = apps.get(i).packageName;
+        }
+        return names;
+    }
+
+    // Execute one tick of whatever state machine the task implements
+    @Override
+    public void execute() {
+        if (MORE_DEBUG) {
+            Slog.v(TAG, "*** Executing restore step " + mState);
+        }
+        switch (mState) {
+            case INITIAL:
+                startRestore();
+                break;
+
+            case RUNNING_QUEUE:
+                dispatchNextRestore();
+                break;
+
+            case RESTORE_KEYVALUE:
+                restoreKeyValue();
+                break;
+
+            case RESTORE_FULL:
+                restoreFull();
+                break;
+
+            case RESTORE_FINISHED:
+                restoreFinished();
+                break;
+
+            case FINAL:
+                if (!mFinished) {
+                    finalizeRestore();
+                } else {
+                    Slog.e(TAG, "Duplicate finish");
+                }
+                mFinished = true;
+                break;
+        }
+    }
+
+    /*
+     * SKETCH OF OPERATION
+     *
+     * create one of these PerformUnifiedRestoreTask objects, telling it which
+     * dataset & transport to address, and then parameters within the restore
+     * operation: single target package vs many, etc.
+     *
+     * 1. transport.startRestore(token, list-of-packages).  If we need @pm@  it is
+     * always placed first and the settings provider always placed last [for now].
+     *
+     * 1a [if we needed @pm@ then nextRestorePackage() and restore the PMBA inline]
+     *
+     *   [ state change => RUNNING_QUEUE ]
+     *
+     * NOW ITERATE:
+     *
+     * { 3. t.nextRestorePackage()
+     *   4. does the metadata for this package allow us to restore it?
+     *      does the on-disk app permit us to restore it? [re-check allowBackup etc]
+     *   5. is this a key/value dataset?  => key/value agent restore
+     *       [ state change => RESTORE_KEYVALUE ]
+     *       5a. spin up agent
+     *       5b. t.getRestoreData() to stage it properly
+     *       5c. call into agent to perform restore
+     *       5d. tear down agent
+     *       [ state change => RUNNING_QUEUE ]
+     *
+     *   6. else it's a stream dataset:
+     *       [ state change => RESTORE_FULL ]
+     *       6a. instantiate the engine for a stream restore: engine handles agent lifecycles
+     *       6b. spin off engine runner on separate thread
+     *       6c. ITERATE getNextFullRestoreDataChunk() and copy data to engine runner socket
+     *       [ state change => RUNNING_QUEUE ]
+     * }
+     *
+     *   [ state change => FINAL ]
+     *
+     * 7. t.finishRestore(), call listeners, etc.
+     *
+     *
+     */
+
+    // state INITIAL : set up for the restore and read the metadata if necessary
+    private void startRestore() {
+        sendStartRestore(mAcceptSet.size());
+
+        // If we're starting a full-system restore, set up to begin widget ID remapping
+        if (mIsSystemRestore) {
+            AppWidgetBackupBridge.restoreStarting(mUserId);
+        }
+
+        try {
+            String transportDirName =
+                    mTransportManager.getTransportDirName(mTransportClient.getTransportComponent());
+            mStateDir = new File(backupManagerService.getBaseStateDir(), transportDirName);
+
+            // Fetch the current metadata from the dataset first
+            PackageInfo pmPackage = new PackageInfo();
+            pmPackage.packageName = PACKAGE_MANAGER_SENTINEL;
+            mAcceptSet.add(0, pmPackage);
+
+            PackageInfo[] packages = mAcceptSet.toArray(new PackageInfo[0]);
+
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow("PerformUnifiedRestoreTask.startRestore()");
+
+            mStatus = transport.startRestore(mToken, packages);
+            if (mStatus != BackupTransport.TRANSPORT_OK) {
+                Slog.e(TAG, "Transport error " + mStatus + "; no restore possible");
+                mStatus = BackupTransport.TRANSPORT_ERROR;
+                executeNextState(UnifiedRestoreState.FINAL);
+                return;
+            }
+
+            RestoreDescription desc = transport.nextRestorePackage();
+            if (desc == null) {
+                Slog.e(TAG, "No restore metadata available; halting");
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE,
+                        mCurrentPackage,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                mStatus = BackupTransport.TRANSPORT_ERROR;
+                executeNextState(UnifiedRestoreState.FINAL);
+                return;
+            }
+            if (!PACKAGE_MANAGER_SENTINEL.equals(
+                    desc.getPackageName())) {
+                Slog.e(TAG, "Required package metadata but got "
+                        + desc.getPackageName());
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED,
+                        mCurrentPackage,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                mStatus = BackupTransport.TRANSPORT_ERROR;
+                executeNextState(UnifiedRestoreState.FINAL);
+                return;
+            }
+
+            // Pull the Package Manager metadata from the restore set first
+            mCurrentPackage = new PackageInfo();
+            mCurrentPackage.packageName = PACKAGE_MANAGER_SENTINEL;
+            mPmAgent = backupManagerService.makeMetadataAgent(null);
+            mAgent = IBackupAgent.Stub.asInterface(mPmAgent.onBind());
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "initiating restore for PMBA");
+            }
+            initiateOneRestore(mCurrentPackage, 0);
+            // The PM agent called operationComplete() already, because our invocation
+            // of it is process-local and therefore synchronous.  That means that the
+            // next-state message (RUNNING_QUEUE) is already enqueued.  Only if we're
+            // unable to proceed with running the queue do we remove that pending
+            // message and jump straight to the FINAL state.  Because this was
+            // synchronous we also know that we should cancel the pending timeout
+            // message.
+            backupManagerService.getBackupHandler().removeMessages(
+                    MSG_RESTORE_OPERATION_TIMEOUT);
+
+            // Verify that the backup set includes metadata.  If not, we can't do
+            // signature/version verification etc, so we simply do not proceed with
+            // the restore operation.
+            if (!mPmAgent.hasMetadata()) {
+                Slog.e(TAG, "PM agent has no metadata, so not restoring");
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA,
+                        mCurrentPackage,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                        PACKAGE_MANAGER_SENTINEL,
+                        "Package manager restore metadata missing");
+                mStatus = BackupTransport.TRANSPORT_ERROR;
+                backupManagerService.getBackupHandler().removeMessages(
+                        MSG_BACKUP_RESTORE_STEP, this);
+                executeNextState(UnifiedRestoreState.FINAL);
+                return;
+            }
+
+            // Success; cache the metadata and continue as expected with the
+            // next state already enqueued
+
+        } catch (Exception e) {
+            // If we lost the transport at any time, halt
+            Slog.e(TAG, "Unable to contact transport for restore: " + e.getMessage());
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT,
+                    null,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
+            mStatus = BackupTransport.TRANSPORT_ERROR;
+            backupManagerService.getBackupHandler().removeMessages(
+                    MSG_BACKUP_RESTORE_STEP, this);
+            executeNextState(UnifiedRestoreState.FINAL);
+            return;
+        }
+    }
+
+    // state RUNNING_QUEUE : figure out what the next thing to be restored is,
+    // and fire the appropriate next step
+    private void dispatchNextRestore() {
+        UnifiedRestoreState nextState = UnifiedRestoreState.FINAL;
+        try {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow(
+                            "PerformUnifiedRestoreTask.dispatchNextRestore()");
+            mRestoreDescription = transport.nextRestorePackage();
+            final String pkgName = (mRestoreDescription != null)
+                    ? mRestoreDescription.getPackageName() : null;
+            if (pkgName == null) {
+                Slog.e(TAG, "Failure getting next package name");
+                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+                nextState = UnifiedRestoreState.FINAL;
+                return;
+            } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
+                // Yay we've reached the end cleanly
+                if (DEBUG) {
+                    Slog.v(TAG, "No more packages; finishing restore");
+                }
+                int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime);
+                EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis);
+                nextState = UnifiedRestoreState.FINAL;
+                return;
+            }
+
+            if (DEBUG) {
+                Slog.i(TAG, "Next restore package: " + mRestoreDescription);
+            }
+            sendOnRestorePackage(pkgName);
+
+            Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName);
+            if (metaInfo == null) {
+                Slog.e(TAG, "No metadata for " + pkgName);
+                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
+                        "Package metadata missing");
+                nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                return;
+            }
+
+            try {
+                mCurrentPackage = backupManagerService.getPackageManager().getPackageInfoAsUser(
+                        pkgName, PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
+            } catch (NameNotFoundException e) {
+                // Whoops, we thought we could restore this package but it
+                // turns out not to be present.  Skip it.
+                Slog.e(TAG, "Package not present: " + pkgName);
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT,
+                        mCurrentPackage,
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        null);
+                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
+                        "Package missing on device");
+                nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                return;
+            }
+
+            if (metaInfo.versionCode > mCurrentPackage.getLongVersionCode()) {
+                // Data is from a "newer" version of the app than we have currently
+                // installed.  If the app has not declared that it is prepared to
+                // handle this case, we do not attempt the restore.
+                if ((mCurrentPackage.applicationInfo.flags
+                        & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) {
+                    String message = "Source version " + metaInfo.versionCode
+                            + " > installed version " + mCurrentPackage.getLongVersionCode();
+                    Slog.w(TAG, "Package " + pkgName + ": " + message);
+                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                            BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
+                            metaInfo.versionCode);
+                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                            monitoringExtras,
+                            BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, false);
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
+                            mCurrentPackage,
+                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            monitoringExtras);
+                    EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                            pkgName, message);
+                    nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                    return;
+                } else {
+                    if (DEBUG) {
+                        Slog.v(TAG, "Source version " + metaInfo.versionCode
+                                + " > installed version " + mCurrentPackage.getLongVersionCode()
+                                + " but restoreAnyVersion");
+                    }
+                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                            BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
+                            metaInfo.versionCode);
+                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                            monitoringExtras,
+                            BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, true);
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
+                            mCurrentPackage,
+                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            monitoringExtras);
+                }
+            }
+
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "Package " + pkgName
+                        + " restore version [" + metaInfo.versionCode
+                        + "] is compatible with installed version ["
+                        + mCurrentPackage.getLongVersionCode() + "]");
+            }
+
+            // Reset per-package preconditions and fire the appropriate next state
+            mWidgetData = null;
+            final int type = mRestoreDescription.getDataType();
+            if (type == RestoreDescription.TYPE_KEY_VALUE) {
+                nextState = UnifiedRestoreState.RESTORE_KEYVALUE;
+            } else if (type == RestoreDescription.TYPE_FULL_STREAM) {
+                nextState = UnifiedRestoreState.RESTORE_FULL;
+            } else {
+                // Unknown restore type; ignore this package and move on
+                Slog.e(TAG, "Unrecognized restore type " + type);
+                nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                return;
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Can't get next restore target from transport; halting: "
+                    + e.getMessage());
+            EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+            nextState = UnifiedRestoreState.FINAL;
+            return;
+        } finally {
+            executeNextState(nextState);
+        }
+    }
+
+    // state RESTORE_KEYVALUE : restore one package via key/value API set
+    private void restoreKeyValue() {
+        // Initiating the restore will pass responsibility for the state machine's
+        // progress to the agent callback, so we do not always execute the
+        // next state here.
+        final String packageName = mCurrentPackage.packageName;
+        // Validate some semantic requirements that apply in this way
+        // only to the key/value restore API flow
+        if (mCurrentPackage.applicationInfo.backupAgentName == null
+                || "".equals(mCurrentPackage.applicationInfo.backupAgentName)) {
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Data exists for package " + packageName
+                        + " but app has no agent; skipping");
+            }
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT, mCurrentPackage,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
+                    "Package has no agent");
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+            return;
+        }
+
+        Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName);
+        PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+        if (!BackupUtils.signaturesMatch(metaInfo.sigHashes, mCurrentPackage, pmi)) {
+            Slog.w(TAG, "Signature mismatch restoring " + packageName);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH, mCurrentPackage,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
+                    "Signature mismatch");
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+            return;
+        }
+
+        // Good to go!  Set up and bind the agent...
+        mAgent = backupManagerService.bindToAgentSynchronous(
+                mCurrentPackage.applicationInfo,
+                ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL);
+        if (mAgent == null) {
+            Slog.w(TAG, "Can't find backup agent for " + packageName);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT, mCurrentPackage,
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
+                    "Restore agent missing");
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+            return;
+        }
+
+        // Whatever happens next, we've launched the target app now; remember that.
+        mDidLaunch = true;
+
+        // And then finally start the restore on this agent
+        try {
+            initiateOneRestore(mCurrentPackage, metaInfo.versionCode);
+            ++mCount;
+        } catch (Exception e) {
+            Slog.e(TAG, "Error when attempting restore: " + e.toString());
+            keyValueAgentErrorCleanup(false);
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+        }
+    }
+
+    // Guts of a key/value restore operation
+    private void initiateOneRestore(PackageInfo app, long appVersionCode) {
+        final String packageName = app.packageName;
+
+        if (DEBUG) {
+            Slog.d(TAG, "initiateOneRestore packageName=" + packageName);
+        }
+
+        // !!! TODO: get the dirs from the transport
+        mBackupDataName = new File(backupManagerService.getDataDir(), packageName + ".restore");
+        mStageName = new File(backupManagerService.getDataDir(), packageName + ".stage");
+        mNewStateName = new File(mStateDir, packageName + ".new");
+
+        boolean staging = shouldStageBackupData(packageName);
+        ParcelFileDescriptor stage;
+        File downloadFile = (staging) ? mStageName : mBackupDataName;
+        boolean startedAgentRestore = false;
+
+        try {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow(
+                            "PerformUnifiedRestoreTask.initiateOneRestore()");
+
+            // Run the transport's restore pass
+            stage = ParcelFileDescriptor.open(downloadFile,
+                    ParcelFileDescriptor.MODE_READ_WRITE |
+                            ParcelFileDescriptor.MODE_CREATE |
+                            ParcelFileDescriptor.MODE_TRUNCATE);
+
+            if (transport.getRestoreData(stage) != BackupTransport.TRANSPORT_OK) {
+                // Transport-level failure, so we wind everything up and
+                // terminate the restore operation.
+                Slog.e(TAG, "Error getting restore data for " + packageName);
+                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+                stage.close();
+                downloadFile.delete();
+                executeNextState(UnifiedRestoreState.FINAL);
+                return;
+            }
+
+            // We have the data from the transport. Now we extract and strip
+            // any per-package metadata (typically widget-related information)
+            // if appropriate
+            if (staging) {
+                stage.close();
+                stage = ParcelFileDescriptor.open(downloadFile,
+                        ParcelFileDescriptor.MODE_READ_ONLY);
+
+                mBackupData = ParcelFileDescriptor.open(mBackupDataName,
+                        ParcelFileDescriptor.MODE_READ_WRITE |
+                                ParcelFileDescriptor.MODE_CREATE |
+                                ParcelFileDescriptor.MODE_TRUNCATE);
+
+                BackupDataInput in = new BackupDataInput(stage.getFileDescriptor());
+                BackupDataOutput out = new BackupDataOutput(mBackupData.getFileDescriptor());
+                filterExcludedKeys(packageName, in, out);
+
+                mBackupData.close();
+            }
+
+            // Okay, we have the data.  Now have the agent do the restore.
+            stage.close();
+
+            mBackupData = ParcelFileDescriptor.open(mBackupDataName,
+                    ParcelFileDescriptor.MODE_READ_ONLY);
+
+            mNewState = ParcelFileDescriptor.open(mNewStateName,
+                    ParcelFileDescriptor.MODE_READ_WRITE |
+                            ParcelFileDescriptor.MODE_CREATE |
+                            ParcelFileDescriptor.MODE_TRUNCATE);
+
+            // Kick off the restore, checking for hung agents.  The timeout or
+            // the operationComplete() callback will schedule the next step,
+            // so we do not do that here.
+            long restoreAgentTimeoutMillis = mAgentTimeoutParameters.getRestoreAgentTimeoutMillis();
+            backupManagerService.prepareOperationTimeout(
+                    mEphemeralOpToken, restoreAgentTimeoutMillis, this, OP_TYPE_RESTORE_WAIT);
+            startedAgentRestore = true;
+            mAgent.doRestoreWithExcludedKeys(mBackupData, appVersionCode, mNewState,
+                    mEphemeralOpToken, backupManagerService.getBackupManagerBinder(),
+                    new ArrayList<>(getExcludedKeysForPackage(packageName)));
+        } catch (Exception e) {
+            Slog.e(TAG, "Unable to call app for restore: " + packageName, e);
+            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                    packageName, e.toString());
+            // Clears any pending timeout messages as well.
+            keyValueAgentErrorCleanup(startedAgentRestore);
+
+            // After a restore failure we go back to running the queue.  If there
+            // are no more packages to be restored that will be handled by the
+            // next step.
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldStageBackupData(String packageName) {
+        // Backup data is staged for 2 reasons:
+        // 1. We might need to exclude keys from the data before passing it to the agent
+        // 2. Widget metadata needs to be separated from the rest to be handled separately
+        // But 'android' package doesn't contain widget metadata so we want to skip staging for it
+        // when there are no keys to be excluded either.
+        return !packageName.equals(PLATFORM_PACKAGE_NAME) ||
+                !getExcludedKeysForPackage(PLATFORM_PACKAGE_NAME).isEmpty();
+    }
+
+    @VisibleForTesting
+    Set<String> getExcludedKeysForPackage(String packageName) {
+        return backupManagerService.getExcludedRestoreKeys(packageName);
+    }
+
+    @VisibleForTesting
+    void filterExcludedKeys(String packageName, BackupDataInput in, BackupDataOutput out)
+            throws Exception {
+        Set<String> excludedKeysForPackage = getExcludedKeysForPackage(packageName);
+
+        byte[] buffer = new byte[8192]; // will grow when needed
+        while (in.readNextHeader()) {
+            final String key = in.getKey();
+            final int size = in.getDataSize();
+
+            if (excludedKeysForPackage != null && excludedKeysForPackage.contains(key)) {
+                Slog.i(TAG, "Skipping blocked key " + key);
+                in.skipEntityData();
+                continue;
+            }
+
+            // is this a special key?
+            if (key.equals(KEY_WIDGET_STATE)) {
+                if (DEBUG) {
+                    Slog.i(TAG, "Restoring widget state for " + packageName);
+                }
+                mWidgetData = new byte[size];
+                in.readEntityData(mWidgetData, 0, size);
+            } else {
+                if (size > buffer.length) {
+                    buffer = new byte[size];
+                }
+                in.readEntityData(buffer, 0, size);
+                out.writeEntityHeader(key, size);
+                out.writeEntityData(buffer, size);
+            }
+        }
+    }
+
+    // state RESTORE_FULL : restore one package via streaming engine
+    private void restoreFull() {
+        // None of this can run on the work looper here, so we spin asynchronous
+        // work like this:
+        //
+        //   StreamFeederThread: read data from transport.getNextFullRestoreDataChunk()
+        //                       write it into the pipe to the engine
+        //   EngineThread: FullRestoreEngine thread communicating with the target app
+        //
+        // When finished, StreamFeederThread executes next state as appropriate on the
+        // backup looper, and the overall unified restore task resumes
+        try {
+            StreamFeederThread feeder = new StreamFeederThread();
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Spinning threads for stream restore of "
+                        + mCurrentPackage.packageName);
+            }
+            new Thread(feeder, "unified-stream-feeder").start();
+
+            // At this point the feeder is responsible for advancing the restore
+            // state, so we're done here.
+        } catch (IOException e) {
+            // Unable to instantiate the feeder thread -- we need to bail on the
+            // current target.  We haven't asked the transport for data yet, though,
+            // so we can do that simply by going back to running the restore queue.
+            Slog.e(TAG, "Unable to construct pipes for stream restore!");
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+        }
+    }
+
+    // state RESTORE_FINISHED : provide the "no more data" signpost callback at the end
+    private void restoreFinished() {
+        if (DEBUG) {
+            Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName);
+        }
+        try {
+            long restoreAgentFinishedTimeoutMillis =
+                    mAgentTimeoutParameters.getRestoreAgentFinishedTimeoutMillis();
+            backupManagerService
+                    .prepareOperationTimeout(mEphemeralOpToken,
+                            restoreAgentFinishedTimeoutMillis, this,
+                            OP_TYPE_RESTORE_WAIT);
+            mAgent.doRestoreFinished(mEphemeralOpToken,
+                    backupManagerService.getBackupManagerBinder());
+            // If we get this far, the callback or timeout will schedule the
+            // next restore state, so we're done
+        } catch (Exception e) {
+            final String packageName = mCurrentPackage.packageName;
+            Slog.e(TAG, "Unable to finalize restore of " + packageName);
+            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                    packageName, e.toString());
+            keyValueAgentErrorCleanup(true);
+            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+        }
+    }
+
+    class StreamFeederThread extends RestoreEngine implements Runnable, BackupRestoreTask {
+
+        final String TAG = "StreamFeederThread";
+        FullRestoreEngine mEngine;
+        FullRestoreEngineThread mEngineThread;
+
+        // pipe through which we read data from the transport. [0] read, [1] write
+        ParcelFileDescriptor[] mTransportPipes;
+
+        // pipe through which the engine will read data.  [0] read, [1] write
+        ParcelFileDescriptor[] mEnginePipes;
+
+        private final int mEphemeralOpToken;
+
+        public StreamFeederThread() throws IOException {
+            mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
+            mTransportPipes = ParcelFileDescriptor.createPipe();
+            mEnginePipes = ParcelFileDescriptor.createPipe();
+            setRunning(true);
+        }
+
+        @Override
+        public void run() {
+            UnifiedRestoreState nextState = UnifiedRestoreState.RUNNING_QUEUE;
+            int status = BackupTransport.TRANSPORT_OK;
+
+            EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
+                    mCurrentPackage.packageName);
+
+            mEngine = new FullRestoreEngine(backupManagerService, this, null,
+                    mMonitor, mCurrentPackage, false, mEphemeralOpToken, false);
+            mEngineThread = new FullRestoreEngineThread(mEngine, mEnginePipes[0]);
+
+            ParcelFileDescriptor eWriteEnd = mEnginePipes[1];
+            ParcelFileDescriptor tReadEnd = mTransportPipes[0];
+            ParcelFileDescriptor tWriteEnd = mTransportPipes[1];
+
+            int bufferSize = 32 * 1024;
+            byte[] buffer = new byte[bufferSize];
+            FileOutputStream engineOut = new FileOutputStream(eWriteEnd.getFileDescriptor());
+            FileInputStream transportIn = new FileInputStream(tReadEnd.getFileDescriptor());
+
+            // spin up the engine and start moving data to it
+            new Thread(mEngineThread, "unified-restore-engine").start();
+
+            String callerLogString = "PerformUnifiedRestoreTask$StreamFeederThread.run()";
+            try {
+                IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString);
+                while (status == BackupTransport.TRANSPORT_OK) {
+                    // have the transport write some of the restoring data to us
+                    int result = transport.getNextFullRestoreDataChunk(tWriteEnd);
+                    if (result > 0) {
+                        // The transport wrote this many bytes of restore data to the
+                        // pipe, so pass it along to the engine.
+                        if (MORE_DEBUG) {
+                            Slog.v(TAG, "  <- transport provided chunk size " + result);
+                        }
+                        if (result > bufferSize) {
+                            bufferSize = result;
+                            buffer = new byte[bufferSize];
+                        }
+                        int toCopy = result;
+                        while (toCopy > 0) {
+                            int n = transportIn.read(buffer, 0, toCopy);
+                            engineOut.write(buffer, 0, n);
+                            toCopy -= n;
+                            if (MORE_DEBUG) {
+                                Slog.v(TAG, "  -> wrote " + n + " to engine, left=" + toCopy);
+                            }
+                        }
+                    } else if (result == BackupTransport.NO_MORE_DATA) {
+                        // Clean finish.  Wind up and we're done!
+                        if (MORE_DEBUG) {
+                            Slog.i(TAG, "Got clean full-restore EOF for "
+                                    + mCurrentPackage.packageName);
+                        }
+                        status = BackupTransport.TRANSPORT_OK;
+                        break;
+                    } else {
+                        // Transport reported some sort of failure; the fall-through
+                        // handling will deal properly with that.
+                        Slog.e(TAG, "Error " + result + " streaming restore for "
+                                + mCurrentPackage.packageName);
+                        EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+                        status = result;
+                    }
+                }
+                if (MORE_DEBUG) {
+                    Slog.v(TAG, "Done copying to engine, falling through");
+                }
+            } catch (IOException e) {
+                // We lost our ability to communicate via the pipes.  That's worrying
+                // but potentially recoverable; abandon this package's restore but
+                // carry on with the next restore target.
+                Slog.e(TAG, "Unable to route data for restore");
+                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                        mCurrentPackage.packageName, "I/O error on pipes");
+                status = BackupTransport.AGENT_ERROR;
+            } catch (Exception e) {
+                // The transport threw; terminate the whole operation.  Closing
+                // the sockets will wake up the engine and it will then tidy up the
+                // remote end.
+                Slog.e(TAG, "Transport failed during restore: " + e.getMessage());
+                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+                status = BackupTransport.TRANSPORT_ERROR;
+            } finally {
+                // Close the transport pipes and *our* end of the engine pipe,
+                // but leave the engine thread's end open so that it properly
+                // hits EOF and winds up its operations.
+                IoUtils.closeQuietly(mEnginePipes[1]);
+                IoUtils.closeQuietly(mTransportPipes[0]);
+                IoUtils.closeQuietly(mTransportPipes[1]);
+
+                // Don't proceed until the engine has wound up operations
+                mEngineThread.waitForResult();
+
+                // Now we're really done with this one too
+                IoUtils.closeQuietly(mEnginePipes[0]);
+
+                // In all cases we want to remember whether we launched
+                // the target app as part of our work so far.
+                mDidLaunch = (mEngine.getAgent() != null);
+
+                // If we hit a transport-level error, we are done with everything;
+                // if we hit an agent error we just go back to running the queue.
+                if (status == BackupTransport.TRANSPORT_OK) {
+                    // Clean finish means we issue the restore-finished callback
+                    nextState = UnifiedRestoreState.RESTORE_FINISHED;
+
+                    // the engine bound the target's agent, so recover that binding
+                    // to use for the callback.
+                    mAgent = mEngine.getAgent();
+
+                    // and the restored widget data, if any
+                    mWidgetData = mEngine.getWidgetData();
+                } else {
+                    // Something went wrong somewhere.  Whether it was at the transport
+                    // level is immaterial; we need to tell the transport to bail
+                    try {
+                        IBackupTransport transport =
+                                mTransportClient.connectOrThrow(callerLogString);
+                        transport.abortFullRestore();
+                    } catch (Exception e) {
+                        // transport itself is dead; make sure we handle this as a
+                        // fatal error
+                        Slog.e(TAG, "Transport threw from abortFullRestore: " + e.getMessage());
+                        status = BackupTransport.TRANSPORT_ERROR;
+                    }
+
+                    // We also need to wipe the current target's data, as it's probably
+                    // in an incoherent state.
+                    backupManagerService.clearApplicationDataAfterRestoreFailure(
+                            mCurrentPackage.packageName);
+
+                    // Schedule the next state based on the nature of our failure
+                    if (status == BackupTransport.TRANSPORT_ERROR) {
+                        nextState = UnifiedRestoreState.FINAL;
+                    } else {
+                        nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                    }
+                }
+                executeNextState(nextState);
+                setRunning(false);
+            }
+        }
+
+        // BackupRestoreTask interface, specifically for timeout handling
+
+        @Override
+        public void execute() { /* intentionally empty */ }
+
+        @Override
+        public void operationComplete(long result) { /* intentionally empty */ }
+
+        // The app has timed out handling a restoring file
+        @Override
+        public void handleCancel(boolean cancelAll) {
+            backupManagerService.removeOperation(mEphemeralOpToken);
+            if (DEBUG) {
+                Slog.w(TAG, "Full-data restore target timed out; shutting down");
+            }
+
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT,
+                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+            mEngineThread.handleTimeout();
+
+            IoUtils.closeQuietly(mEnginePipes[1]);
+            mEnginePipes[1] = null;
+            IoUtils.closeQuietly(mEnginePipes[0]);
+            mEnginePipes[0] = null;
+        }
+    }
+
+    // state FINAL : tear everything down and we're done.
+    private void finalizeRestore() {
+        if (MORE_DEBUG) {
+            Slog.d(TAG, "finishing restore mObserver=" + mObserver);
+        }
+
+        String callerLogString = "PerformUnifiedRestoreTask.finalizeRestore()";
+        try {
+            IBackupTransport transport =
+                    mTransportClient.connectOrThrow(callerLogString);
+            transport.finishRestore();
+        } catch (Exception e) {
+            Slog.e(TAG, "Error finishing restore", e);
+        }
+
+        // Tell the observer we're done
+        if (mObserver != null) {
+            try {
+                mObserver.restoreFinished(mStatus);
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Restore observer died at restoreFinished");
+            }
+        }
+
+        // Clear any ongoing session timeout.
+        backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+
+        // If we have a PM token, we must under all circumstances be sure to
+        // handshake when we've finished.
+        if (mPmToken > 0) {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "finishing PM token " + mPmToken);
+            }
+            try {
+                backupManagerService.getPackageManagerBinder().finishPackageInstall(mPmToken,
+                        mDidLaunch);
+            } catch (RemoteException e) { /* can't happen */ }
+        } else {
+            // We were invoked via an active restore session, not by the Package
+            // Manager, so start up the session timeout again.
+            long restoreAgentTimeoutMillis = mAgentTimeoutParameters.getRestoreAgentTimeoutMillis();
+            backupManagerService.getBackupHandler().sendEmptyMessageDelayed(
+                    MSG_RESTORE_SESSION_TIMEOUT,
+                    restoreAgentTimeoutMillis);
+        }
+
+        // Kick off any work that may be needed regarding app widget restores
+        AppWidgetBackupBridge.restoreFinished(mUserId);
+
+        // If this was a full-system restore, record the ancestral
+        // dataset information
+        if (mIsSystemRestore && mPmAgent != null) {
+            backupManagerService.setAncestralPackages(mPmAgent.getRestoredPackages());
+            backupManagerService.setAncestralToken(mToken);
+            backupManagerService.writeRestoreTokens();
+        }
+
+        synchronized (backupManagerService.getPendingRestores()) {
+            if (backupManagerService.getPendingRestores().size() > 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Starting next pending restore.");
+                }
+                PerformUnifiedRestoreTask task = backupManagerService.getPendingRestores().remove();
+                backupManagerService.getBackupHandler().sendMessage(
+                        backupManagerService.getBackupHandler().obtainMessage(
+                                MSG_BACKUP_RESTORE_STEP, task));
+
+            } else {
+                backupManagerService.setRestoreInProgress(false);
+                if (MORE_DEBUG) {
+                    Slog.d(TAG, "No pending restores.");
+                }
+            }
+        }
+
+        Slog.i(TAG, "Restore complete.");
+        mListener.onFinished(callerLogString);
+    }
+
+    /**
+     * @param clearAppData - set to {@code true} if the backup agent had already been invoked when
+     *     restore faied. So the app data may be in corrupted state and has to be cleared.
+     */
+    void keyValueAgentErrorCleanup(boolean clearAppData) {
+        if (clearAppData) {
+            // If the agent fails restore, it might have put the app's data
+            // into an incoherent state.  For consistency we wipe its data
+            // again in this case before continuing with normal teardown
+            backupManagerService.clearApplicationDataAfterRestoreFailure(
+                    mCurrentPackage.packageName);
+        }
+        keyValueAgentCleanup();
+    }
+
+    // TODO: clean up naming; this is now used at finish by both k/v and stream restores
+    void keyValueAgentCleanup() {
+        mBackupDataName.delete();
+        mStageName.delete();
+        try {
+            if (mBackupData != null) {
+                mBackupData.close();
+            }
+        } catch (IOException e) {
+        }
+        try {
+            if (mNewState != null) {
+                mNewState.close();
+            }
+        } catch (IOException e) {
+        }
+        mBackupData = mNewState = null;
+
+        // if everything went okay, remember the recorded state now
+        //
+        // !!! TODO: the restored data could be migrated on the server
+        // side into the current dataset.  In that case the new state file
+        // we just created would reflect the data already extant in the
+        // backend, so there'd be nothing more to do.  Until that happens,
+        // however, we need to make sure that we record the data to the
+        // current backend dataset.  (Yes, this means shipping the data over
+        // the wire in both directions.  That's bad, but consistency comes
+        // first, then efficiency.)  Once we introduce server-side data
+        // migration to the newly-restored device's dataset, we will change
+        // the following from a discard of the newly-written state to the
+        // "correct" operation of renaming into the canonical state blob.
+        mNewStateName.delete();                      // TODO: remove; see above comment
+
+        // If this wasn't the PM pseudopackage, tear down the agent side
+        if (mCurrentPackage.applicationInfo != null) {
+            // unbind and tidy up even on timeout or failure
+            try {
+                backupManagerService.getActivityManager().unbindBackupAgent(
+                        mCurrentPackage.applicationInfo);
+
+                // The agent was probably running with a stub Application object,
+                // which isn't a valid run mode for the main app logic.  Shut
+                // down the app so that next time it's launched, it gets the
+                // usual full initialization.  Note that this is only done for
+                // full-system restores: when a single app has requested a restore,
+                // it is explicitly not killed following that operation.
+                //
+                // We execute this kill when these conditions hold:
+                //    1. it's not a system-uid process,
+                //    2. the app did not request its own restore (mTargetPackage == null), and
+                // either
+                //    3a. the app is a full-data target (TYPE_FULL_STREAM) or
+                //     b. the app does not state android:killAfterRestore="false" in its manifest
+                final int appFlags = mCurrentPackage.applicationInfo.flags;
+                final boolean killAfterRestore =
+                        !UserHandle.isCore(mCurrentPackage.applicationInfo.uid)
+                                && ((mRestoreDescription.getDataType()
+                                == RestoreDescription.TYPE_FULL_STREAM)
+                                || ((appFlags & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0));
+
+                if (mTargetPackage == null && killAfterRestore) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Restore complete, killing host process of "
+                                + mCurrentPackage.applicationInfo.processName);
+                    }
+                    backupManagerService.getActivityManager().killApplicationProcess(
+                            mCurrentPackage.applicationInfo.processName,
+                            mCurrentPackage.applicationInfo.uid);
+                }
+            } catch (RemoteException e) {
+                // can't happen; we run in the same process as the activity manager
+            }
+        }
+
+        // The caller is responsible for reestablishing the state machine; our
+        // responsibility here is to clear the decks for whatever comes next.
+        backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_OPERATION_TIMEOUT, this);
+    }
+
+    @Override
+    public void operationComplete(long unusedResult) {
+        backupManagerService.removeOperation(mEphemeralOpToken);
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "operationComplete() during restore: target="
+                    + mCurrentPackage.packageName
+                    + " state=" + mState);
+        }
+
+        final UnifiedRestoreState nextState;
+        switch (mState) {
+            case INITIAL:
+                // We've just (manually) restored the PMBA.  It doesn't need the
+                // additional restore-finished callback so we bypass that and go
+                // directly to running the queue.
+                nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                break;
+
+            case RESTORE_KEYVALUE:
+            case RESTORE_FULL: {
+                // Okay, we've just heard back from the agent that it's done with
+                // the restore itself.  We now have to send the same agent its
+                // doRestoreFinished() callback, so roll into that state.
+                nextState = UnifiedRestoreState.RESTORE_FINISHED;
+                break;
+            }
+
+            case RESTORE_FINISHED: {
+                // Okay, we're done with this package.  Tidy up and go on to the next
+                // app in the queue.
+                int size = (int) mBackupDataName.length();
+                EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE,
+                        mCurrentPackage.packageName, size);
+
+                // Just go back to running the restore queue
+                keyValueAgentCleanup();
+
+                // If there was widget state associated with this app, get the OS to
+                // incorporate it into current bookeeping and then pass that along to
+                // the app as part of the restore-time work.
+                if (mWidgetData != null) {
+                    backupManagerService.restoreWidgetData(mCurrentPackage.packageName,
+                            mWidgetData);
+                }
+
+                nextState = UnifiedRestoreState.RUNNING_QUEUE;
+                break;
+            }
+
+            default: {
+                // Some kind of horrible semantic error; we're in an unexpected state.
+                // Back off hard and wind up.
+                Slog.e(TAG, "Unexpected restore callback into state " + mState);
+                keyValueAgentErrorCleanup(true);
+                nextState = UnifiedRestoreState.FINAL;
+                break;
+            }
+        }
+
+        executeNextState(nextState);
+    }
+
+    // A call to agent.doRestore() or agent.doRestoreFinished() has timed out
+    @Override
+    public void handleCancel(boolean cancelAll) {
+        backupManagerService.removeOperation(mEphemeralOpToken);
+        Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
+        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
+                mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+        EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
+                mCurrentPackage.packageName, "restore timeout");
+        // Handle like an agent that threw on invocation: wipe it and go on to the next
+        keyValueAgentErrorCleanup(true);
+        executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
+    }
+
+    void executeNextState(UnifiedRestoreState nextState) {
+        if (MORE_DEBUG) {
+            Slog.i(TAG, " => executing next step on "
+                    + this + " nextState=" + nextState);
+        }
+        mState = nextState;
+        Message msg = backupManagerService.getBackupHandler().obtainMessage(
+                MSG_BACKUP_RESTORE_STEP, this);
+        backupManagerService.getBackupHandler().sendMessage(msg);
+    }
+
+    // restore observer support
+    void sendStartRestore(int numPackages) {
+        if (mObserver != null) {
+            try {
+                mObserver.restoreStarting(numPackages);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Restore observer went away: startRestore");
+                mObserver = null;
+            }
+        }
+    }
+
+    void sendOnRestorePackage(String name) {
+        if (mObserver != null) {
+            try {
+                mObserver.onUpdate(mCount, name);
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Restore observer died in onUpdate");
+                mObserver = null;
+            }
+        }
+    }
+
+    void sendEndRestore() {
+        if (mObserver != null) {
+            try {
+                mObserver.restoreFinished(mStatus);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Restore observer went away: endRestore");
+                mObserver = null;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/restore/RestoreDeleteObserver.java b/com/android/server/backup/restore/RestoreDeleteObserver.java
new file mode 100644
index 0000000..12be84c
--- /dev/null
+++ b/com/android/server/backup/restore/RestoreDeleteObserver.java
@@ -0,0 +1,69 @@
+/*
+ * 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.restore;
+
+import android.content.pm.IPackageDeleteObserver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Synchronous implementation of IPackageDeleteObserver.Stub.
+ *
+ * Allows the caller to synchronously wait for package deleted event.
+ */
+public class RestoreDeleteObserver extends IPackageDeleteObserver.Stub {
+
+    @GuardedBy("mDone")
+    private final AtomicBoolean mDone = new AtomicBoolean();
+
+    public RestoreDeleteObserver() {
+    }
+
+    /**
+     * Resets the observer to prepare for another removal.
+     */
+    public void reset() {
+        synchronized (mDone) {
+            mDone.set(false);
+        }
+    }
+
+    /**
+     * Synchronously waits for completion.
+     */
+    public void waitForCompletion() {
+        synchronized (mDone) {
+            while (mDone.get() == false) {
+                try {
+                    mDone.wait();
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public void packageDeleted(String packageName, int returnCode) throws RemoteException {
+        synchronized (mDone) {
+            mDone.set(true);
+            mDone.notifyAll();
+        }
+    }
+}
diff --git a/com/android/server/backup/restore/RestoreEngine.java b/com/android/server/backup/restore/RestoreEngine.java
new file mode 100644
index 0000000..9d3ae86
--- /dev/null
+++ b/com/android/server/backup/restore/RestoreEngine.java
@@ -0,0 +1,68 @@
+/*
+ * 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.restore;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Restore infrastructure.
+ */
+public abstract class RestoreEngine {
+
+    private static final String TAG = "RestoreEngine";
+
+    public static final int SUCCESS = 0;
+    public static final int TARGET_FAILURE = -2;
+    public static final int TRANSPORT_FAILURE = -3;
+
+    private final AtomicBoolean mRunning = new AtomicBoolean(false);
+    private final AtomicInteger mResult = new AtomicInteger(SUCCESS);
+
+    public boolean isRunning() {
+        return mRunning.get();
+    }
+
+    public void setRunning(boolean stillRunning) {
+        synchronized (mRunning) {
+            mRunning.set(stillRunning);
+            mRunning.notifyAll();
+        }
+    }
+
+    public int waitForResult() {
+        synchronized (mRunning) {
+            while (isRunning()) {
+                try {
+                    mRunning.wait();
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+        return getResult();
+    }
+
+    public int getResult() {
+        return mResult.get();
+    }
+
+    public void setResult(int result) {
+        mResult.set(result);
+    }
+
+    // TODO: abstract restore state and APIs
+}
diff --git a/com/android/server/backup/restore/RestoreFileRunnable.java b/com/android/server/backup/restore/RestoreFileRunnable.java
new file mode 100644
index 0000000..c4aa2d7
--- /dev/null
+++ b/com/android/server/backup/restore/RestoreFileRunnable.java
@@ -0,0 +1,65 @@
+/*
+ * 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.restore;
+
+import android.app.IBackupAgent;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+
+import com.android.server.backup.FileMetadata;
+import com.android.server.backup.UserBackupManagerService;
+
+import java.io.IOException;
+
+/**
+ * Runner that can be placed in a separate thread to do in-process invocations of the full restore
+ * API asynchronously. Used by adb restore.
+ */
+class RestoreFileRunnable implements Runnable {
+
+    private final IBackupAgent mAgent;
+    private final FileMetadata mInfo;
+    private final ParcelFileDescriptor mSocket;
+    private final int mToken;
+    private final UserBackupManagerService mBackupManagerService;
+
+    RestoreFileRunnable(UserBackupManagerService backupManagerService, IBackupAgent agent,
+            FileMetadata info, ParcelFileDescriptor socket, int token) throws IOException {
+        mAgent = agent;
+        mInfo = info;
+        mToken = token;
+
+        // This class is used strictly for process-local binder invocations.  The
+        // semantics of ParcelFileDescriptor differ in this case; in particular, we
+        // do not automatically get a 'dup'ed descriptor that we can can continue
+        // to use asynchronously from the caller.  So, we make sure to dup it ourselves
+        // before proceeding to do the restore.
+        mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
+        this.mBackupManagerService = backupManagerService;
+    }
+
+    @Override
+    public void run() {
+        try {
+            mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type,
+                    mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime,
+                    mToken, mBackupManagerService.getBackupManagerBinder());
+        } catch (RemoteException e) {
+            // never happens; this is used strictly for local binder calls
+        }
+    }
+}
diff --git a/com/android/server/backup/restore/RestorePolicy.java b/com/android/server/backup/restore/RestorePolicy.java
new file mode 100644
index 0000000..db25472
--- /dev/null
+++ b/com/android/server/backup/restore/RestorePolicy.java
@@ -0,0 +1,26 @@
+/*
+ * 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.restore;
+
+/**
+ * Full restore from a file/socket.
+ */
+public enum RestorePolicy {
+    IGNORE,
+    ACCEPT,
+    ACCEPT_IF_APK
+}
diff --git a/com/android/server/backup/restore/UnifiedRestoreState.java b/com/android/server/backup/restore/UnifiedRestoreState.java
new file mode 100644
index 0000000..f5bff5e
--- /dev/null
+++ b/com/android/server/backup/restore/UnifiedRestoreState.java
@@ -0,0 +1,29 @@
+/*
+ * 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.restore;
+
+/**
+ * States of the unified-restore state machine.
+ */
+public enum UnifiedRestoreState {
+    INITIAL,
+    RUNNING_QUEUE,
+    RESTORE_KEYVALUE,
+    RESTORE_FULL,
+    RESTORE_FINISHED,
+    FINAL
+}
diff --git a/com/android/server/backup/testing/BackupManagerServiceTestUtils.java b/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
new file mode 100644
index 0000000..77b5b61
--- /dev/null
+++ b/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
@@ -0,0 +1,229 @@
+/*
+ * 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.testing;
+
+import static com.android.server.backup.testing.TestUtils.runToEndOfTasks;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.annotation.Nullable;
+import android.app.Application;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupManagerService;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+
+import org.mockito.stubbing.Answer;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowBinder;
+
+import java.io.File;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Test utils for {@link UserBackupManagerService} and friends. */
+public class BackupManagerServiceTestUtils {
+    /**
+     * Creates an instance of {@link UserBackupManagerService} with a new backup thread and runs
+     * tasks that were posted to it during instantiation.
+     *
+     * <p>If the class-under-test is going to execute methods as the system, it's a good idea to
+     * also call {@link #setUpBinderCallerAndApplicationAsSystem(Application)} before this method.
+     *
+     * @see #createUserBackupManagerServiceAndRunTasks(int, Context, HandlerThread, File, File,
+     *     TransportManager)
+     */
+    public static UserBackupManagerService createUserBackupManagerServiceAndRunTasks(
+            int userId,
+            Context context,
+            File baseStateDir,
+            File dataDir,
+            TransportManager transportManager) {
+        return createUserBackupManagerServiceAndRunTasks(
+                userId, context, startBackupThread(null), baseStateDir, dataDir, transportManager);
+    }
+
+    /**
+     * Creates an instance of {@link UserBackupManagerService} with the supplied backup thread
+     * {@code backupThread} and runs tasks that were posted to it during instantiation.
+     *
+     * <p>If the class-under-test is going to execute methods as the system, it's a good idea to
+     * also call {@link #setUpBinderCallerAndApplicationAsSystem(Application)} before this method.
+     */
+    public static UserBackupManagerService createUserBackupManagerServiceAndRunTasks(
+            int userId,
+            Context context,
+            HandlerThread backupThread,
+            File baseStateDir,
+            File dataDir,
+            TransportManager transportManager) {
+        UserBackupManagerService backupManagerService =
+                UserBackupManagerService.createAndInitializeService(
+                        userId,
+                        context,
+                        new BackupManagerService(context),
+                        backupThread,
+                        baseStateDir,
+                        dataDir,
+                        transportManager);
+        runToEndOfTasks(backupThread.getLooper());
+        return backupManagerService;
+    }
+
+    /**
+     * Sets up basic mocks for {@link UserBackupManagerService} mock. If {@code
+     * backupManagerService} is a spy, make sure you provide in the arguments the same objects that
+     * the original object uses.
+     *
+     * <p>If the class-under-test is going to execute methods as the system, it's a good idea to
+     * also call {@link #setUpBinderCallerAndApplicationAsSystem(Application)}.
+     */
+    @SuppressWarnings("ResultOfMethodCallIgnored")
+    public static void setUpBackupManagerServiceBasics(
+            UserBackupManagerService backupManagerService,
+            Application application,
+            TransportManager transportManager,
+            PackageManager packageManager,
+            Handler backupHandler,
+            UserBackupManagerService.BackupWakeLock wakeLock,
+            BackupAgentTimeoutParameters agentTimeoutParameters) {
+
+        when(backupManagerService.getContext()).thenReturn(application);
+        when(backupManagerService.getTransportManager()).thenReturn(transportManager);
+        when(backupManagerService.getPackageManager()).thenReturn(packageManager);
+        when(backupManagerService.getBackupHandler()).thenReturn(backupHandler);
+        when(backupManagerService.getCurrentOpLock()).thenReturn(new Object());
+        when(backupManagerService.getQueueLock()).thenReturn(new Object());
+        when(backupManagerService.getActivityManager()).thenReturn(mock(IActivityManager.class));
+        when(backupManagerService.getWakelock()).thenReturn(wakeLock);
+        when(backupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
+
+        AccessorMock backupEnabled = mockAccessor(false);
+        doAnswer(backupEnabled.getter).when(backupManagerService).isBackupEnabled();
+        doAnswer(backupEnabled.setter).when(backupManagerService).setBackupEnabled(anyBoolean());
+
+        AccessorMock backupRunning = mockAccessor(false);
+        doAnswer(backupEnabled.getter).when(backupManagerService).isBackupRunning();
+        doAnswer(backupRunning.setter).when(backupManagerService).setBackupRunning(anyBoolean());
+    }
+
+    public static void setUpBinderCallerAndApplicationAsSystem(Application application) {
+        final int uid = Process.SYSTEM_UID;
+        final int pid = 1211;
+        ShadowBinder.setCallingUid(uid);
+        ShadowBinder.setCallingPid(pid);
+        ShadowApplication shadowApplication = shadowOf(application);
+        shadowApplication.grantPermissions(pid, uid, "android.permission.BACKUP");
+        shadowApplication.grantPermissions(pid, uid, "android.permission.CONFIRM_FULL_BACKUP");
+    }
+
+    /**
+     * Returns one getter {@link Answer<T>} and one setter {@link Answer<T>} to be easily passed to
+     * Mockito mocking facilities.
+     *
+     * @param defaultValue Value returned by the getter if there was no setter call until then.
+     */
+    public static <T> AccessorMock<T> mockAccessor(T defaultValue) {
+        AtomicReference<T> holder = new AtomicReference<>(defaultValue);
+        return new AccessorMock<>(
+                invocation -> holder.get(),
+                invocation -> {
+                    holder.set(invocation.getArgument(0));
+                    return null;
+                });
+    }
+
+    public static UserBackupManagerService.BackupWakeLock createBackupWakeLock(
+            Application application) {
+        PowerManager powerManager =
+                (PowerManager) application.getSystemService(Context.POWER_SERVICE);
+        return new UserBackupManagerService.BackupWakeLock(
+                powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"), 0);
+    }
+
+    /**
+     * Creates a backup thread associated with a looper, starts it and returns its looper for
+     * shadowing and creation of the backup handler.
+     *
+     * <p>Note that Robolectric simulates multi-thread in a single-thread to avoid flakiness, so
+     * even though we started the thread, you should control its execution via the shadow of the
+     * looper returned.
+     *
+     * @return The {@link Looper} for the backup thread.
+     */
+    public static Looper startBackupThreadAndGetLooper() {
+        HandlerThread backupThread = new HandlerThread("backup");
+        backupThread.start();
+        return backupThread.getLooper();
+    }
+
+    /**
+     * Similar to {@link #startBackupThreadAndGetLooper()} but with a custom exception handler and
+     * returning the thread instead of the looper associated with it.
+     *
+     * @param exceptionHandler Uncaught exception handler for backup thread.
+     * @return The backup thread.
+     * @see #startBackupThreadAndGetLooper()
+     */
+    public static HandlerThread startBackupThread(
+            @Nullable UncaughtExceptionHandler exceptionHandler) {
+        HandlerThread backupThread = new HandlerThread("backup");
+        backupThread.setUncaughtExceptionHandler(exceptionHandler);
+        backupThread.start();
+        return backupThread;
+    }
+
+    /**
+     * Similar to {@link #startBackupThread(UncaughtExceptionHandler)} but logging uncaught
+     * exceptions to logcat.
+     *
+     * @param tag Tag used for logging exceptions.
+     * @return The backup thread.
+     * @see #startBackupThread(UncaughtExceptionHandler)
+     */
+    public static HandlerThread startSilentBackupThread(String tag) {
+        return startBackupThread(
+                (thread, e) ->
+                        Log.e(tag, "Uncaught exception in test thread " + thread.getName(), e));
+    }
+
+    private BackupManagerServiceTestUtils() {}
+
+    public static class AccessorMock<T> {
+        public Answer<T> getter;
+        public Answer<T> setter;
+
+        private AccessorMock(Answer<T> getter, Answer<T> setter) {
+            this.getter = getter;
+            this.setter = setter;
+        }
+    }
+}
diff --git a/com/android/server/backup/testing/CryptoTestUtils.java b/com/android/server/backup/testing/CryptoTestUtils.java
new file mode 100644
index 0000000..b0c02ba
--- /dev/null
+++ b/com/android/server/backup/testing/CryptoTestUtils.java
@@ -0,0 +1,177 @@
+/*
+ * 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.testing;
+
+import android.util.Pair;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+
+import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/** Helpers for crypto code tests. */
+public class CryptoTestUtils {
+    private static final String KEY_ALGORITHM = "AES";
+    private static final int KEY_SIZE_BITS = 256;
+
+    private CryptoTestUtils() {}
+
+    public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+        keyGenerator.init(KEY_SIZE_BITS);
+        return keyGenerator.generateKey();
+    }
+
+    /** Generates a byte array of size {@code n} containing random bytes. */
+    public static byte[] generateRandomBytes(int n) {
+        byte[] bytes = new byte[n];
+        Random random = new Random();
+        random.nextBytes(bytes);
+        return bytes;
+    }
+
+    public static ChunksMetadataProto.Chunk newChunk(ChunkHash hash, int length) {
+        return newChunk(hash.getHash(), length);
+    }
+
+    public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
+        ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
+        newChunk.hash = Arrays.copyOf(hash, hash.length);
+        newChunk.length = length;
+        return newChunk;
+    }
+
+    public static ChunksMetadataProto.ChunkListing newChunkListing(
+            String docId,
+            byte[] fingerprintSalt,
+            int cipherType,
+            int orderingType,
+            ChunksMetadataProto.Chunk... chunks) {
+        ChunksMetadataProto.ChunkListing chunkListing =
+                newChunkListingWithoutDocId(fingerprintSalt, cipherType, orderingType, chunks);
+        chunkListing.documentId = docId;
+        return chunkListing;
+    }
+
+    public static ChunksMetadataProto.ChunkListing newChunkListingWithoutDocId(
+            byte[] fingerprintSalt,
+            int cipherType,
+            int orderingType,
+            ChunksMetadataProto.Chunk... chunks) {
+        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
+        chunkListing.fingerprintMixerSalt =
+                fingerprintSalt == null
+                        ? null
+                        : Arrays.copyOf(fingerprintSalt, fingerprintSalt.length);
+        chunkListing.cipherType = cipherType;
+        chunkListing.chunkOrderingType = orderingType;
+        chunkListing.chunks = chunks;
+        return chunkListing;
+    }
+
+    public static ChunksMetadataProto.ChunkOrdering newChunkOrdering(
+            int[] starts, byte[] checksum) {
+        ChunksMetadataProto.ChunkOrdering chunkOrdering = new ChunksMetadataProto.ChunkOrdering();
+        chunkOrdering.starts = starts == null ? null : Arrays.copyOf(starts, starts.length);
+        chunkOrdering.checksum =
+                checksum == null ? checksum : Arrays.copyOf(checksum, checksum.length);
+        return chunkOrdering;
+    }
+
+    public static ChunksMetadataProto.ChunksMetadata newChunksMetadata(
+            int cipherType, int checksumType, int chunkOrderingType, byte[] chunkOrdering) {
+        ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
+        metadata.cipherType = cipherType;
+        metadata.checksumType = checksumType;
+        metadata.chunkOrdering = Arrays.copyOf(chunkOrdering, chunkOrdering.length);
+        metadata.chunkOrderingType = chunkOrderingType;
+        return metadata;
+    }
+
+    public static KeyValuePairProto.KeyValuePair newPair(String key, String value) {
+        return newPair(key, value.getBytes(Charset.forName("UTF-8")));
+    }
+
+    public static KeyValuePairProto.KeyValuePair newPair(String key, byte[] value) {
+        KeyValuePairProto.KeyValuePair newPair = new KeyValuePairProto.KeyValuePair();
+        newPair.key = key;
+        newPair.value = value;
+        return newPair;
+    }
+
+    public static ChunksMetadataProto.ChunkListing clone(
+            ChunksMetadataProto.ChunkListing original) {
+        ChunksMetadataProto.Chunk[] clonedChunks;
+        if (original.chunks == null) {
+            clonedChunks = null;
+        } else {
+            clonedChunks = new ChunksMetadataProto.Chunk[original.chunks.length];
+            for (int i = 0; i < original.chunks.length; i++) {
+                clonedChunks[i] = clone(original.chunks[i]);
+            }
+        }
+
+        return newChunkListing(
+                original.documentId,
+                original.fingerprintMixerSalt,
+                original.cipherType,
+                original.chunkOrderingType,
+                clonedChunks);
+    }
+
+    public static ChunksMetadataProto.Chunk clone(ChunksMetadataProto.Chunk original) {
+        return newChunk(original.hash, original.length);
+    }
+
+    public static ChunksMetadataProto.ChunksMetadata clone(
+            ChunksMetadataProto.ChunksMetadata original) {
+        ChunksMetadataProto.ChunksMetadata cloneMetadata = new ChunksMetadataProto.ChunksMetadata();
+        cloneMetadata.chunkOrderingType = original.chunkOrderingType;
+        cloneMetadata.chunkOrdering =
+                original.chunkOrdering == null
+                        ? null
+                        : Arrays.copyOf(original.chunkOrdering, original.chunkOrdering.length);
+        cloneMetadata.checksumType = original.checksumType;
+        cloneMetadata.cipherType = original.cipherType;
+        return cloneMetadata;
+    }
+
+    public static ChunksMetadataProto.ChunkOrdering clone(
+            ChunksMetadataProto.ChunkOrdering original) {
+        ChunksMetadataProto.ChunkOrdering clone = new ChunksMetadataProto.ChunkOrdering();
+        clone.starts = Arrays.copyOf(original.starts, original.starts.length);
+        clone.checksum = Arrays.copyOf(original.checksum, original.checksum.length);
+        return clone;
+    }
+
+    public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
+        Map<K, V> map = new HashMap<>();
+        for (Pair<K, V> pair : pairs) {
+            map.put(pair.first, pair.second);
+        }
+        return map;
+    }
+}
diff --git a/com/android/server/backup/testing/PackageData.java b/com/android/server/backup/testing/PackageData.java
new file mode 100644
index 0000000..f9177a8
--- /dev/null
+++ b/com/android/server/backup/testing/PackageData.java
@@ -0,0 +1,154 @@
+/*
+ * 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.testing;
+
+import android.annotation.IntDef;
+import android.content.pm.ApplicationInfo;
+import android.os.Process;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+// TODO: Preconditions is not available, include its target in dependencies
+public class PackageData {
+    public static final PackageData PM_PACKAGE = new PmPackageData();
+
+    public static PackageData keyValuePackage(int identifier) {
+        return androidPackage(identifier, BackupStatus.KEY_VALUE_BACKUP);
+    }
+
+    public static PackageData fullBackupPackage(int identifier) {
+        return androidPackage(identifier, BackupStatus.FULL_BACKUP);
+    }
+
+    /** Returns {@link PackageData} for unique package identifier {@code identifier}. */
+    public static PackageData androidPackage(int identifier) {
+        return androidPackage(identifier, BackupStatus.KEY_VALUE_BACKUP);
+    }
+
+    public final String packageName;
+    public final String agentName;
+    @BackupStatus public final int backupStatus;
+    public final boolean available;
+    public final boolean stopped;
+    public final int uid;
+
+    private PackageData(
+            String packageName,
+            String agentName,
+            int backupStatus,
+            boolean stopped,
+            boolean available,
+            int uid) {
+        // checkArgument(!stopped || !available, "stopped => !available")
+
+        this.packageName = packageName;
+        this.agentName = agentName;
+        this.backupStatus = backupStatus;
+        this.stopped = stopped;
+        this.available = available;
+        this.uid = uid;
+    }
+
+    public int flags() {
+        int flags = 0;
+        if (backupStatus != BackupStatus.BACKUP_NOT_ALLOWED) {
+            flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+        }
+        if (backupStatus == BackupStatus.FULL_BACKUP) {
+            flags |= ApplicationInfo.FLAG_FULL_BACKUP_ONLY;
+        }
+        if (stopped) {
+            flags |= ApplicationInfo.FLAG_STOPPED;
+        }
+        return flags;
+    }
+
+    public PackageData backupNotAllowed() {
+        return new PackageData(
+                packageName, agentName, BackupStatus.BACKUP_NOT_ALLOWED, stopped, available, uid);
+    }
+
+    public PackageData stopped() {
+        return new PackageData(packageName, agentName, backupStatus, true, false, uid);
+    }
+
+    public PackageData unavailable() {
+        return new PackageData(packageName, agentName, backupStatus, stopped, false, uid);
+    }
+
+    public boolean isPm() {
+        return this == PM_PACKAGE;
+    }
+
+    private static PackageData androidPackage(int identifier, @BackupStatus int backupStatus) {
+        // checkArgument(identifier >= 0, "identifier can't be < 0");
+
+        String packageName = "com.sample.package" + identifier;
+        return new PackageData(
+                packageName,
+                packageName + ".BackupAgent",
+                backupStatus,
+                false,
+                true,
+                Process.FIRST_APPLICATION_UID + identifier);
+    }
+
+    private static class PmPackageData extends PackageData {
+        private PmPackageData() {
+            super(
+                    "@pm@",
+                    "com.android.server.backup.PackageManagerBackupAgent",
+                    BackupStatus.KEY_VALUE_BACKUP,
+                    false,
+                    true,
+                    Process.SYSTEM_UID);
+        }
+
+        @Override
+        public int flags() {
+            throw new UnsupportedOperationException("PM \"package\" has no flags");
+        }
+
+        @Override
+        public PackageData backupNotAllowed() {
+            throw new UnsupportedOperationException("PM \"package\" has backup allowed");
+        }
+
+        @Override
+        public PackageData stopped() {
+            throw new UnsupportedOperationException("PM \"package\" can't be stopped");
+        }
+
+        @Override
+        public PackageData unavailable() {
+            throw new UnsupportedOperationException("PM \"package\" is always available");
+        }
+    }
+
+    @IntDef({
+        BackupStatus.KEY_VALUE_BACKUP,
+        BackupStatus.FULL_BACKUP,
+        BackupStatus.BACKUP_NOT_ALLOWED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupStatus {
+        int KEY_VALUE_BACKUP = 0;
+        int FULL_BACKUP = 1;
+        int BACKUP_NOT_ALLOWED = 2;
+    }
+}
diff --git a/com/android/server/backup/testing/RandomInputStream.java b/com/android/server/backup/testing/RandomInputStream.java
new file mode 100644
index 0000000..998da0b
--- /dev/null
+++ b/com/android/server/backup/testing/RandomInputStream.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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.testing;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+/** {@link InputStream} that generates random bytes up to a given length. For testing purposes. */
+public class RandomInputStream extends InputStream {
+    private static final int BYTE_MAX_VALUE = 255;
+
+    private final Random mRandom;
+    private final int mSizeBytes;
+    private int mBytesRead;
+
+    /**
+     * A new instance, generating {@code sizeBytes} from {@code random} as a source.
+     *
+     * @param random Source of random bytes.
+     * @param sizeBytes The number of bytes to generate before closing the stream.
+     */
+    public RandomInputStream(Random random, int sizeBytes) {
+        mRandom = random;
+        mSizeBytes = sizeBytes;
+        mBytesRead = 0;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (isFinished()) {
+            return -1;
+        }
+        mBytesRead++;
+        return mRandom.nextInt(BYTE_MAX_VALUE);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        checkArgument(off + len <= b.length);
+        if (isFinished()) {
+            return -1;
+        }
+        int length = Math.min(len, mSizeBytes - mBytesRead);
+        int end = off + length;
+
+        for (int i = off; i < end; ) {
+            for (int rnd = mRandom.nextInt(), n = Math.min(end - i, Integer.SIZE / Byte.SIZE);
+                    n-- > 0;
+                    rnd >>= Byte.SIZE) {
+                b[i++] = (byte) rnd;
+            }
+        }
+
+        mBytesRead += length;
+        return length;
+    }
+
+    private boolean isFinished() {
+        return mBytesRead >= mSizeBytes;
+    }
+}
diff --git a/com/android/server/backup/testing/TestUtils.java b/com/android/server/backup/testing/TestUtils.java
new file mode 100644
index 0000000..3fe1f3f
--- /dev/null
+++ b/com/android/server/backup/testing/TestUtils.java
@@ -0,0 +1,200 @@
+/*
+ * 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.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import static java.util.stream.Collectors.toSet;
+
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+
+import com.android.server.testing.shadows.ShadowEventLog;
+
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+public class TestUtils {
+    private static final long TIMEOUT_MS = 3000;
+    private static final long STEP_MS = 50;
+
+    /**
+     * Counts the number of messages in the looper {@code looper} that satisfy {@code
+     * messageFilter}.
+     */
+    public static int messagesInLooper(Looper looper, Predicate<Message> messageFilter) {
+        MessageQueue queue = looper.getQueue();
+        int i = 0;
+        for (Message m = shadowOf(queue).getHead(); m != null; m = shadowOf(m).getNext()) {
+            if (messageFilter.test(m)) {
+                i += 1;
+            }
+        }
+        return i;
+    }
+
+    public static void waitUntil(Supplier<Boolean> condition)
+            throws InterruptedException, TimeoutException {
+        waitUntil(condition, STEP_MS, TIMEOUT_MS);
+    }
+
+    public static void waitUntil(Supplier<Boolean> condition, long stepMs, long timeoutMs)
+            throws InterruptedException, TimeoutException {
+        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+        while (true) {
+            if (condition.get()) {
+                return;
+            }
+            if (System.nanoTime() > deadline) {
+                throw new TimeoutException("Test timed-out waiting for condition");
+            }
+            Thread.sleep(stepMs);
+        }
+    }
+
+    /** Version of {@link ShadowLooper#runToEndOfTasks()} that also advances the system clock. */
+    public static void runToEndOfTasks(Looper looper) {
+        ShadowLooper shadowLooper = shadowOf(looper);
+        shadowLooper.runToEndOfTasks();
+        // Handler instances have their own clock, so advancing looper (with runToEndOfTasks())
+        // above does NOT advance the handlers' clock, hence whenever a handler post messages with
+        // specific time to the looper the time of those messages will be before the looper's time.
+        // To fix this we advance SystemClock as well since that is from where the handlers read
+        // time.
+        SystemClock.setCurrentTimeMillis(shadowLooper.getScheduler().getCurrentTime());
+    }
+
+    /**
+     * Reset logcat with {@link ShadowLog#reset()} before the test case if you do anything that uses
+     * logcat before that.
+     */
+    public static void assertLogcatAtMost(String tag, int level) {
+        assertThat(ShadowLog.getLogsForTag(tag).stream().allMatch(logItem -> logItem.type <= level))
+                .named("All logs <= " + level)
+                .isTrue();
+    }
+
+    /**
+     * Reset logcat with {@link ShadowLog#reset()} before the test case if you do anything that uses
+     * logcat before that.
+     */
+    public static void assertLogcatAtLeast(String tag, int level) {
+        assertThat(ShadowLog.getLogsForTag(tag).stream().anyMatch(logItem -> logItem.type >= level))
+                .named("Any log >= " + level)
+                .isTrue();
+    }
+
+    /**
+     * Verifies that logcat has produced log items as specified per level in {@code logs} (with
+     * repetition).
+     *
+     * <p>So, if you call {@code assertLogcat(TAG, Log.ERROR, Log.ERROR)}, you assert that there are
+     * exactly 2 log items, each with level ERROR.
+     *
+     * <p>Reset logcat with {@link ShadowLog#reset()} before the test case if you do anything
+     * that uses logcat before that.
+     */
+    public static void assertLogcat(String tag, int... logs) {
+        assertThat(
+                        ShadowLog.getLogsForTag(tag).stream()
+                                .map(logItem -> logItem.type)
+                                .collect(toSet()))
+                .named("Log items (specified per level)")
+                .containsExactly(IntStream.of(logs).boxed().toArray());
+    }
+
+    public static void assertLogcatContains(String tag, Predicate<ShadowLog.LogItem> predicate) {
+        assertThat(ShadowLog.getLogsForTag(tag).stream().anyMatch(predicate)).isTrue();
+    }
+
+    /** Declare shadow {@link ShadowEventLog} to use this. */
+    public static void assertEventLogged(int tag, Object... values) {
+        assertThat(ShadowEventLog.getEntries())
+                .named("Event logs")
+                .contains(new ShadowEventLog.Entry(tag, Arrays.asList(values)));
+    }
+
+    /** Declare shadow {@link ShadowEventLog} to use this. */
+    public static void assertEventNotLogged(int tag, Object... values) {
+        assertThat(ShadowEventLog.getEntries())
+                .named("Event logs")
+                .doesNotContain(new ShadowEventLog.Entry(tag, Arrays.asList(values)));
+    }
+
+    /**
+     * Calls {@link Runnable#run()} and returns if no exception is thrown. Otherwise, if the
+     * exception is unchecked, rethrow it; if it's checked wrap in a {@link RuntimeException} and
+     * throw.
+     *
+     * <p><b>Warning:</b>DON'T use this outside tests. A wrapped checked exception is just a failure
+     * in a test.
+     */
+    public static void uncheck(ThrowingRunnable runnable) {
+        try {
+            runnable.runOrThrow();
+        } catch (Exception e) {
+            throw wrapIfChecked(e);
+        }
+    }
+
+    /**
+     * Calls {@link Callable#call()} and returns the value if no exception is thrown. Otherwise, if
+     * the exception is unchecked, rethrow it; if it's checked wrap in a {@link RuntimeException}
+     * and throw.
+     *
+     * <p><b>Warning:</b>DON'T use this outside tests. A wrapped checked exception is just a failure
+     * in a test.
+     */
+    public static <T> T uncheck(Callable<T> callable) {
+        try {
+            return callable.call();
+        } catch (Exception e) {
+            throw wrapIfChecked(e);
+        }
+    }
+
+    /**
+     * Wrap {@code e} in a {@link RuntimeException} only if it's not one already, in which case it's
+     * returned.
+     */
+    public static RuntimeException wrapIfChecked(Exception e) {
+        if (e instanceof RuntimeException) {
+            return (RuntimeException) e;
+        }
+        return new RuntimeException(e);
+    }
+
+    /** An equivalent of {@link Runnable} that allows throwing checked exceptions. */
+    @FunctionalInterface
+    public interface ThrowingRunnable {
+        void runOrThrow() throws Exception;
+    }
+
+    private TestUtils() {}
+}
diff --git a/com/android/server/backup/testing/TransportData.java b/com/android/server/backup/testing/TransportData.java
new file mode 100644
index 0000000..3c29810
--- /dev/null
+++ b/com/android/server/backup/testing/TransportData.java
@@ -0,0 +1,149 @@
+/*
+ * 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.testing;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+
+import com.android.server.backup.testing.TransportTestUtils.TransportStatus;
+
+public class TransportData {
+    // No constants since new Intent() can't be called in static context because of Robolectric
+    public static TransportData backupTransport() {
+        return new TransportData(
+                "com.google.android.gms/.backup.BackupTransportService",
+                "com.google.android.gms/.backup.BackupTransportService",
+                "com.google.android.gms.backup.BackupTransportService",
+                new Intent(),
+                "user@gmail.com",
+                new Intent(),
+                "Google Account");
+    }
+
+    public static TransportData d2dTransport() {
+        return new TransportData(
+                "com.google.android.gms/.backup.migrate.service.D2dTransport",
+                "com.google.android.gms/.backup.component.D2dTransportService",
+                "d2dMigrateTransport",
+                null,
+                "Moving data to new device",
+                null,
+                "");
+    }
+
+    public static TransportData localTransport() {
+        return new TransportData(
+                "com.android.localtransport/.LocalTransport",
+                "com.android.localtransport/.LocalTransportService",
+                "com.android.localtransport.LocalTransport",
+                null,
+                "Backing up to debug-only private cache",
+                null,
+                "");
+    }
+
+    public static TransportData genericTransport(String packageName, String className) {
+        return new TransportData(
+                packageName + "/." + className,
+                packageName + "/." + className + "Service",
+                packageName + "." + className,
+                new Intent(),
+                "currentDestinationString",
+                new Intent(),
+                "dataManagementLabel");
+    }
+
+    @TransportStatus public int transportStatus;
+    public final String transportName;
+    private final String transportComponentShort;
+    @Nullable public String transportDirName;
+    @Nullable public Intent configurationIntent;
+    @Nullable public String currentDestinationString;
+    @Nullable public Intent dataManagementIntent;
+    @Nullable public CharSequence dataManagementLabel;
+
+    private TransportData(
+            @TransportStatus int transportStatus,
+            String transportName,
+            String transportComponentShort,
+            String transportDirName,
+            Intent configurationIntent,
+            String currentDestinationString,
+            Intent dataManagementIntent,
+            CharSequence dataManagementLabel) {
+        this.transportStatus = transportStatus;
+        this.transportName = transportName;
+        this.transportComponentShort = transportComponentShort;
+        this.transportDirName = transportDirName;
+        this.configurationIntent = configurationIntent;
+        this.currentDestinationString = currentDestinationString;
+        this.dataManagementIntent = dataManagementIntent;
+        this.dataManagementLabel = dataManagementLabel;
+    }
+
+    public TransportData(
+            String transportName,
+            String transportComponentShort,
+            String transportDirName,
+            Intent configurationIntent,
+            String currentDestinationString,
+            Intent dataManagementIntent,
+            CharSequence dataManagementLabel) {
+        this(
+                TransportStatus.REGISTERED_AVAILABLE,
+                transportName,
+                transportComponentShort,
+                transportDirName,
+                configurationIntent,
+                currentDestinationString,
+                dataManagementIntent,
+                dataManagementLabel);
+    }
+
+    /**
+     * Not field because otherwise we'd have to call ComponentName::new in static context and
+     * Robolectric does not like this.
+     */
+    public ComponentName getTransportComponent() {
+        return ComponentName.unflattenFromString(transportComponentShort);
+    }
+
+    public TransportData unavailable() {
+        return new TransportData(
+                TransportStatus.REGISTERED_UNAVAILABLE,
+                transportName,
+                transportComponentShort,
+                transportDirName,
+                configurationIntent,
+                currentDestinationString,
+                dataManagementIntent,
+                dataManagementLabel);
+    }
+
+    public TransportData unregistered() {
+        return new TransportData(
+                TransportStatus.UNREGISTERED,
+                transportName,
+                transportComponentShort,
+                transportDirName,
+                configurationIntent,
+                currentDestinationString,
+                dataManagementIntent,
+                dataManagementLabel);
+    }
+}
diff --git a/com/android/server/backup/testing/TransportTestUtils.java b/com/android/server/backup/testing/TransportTestUtils.java
new file mode 100644
index 0000000..7dd5be5
--- /dev/null
+++ b/com/android/server/backup/testing/TransportTestUtils.java
@@ -0,0 +1,227 @@
+/*
+ * 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.testing;
+
+import static com.android.server.backup.testing.TestUtils.uncheck;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import static java.util.stream.Collectors.toList;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.stream.Stream;
+
+public class TransportTestUtils {
+    /**
+     * Differently from {@link #setUpTransports(TransportManager, TransportData...)}, which
+     * configures {@link TransportManager}, this is meant to mock the environment for a real
+     * TransportManager.
+     */
+    public static void setUpTransportsForTransportManager(
+            ShadowPackageManager shadowPackageManager, TransportData... transports)
+            throws Exception {
+        for (TransportData transport : transports) {
+            if (transport.transportStatus == TransportStatus.UNREGISTERED) {
+                continue;
+            }
+            ComponentName transportComponent = transport.getTransportComponent();
+            String packageName = transportComponent.getPackageName();
+            ResolveInfo resolveInfo = resolveInfo(transportComponent);
+            shadowPackageManager.addResolveInfoForIntent(transportIntent(), resolveInfo);
+            shadowPackageManager.addResolveInfoForIntent(
+                    transportIntent().setPackage(packageName), resolveInfo);
+        }
+    }
+
+    private static Intent transportIntent() {
+        return new Intent(TransportManager.SERVICE_ACTION_TRANSPORT_HOST);
+    }
+
+    private static ResolveInfo resolveInfo(ComponentName transportComponent) {
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.serviceInfo = new ServiceInfo();
+        resolveInfo.serviceInfo.packageName = transportComponent.getPackageName();
+        resolveInfo.serviceInfo.name = transportComponent.getClassName();
+        return resolveInfo;
+    }
+
+    /** {@code transportName} has to be in the {@link ComponentName} format (with '/') */
+    public static TransportMock setUpCurrentTransport(
+            TransportManager transportManager, TransportData transport) throws Exception {
+        TransportMock transportMock = setUpTransport(transportManager, transport);
+        int status = transport.transportStatus;
+        when(transportManager.getCurrentTransportName()).thenReturn(transport.transportName);
+        if (status == TransportStatus.REGISTERED_AVAILABLE
+                || status == TransportStatus.REGISTERED_UNAVAILABLE) {
+            // Transport registered
+            when(transportManager.getCurrentTransportClient(any()))
+                    .thenReturn(transportMock.transportClient);
+            when(transportManager.getCurrentTransportClientOrThrow(any()))
+                    .thenReturn(transportMock.transportClient);
+        } else {
+            // Transport not registered
+            when(transportManager.getCurrentTransportClient(any())).thenReturn(null);
+            when(transportManager.getCurrentTransportClientOrThrow(any()))
+                    .thenThrow(TransportNotRegisteredException.class);
+        }
+        return transportMock;
+    }
+
+    /** @see #setUpTransport(TransportManager, TransportData) */
+    public static List<TransportMock> setUpTransports(
+            TransportManager transportManager, TransportData... transports) throws Exception {
+        return Stream.of(transports)
+                .map(transport -> uncheck(() -> setUpTransport(transportManager, transport)))
+                .collect(toList());
+    }
+
+    public static TransportMock setUpTransport(
+            TransportManager transportManager, TransportData transport) throws Exception {
+        int status = transport.transportStatus;
+        String transportName = transport.transportName;
+        ComponentName transportComponent = transport.getTransportComponent();
+        String transportDirName = transport.transportDirName;
+
+        TransportMock transportMock = mockTransport(transport);
+        if (status == TransportStatus.REGISTERED_AVAILABLE
+                || status == TransportStatus.REGISTERED_UNAVAILABLE) {
+            // Transport registered
+            when(transportManager.getTransportClient(eq(transportName), any()))
+                    .thenReturn(transportMock.transportClient);
+            when(transportManager.getTransportClientOrThrow(eq(transportName), any()))
+                    .thenReturn(transportMock.transportClient);
+            when(transportManager.getTransportName(transportComponent)).thenReturn(transportName);
+            when(transportManager.getTransportDirName(eq(transportName)))
+                    .thenReturn(transportDirName);
+            when(transportManager.getTransportDirName(eq(transportComponent)))
+                    .thenReturn(transportDirName);
+            when(transportManager.isTransportRegistered(eq(transportName))).thenReturn(true);
+            // TODO: Mock rest of description methods
+        } else {
+            // Transport not registered
+            when(transportManager.getTransportClient(eq(transportName), any())).thenReturn(null);
+            when(transportManager.getTransportClientOrThrow(eq(transportName), any()))
+                    .thenThrow(TransportNotRegisteredException.class);
+            when(transportManager.getTransportName(transportComponent))
+                    .thenThrow(TransportNotRegisteredException.class);
+            when(transportManager.getTransportDirName(eq(transportName)))
+                    .thenThrow(TransportNotRegisteredException.class);
+            when(transportManager.getTransportDirName(eq(transportComponent)))
+                    .thenThrow(TransportNotRegisteredException.class);
+            when(transportManager.isTransportRegistered(eq(transportName))).thenReturn(false);
+        }
+        return transportMock;
+    }
+
+    public static TransportMock mockTransport(TransportData transport) throws Exception {
+        final TransportClient transportClientMock;
+        int status = transport.transportStatus;
+        ComponentName transportComponent = transport.getTransportComponent();
+        if (status == TransportStatus.REGISTERED_AVAILABLE
+                || status == TransportStatus.REGISTERED_UNAVAILABLE) {
+            // Transport registered
+            transportClientMock = mock(TransportClient.class);
+            when(transportClientMock.getTransportComponent()).thenReturn(transportComponent);
+            if (status == TransportStatus.REGISTERED_AVAILABLE) {
+                // Transport registered and available
+                IBackupTransport transportMock = mockTransportBinder(transport);
+                when(transportClientMock.connectOrThrow(any())).thenReturn(transportMock);
+                when(transportClientMock.connect(any())).thenReturn(transportMock);
+
+                return new TransportMock(transport, transportClientMock, transportMock);
+            } else {
+                // Transport registered but unavailable
+                when(transportClientMock.connectOrThrow(any()))
+                        .thenThrow(TransportNotAvailableException.class);
+                when(transportClientMock.connect(any())).thenReturn(null);
+
+                return new TransportMock(transport, transportClientMock, null);
+            }
+        } else {
+            // Transport not registered
+            return new TransportMock(transport, null, null);
+        }
+    }
+
+    private static IBackupTransport mockTransportBinder(TransportData transport) throws Exception {
+        IBackupTransport transportBinder = mock(IBackupTransport.class);
+        try {
+            when(transportBinder.name()).thenReturn(transport.transportName);
+            when(transportBinder.transportDirName()).thenReturn(transport.transportDirName);
+            when(transportBinder.configurationIntent()).thenReturn(transport.configurationIntent);
+            when(transportBinder.currentDestinationString())
+                    .thenReturn(transport.currentDestinationString);
+            when(transportBinder.dataManagementIntent()).thenReturn(transport.dataManagementIntent);
+            when(transportBinder.dataManagementIntentLabel())
+                    .thenReturn(transport.dataManagementLabel);
+        } catch (RemoteException e) {
+            fail("RemoteException?");
+        }
+        return transportBinder;
+    }
+
+    public static class TransportMock {
+        public final TransportData transportData;
+        @Nullable public final TransportClient transportClient;
+        @Nullable public final IBackupTransport transport;
+
+        private TransportMock(
+                TransportData transportData,
+                @Nullable TransportClient transportClient,
+                @Nullable IBackupTransport transport) {
+            this.transportData = transportData;
+            this.transportClient = transportClient;
+            this.transport = transport;
+        }
+    }
+
+    @IntDef({
+        TransportStatus.REGISTERED_AVAILABLE,
+        TransportStatus.REGISTERED_UNAVAILABLE,
+        TransportStatus.UNREGISTERED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TransportStatus {
+        int REGISTERED_AVAILABLE = 0;
+        int REGISTERED_UNAVAILABLE = 1;
+        int UNREGISTERED = 2;
+    }
+
+    private TransportTestUtils() {}
+}
diff --git a/com/android/server/backup/testing/Utils.java b/com/android/server/backup/testing/Utils.java
new file mode 100644
index 0000000..b0e00a2
--- /dev/null
+++ b/com/android/server/backup/testing/Utils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.testing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+public class Utils {
+    public static final int BUFFER_SIZE = 8192;
+
+    public static void transferStreamedData(InputStream in, OutputStream out) throws IOException {
+        transferStreamedData(in, out, BUFFER_SIZE);
+    }
+
+    public static void transferStreamedData(InputStream in, OutputStream out, int bufferSize)
+            throws IOException {
+        byte[] buffer = new byte[bufferSize];
+        int read;
+        while ((read = in.read(buffer)) != -1) {
+            out.write(buffer, 0, read);
+        }
+    }
+
+    public static <T> Iterable<T> oneTimeIterable(Iterator<T> iterator) {
+        return () -> iterator;
+    }
+
+    public static boolean isFileNonEmpty(Path path) throws IOException {
+        return Files.exists(path) && Files.size(path) > 0;
+    }
+
+    private Utils() {}
+}
diff --git a/com/android/server/backup/transport/DelegatingTransport.java b/com/android/server/backup/transport/DelegatingTransport.java
new file mode 100644
index 0000000..ab87080
--- /dev/null
+++ b/com/android/server/backup/transport/DelegatingTransport.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2019 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.transport;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+
+import com.android.internal.backup.IBackupTransport;
+
+/**
+ * Delegates all transport methods to the delegate() implemented in the derived class.
+ */
+public abstract class DelegatingTransport extends IBackupTransport.Stub {
+    protected abstract IBackupTransport getDelegate() throws RemoteException;
+
+    /**
+     * Ask the transport for the name under which it should be registered.  This will
+     * typically be its host service's component name, but need not be.
+     */
+    @Override
+    public String name() throws RemoteException {
+        return getDelegate().name();
+    }
+
+    /**
+     * Ask the transport for an Intent that can be used to launch any internal
+     * configuration Activity that it wishes to present.  For example, the transport
+     * may offer a UI for allowing the user to supply login credentials for the
+     * transport's off-device backend.
+     *
+     * If the transport does not supply any user-facing configuration UI, it should
+     * return null from this method.
+     *
+     * @return An Intent that can be passed to Context.startActivity() in order to
+     * launch the transport's configuration UI.  This method will return null
+     * if the transport does not offer any user-facing configuration UI.
+     */
+    @Override
+    public Intent configurationIntent() throws RemoteException {
+        return getDelegate().configurationIntent();
+    }
+
+    /**
+     * On demand, supply a one-line string that can be shown to the user that
+     * describes the current backend destination.  For example, a transport that
+     * can potentially associate backup data with arbitrary user accounts should
+     * include the name of the currently-active account here.
+     *
+     * @return A string describing the destination to which the transport is currently
+     * sending data.  This method should not return null.
+     */
+    @Override
+    public String currentDestinationString() throws RemoteException {
+        return getDelegate().currentDestinationString();
+    }
+
+    /**
+     * Ask the transport for an Intent that can be used to launch a more detailed
+     * secondary data management activity.  For example, the configuration intent might
+     * be one for allowing the user to select which account they wish to associate
+     * their backups with, and the management intent might be one which presents a
+     * UI for managing the data on the backend.
+     *
+     * <p>In the Settings UI, the configuration intent will typically be invoked
+     * when the user taps on the preferences item labeled with the current
+     * destination string, and the management intent will be placed in an overflow
+     * menu labelled with the management label string.
+     *
+     * <p>If the transport does not supply any user-facing data management
+     * UI, then it should return {@code null} from this method.
+     *
+     * @return An intent that can be passed to Context.startActivity() in order to
+     * launch the transport's data-management UI.  This method will return
+     * {@code null} if the transport does not offer any user-facing data
+     * management UI.
+     */
+    @Override
+    public Intent dataManagementIntent() throws RemoteException {
+        return getDelegate().dataManagementIntent();
+    }
+
+    /**
+     * On demand, supply a short {@link CharSequence} that can be shown to the user as the
+     * label on
+     * an overflow menu item used to invoke the data management UI.
+     *
+     * @return A {@link CharSequence} to be used as the label for the transport's data management
+     *         affordance.  If the transport supplies a data management intent, this
+     *         method must not return {@code null}.
+     */
+    @Override
+    public CharSequence dataManagementIntentLabel() throws RemoteException {
+        return getDelegate().dataManagementIntentLabel();
+    }
+
+    /**
+     * Ask the transport where, on local device storage, to keep backup state blobs.
+     * This is per-transport so that mock transports used for testing can coexist with
+     * "live" backup services without interfering with the live bookkeeping.  The
+     * returned string should be a name that is expected to be unambiguous among all
+     * available backup transports; the name of the class implementing the transport
+     * is a good choice.  This MUST be constant.
+     *
+     * @return A unique name, suitable for use as a file or directory name, that the
+     *         Backup Manager could use to disambiguate state files associated with
+     *         different backup transports.
+     */
+    @Override
+    public String transportDirName() throws RemoteException {
+        return getDelegate().transportDirName();
+    }
+
+    /**
+     * Verify that this is a suitable time for a backup pass.  This should return zero
+     * if a backup is reasonable right now, some positive value otherwise.  This method
+     * will be called outside of the {@link #startSession}/{@link #endSession} pair.
+     *
+     * <p>If this is not a suitable time for a backup, the transport should return a
+     * backoff delay, in milliseconds, after which the Backup Manager should try again.
+     *
+     * @return Zero if this is a suitable time for a backup pass, or a positive time delay
+     *   in milliseconds to suggest deferring the backup pass for a while.
+     */
+    @Override
+    public long requestBackupTime() throws RemoteException {
+        return getDelegate().requestBackupTime();
+    }
+
+    /**
+     * Initialize the server side storage for this device, erasing all stored data.
+     * The transport may send the request immediately, or may buffer it.  After
+     * this is called, {@link #finishBackup} must be called to ensure the request
+     * is sent and received successfully.
+     *
+     * @return One of {@link BackupConstants#TRANSPORT_OK} (OK so far) or
+     *   {@link BackupConstants#TRANSPORT_ERROR} (on network error or other failure).
+     */
+    @Override
+    public int initializeDevice() throws RemoteException {
+        return getDelegate().initializeDevice();
+    }
+
+    /**
+     * Send one application's data to the backup destination.  The transport may send
+     * the data immediately, or may buffer it.  After this is called, {@link #finishBackup}
+     * must be called to ensure the data is sent and recorded successfully.
+     *
+     * @param packageInfo The identity of the application whose data is being backed up.
+     *   This specifically includes the signature list for the package.
+     * @param inFd Descriptor of file with data that resulted from invoking the application's
+     *   BackupService.doBackup() method.  This may be a pipe rather than a file on
+     *   persistent media, so it may not be seekable.
+     * @param flags Some of {@link BackupTransport#FLAG_USER_INITIATED}.
+     * @return one of {@link BackupConstants#TRANSPORT_OK} (OK so far),
+     *  {@link BackupConstants#TRANSPORT_ERROR} (on network error or other failure), or
+     *  {@link BackupConstants#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has
+     *  become lost due to inactive expiry or some other reason and needs re-initializing)
+     */
+    @Override
+    public int performBackup(PackageInfo packageInfo,
+            ParcelFileDescriptor inFd, int flags) throws RemoteException {
+        return getDelegate().performBackup(packageInfo, inFd, flags);
+    }
+
+    /**
+     * Erase the give application's data from the backup destination.  This clears
+     * out the given package's data from the current backup set, making it as though
+     * the app had never yet been backed up.  After this is called, {@link finishBackup}
+     * must be called to ensure that the operation is recorded successfully.
+     *
+     * @return the same error codes as {@link #performBackup}.
+     * @param packageInfo
+     */
+    @Override
+    public int clearBackupData(PackageInfo packageInfo) throws RemoteException {
+        return getDelegate().clearBackupData(packageInfo);
+    }
+
+    /**
+     * Finish sending application data to the backup destination.  This must be
+     * called after {@link #performBackup} or {@link clearBackupData} to ensure that
+     * all data is sent.  Only when this method returns true can a backup be assumed
+     * to have succeeded.
+     *
+     * @return the same error codes as {@link #performBackup}.
+     */
+    @Override
+    public int finishBackup() throws RemoteException {
+        return getDelegate().finishBackup();
+    }
+
+    /**
+     * Get the set of all backups currently available over this transport.
+     *
+     * @return Descriptions of the set of restore images available for this device,
+     *   or null if an error occurred (the attempt should be rescheduled).
+     **/
+    @Override
+    public RestoreSet[] getAvailableRestoreSets() throws RemoteException {
+        return getDelegate().getAvailableRestoreSets();
+    }
+
+    /**
+     * Get the identifying token of the backup set currently being stored from
+     * this device.  This is used in the case of applications wishing to restore
+     * their last-known-good data.
+     *
+     * @return A token that can be passed to {@link #startRestore}, or 0 if there
+     *   is no backup set available corresponding to the current device state.
+     */
+    @Override
+    public long getCurrentRestoreSet() throws RemoteException {
+        return getDelegate().getCurrentRestoreSet();
+    }
+
+    /**
+     * Start restoring application data from backup.  After calling this function,
+     * alternate calls to {@link #nextRestorePackage} and {@link #nextRestoreData}
+     * to walk through the actual application data.
+     *
+     * @param token A backup token as returned by {@link #getAvailableRestoreSets}
+     *   or {@link #getCurrentRestoreSet}.
+     * @param packages List of applications to restore (if data is available).
+     *   Application data will be restored in the order given.
+     * @return One of {@link BackupConstants#TRANSPORT_OK} (OK so far, call
+     *   {@link #nextRestorePackage}) or {@link BackupConstants#TRANSPORT_ERROR}
+     *   (an error occurred, the restore should be aborted and rescheduled).
+     */
+    @Override
+    public int startRestore(long token, PackageInfo[] packages) throws RemoteException {
+        return getDelegate().startRestore(token, packages);
+    }
+
+    /**
+     * Get the package name of the next application with data in the backup store, plus
+     * a description of the structure of the restored archive: either TYPE_KEY_VALUE for
+     * an original-API key/value dataset, or TYPE_FULL_STREAM for a tarball-type archive stream.
+     *
+     * <p>If the package name in the returned RestoreDescription object is the singleton
+     * {@link RestoreDescription#NO_MORE_PACKAGES}, it indicates that no further data is available
+     * in the current restore session: all packages described in startRestore() have been
+     * processed.
+     *
+     * <p>If this method returns {@code null}, it means that a transport-level error has
+     * occurred and the entire restore operation should be abandoned.
+     *
+     * @return A RestoreDescription object containing the name of one of the packages
+     *   supplied to {@link #startRestore} plus an indicator of the data type of that
+     *   restore data; or {@link RestoreDescription#NO_MORE_PACKAGES} to indicate that
+     *   no more packages can be restored in this session; or {@code null} to indicate
+     *   a transport-level error.
+     */
+    @Override
+    public RestoreDescription nextRestorePackage() throws RemoteException {
+        return getDelegate().nextRestorePackage();
+    }
+
+    /**
+     * Get the data for the application returned by {@link #nextRestorePackage}.
+     *
+     * @param outFd An open, writable file into which the backup data should be stored.
+     * @return the same error codes as {@link #startRestore}.
+     */
+    @Override
+    public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException {
+        return getDelegate().getRestoreData(outFd);
+    }
+
+    /**
+     * End a restore session (aborting any in-process data transfer as necessary),
+     * freeing any resources and connections used during the restore process.
+     */
+    @Override
+    public void finishRestore() throws RemoteException {
+        getDelegate().finishRestore();
+    }
+
+    @Override
+    public long requestFullBackupTime() throws RemoteException {
+        return getDelegate().requestFullBackupTime();
+    }
+
+    @Override
+    public int performFullBackup(PackageInfo targetPackage,
+            ParcelFileDescriptor socket, int flags) throws RemoteException {
+        return getDelegate().performFullBackup(targetPackage, socket, flags);
+    }
+
+    @Override
+    public int checkFullBackupSize(long size) throws RemoteException {
+        return getDelegate().checkFullBackupSize(size);
+    }
+
+    @Override
+    public int sendBackupData(int numBytes) throws RemoteException {
+        return getDelegate().sendBackupData(numBytes);
+    }
+
+    @Override
+    public void cancelFullBackup() throws RemoteException {
+        getDelegate().cancelFullBackup();
+    }
+
+    /**
+     * Ask the transport whether this app is eligible for backup.
+     *
+     * @param targetPackage The identity of the application.
+     * @param isFullBackup If set, transport should check if app is eligible for full data backup,
+     *   otherwise to check if eligible for key-value backup.
+     * @return Whether this app is eligible for backup.
+     */
+    @Override
+    public boolean isAppEligibleForBackup(PackageInfo targetPackage,
+            boolean isFullBackup) throws RemoteException {
+        return getDelegate().isAppEligibleForBackup(targetPackage, isFullBackup);
+    }
+
+    /**
+     * Ask the transport about current quota for backup size of the package.
+     *
+     * @param packageName  ID of package to provide the quota.
+     * @param isFullBackup If set, transport should return limit for full data backup, otherwise
+     *                     for key-value backup.
+     * @return Current limit on full data backup size in bytes.
+     */
+    @Override
+    public long getBackupQuota(String packageName, boolean isFullBackup) throws RemoteException {
+        return getDelegate().getBackupQuota(packageName, isFullBackup);
+    }
+
+    /**
+     * Ask the transport to provide data for the "current" package being restored.  This
+     * is the package that was just reported by {@link #nextRestorePackage()} as having
+     * {@link RestoreDescription#TYPE_FULL_STREAM} data.
+     *
+     * The transport writes some data to the socket supplied to this call, and returns
+     * the number of bytes written.  The system will then read that many bytes and
+     * stream them to the application's agent for restore, then will call this method again
+     * to receive the next chunk of the archive.  This sequence will be repeated until the
+     * transport returns zero indicating that all of the package's data has been delivered
+     * (or returns a negative value indicating some sort of hard error condition at the
+     * transport level).
+     *
+     * <p>After this method returns zero, the system will then call
+     * {@link #getNextFullRestorePackage()} to begin the restore process for the next
+     * application, and the sequence begins again.
+     *
+     * <p>The transport should always close this socket when returning from this method.
+     * Do not cache this socket across multiple calls or you may leak file descriptors.
+     *
+     * @param socket The file descriptor that the transport will use for delivering the
+     *    streamed archive.  The transport must close this socket in all cases when returning
+     *    from this method.
+     * @return 0 when no more data for the current package is available.  A positive value
+     *    indicates the presence of that many bytes to be delivered to the app.  Any negative
+     *    return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR},
+     *    indicating a fatal error condition that precludes further restore operations
+     *    on the current dataset.
+     */
+    @Override
+    public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) throws RemoteException {
+        return getDelegate().getNextFullRestoreDataChunk(socket);
+    }
+
+    /**
+     * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM}
+     * data for restore, it will invoke this method to tell the transport that it should
+     * abandon the data download for the current package.  The OS will then either call
+     * {@link #nextRestorePackage()} again to move on to restoring the next package in the
+     * set being iterated over, or will call {@link #finishRestore()} to shut down the restore
+     * operation.
+     *
+     * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the
+     *    current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious
+     *    transport-level failure.  If the transport reports an error here, the entire restore
+     *    operation will immediately be finished with no further attempts to restore app data.
+     */
+    @Override
+    public int abortFullRestore() throws RemoteException {
+        return getDelegate().abortFullRestore();
+    }
+
+    /**
+     * Returns flags with additional information about the transport, which is accessible to the
+     * {@link BackupAgent}. This allows the agent to decide what to backup or
+     * restore based on properties of the transport.
+     *
+     * <p>For supported flags see {@link BackupAgent}.
+     */
+    @Override
+    public int getTransportFlags() throws RemoteException {
+        return getDelegate().getTransportFlags();
+    }
+}
diff --git a/com/android/server/backup/transport/OnTransportRegisteredListener.java b/com/android/server/backup/transport/OnTransportRegisteredListener.java
new file mode 100644
index 0000000..391ec2d
--- /dev/null
+++ b/com/android/server/backup/transport/OnTransportRegisteredListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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.transport;
+
+import com.android.server.backup.TransportManager;
+
+/**
+ * Listener called when a transport is registered with the {@link TransportManager}. Can be set
+ * using {@link TransportManager#setOnTransportRegisteredListener(OnTransportRegisteredListener)}.
+ */
+@FunctionalInterface
+public interface OnTransportRegisteredListener {
+    /**
+     * Called when a transport is successfully registered.
+     * @param transportName The name of the transport.
+     * @param transportDirName The dir name of the transport.
+     */
+    public void onTransportRegistered(String transportName, String transportDirName);
+}
diff --git a/com/android/server/backup/transport/TransportClient.java b/com/android/server/backup/transport/TransportClient.java
new file mode 100644
index 0000000..ca89f7f
--- /dev/null
+++ b/com/android/server/backup/transport/TransportClient.java
@@ -0,0 +1,702 @@
+/*
+ * 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.transport;
+
+import static com.android.server.backup.transport.TransportUtils.formatMessage;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.EventLog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.util.Preconditions;
+import com.android.server.EventLogTags;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportUtils.Priority;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A {@link TransportClient} manages the connection to an {@link IBackupTransport} service, obtained
+ * via the {@param bindIntent} parameter provided in the constructor. A {@link TransportClient} is
+ * responsible for only one connection to the transport service, not more.
+ *
+ * <p>After retrieved using {@link TransportManager#getTransportClient(String, String)}, you can
+ * call either {@link #connect(String)}, if you can block your thread, or {@link
+ * #connectAsync(TransportConnectionListener, String)}, otherwise, to obtain a {@link
+ * IBackupTransport} instance. It's meant to be passed around as a token to a connected transport.
+ * When the connection is not needed anymore you should call {@link #unbind(String)} or indirectly
+ * via {@link TransportManager#disposeOfTransportClient(TransportClient, String)}.
+ *
+ * <p>DO NOT forget to unbind otherwise there will be dangling connections floating around.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @see TransportManager
+ */
+public class TransportClient {
+    @VisibleForTesting static final String TAG = "TransportClient";
+    private static final int LOG_BUFFER_SIZE = 5;
+
+    private final @UserIdInt int mUserId;
+    private final Context mContext;
+    private final TransportStats mTransportStats;
+    private final Intent mBindIntent;
+    private final ServiceConnection mConnection;
+    private final String mIdentifier;
+    private final String mCreatorLogString;
+    private final ComponentName mTransportComponent;
+    private final Handler mListenerHandler;
+    private final String mPrefixForLog;
+    private final Object mStateLock = new Object();
+    private final Object mLogBufferLock = new Object();
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    @GuardedBy("mLogBufferLock")
+    private final List<String> mLogBuffer = new LinkedList<>();
+
+    @GuardedBy("mStateLock")
+    private final Map<TransportConnectionListener, String> mListeners = new ArrayMap<>();
+
+    @GuardedBy("mStateLock")
+    @State
+    private int mState = State.IDLE;
+
+    @GuardedBy("mStateLock")
+    private volatile IBackupTransport mTransport;
+
+    TransportClient(
+            @UserIdInt int userId,
+            Context context,
+            TransportStats transportStats,
+            Intent bindIntent,
+            ComponentName transportComponent,
+            String identifier,
+            String caller) {
+        this(
+                userId,
+                context,
+                transportStats,
+                bindIntent,
+                transportComponent,
+                identifier,
+                caller,
+                new Handler(Looper.getMainLooper()));
+    }
+
+    @VisibleForTesting
+    TransportClient(
+            @UserIdInt int userId,
+            Context context,
+            TransportStats transportStats,
+            Intent bindIntent,
+            ComponentName transportComponent,
+            String identifier,
+            String caller,
+            Handler listenerHandler) {
+        mUserId = userId;
+        mContext = context;
+        mTransportStats = transportStats;
+        mTransportComponent = transportComponent;
+        mBindIntent = bindIntent;
+        mIdentifier = identifier;
+        mCreatorLogString = caller;
+        mListenerHandler = listenerHandler;
+        mConnection = new TransportConnection(context, this);
+
+        // For logging
+        String classNameForLog = mTransportComponent.getShortClassName().replaceFirst(".*\\.", "");
+        mPrefixForLog = classNameForLog + "#" + mIdentifier + ":";
+
+        mCloseGuard.open("markAsDisposed");
+    }
+
+    public ComponentName getTransportComponent() {
+        return mTransportComponent;
+    }
+
+    /**
+     * Attempts to connect to the transport (if needed).
+     *
+     * <p>Note that being bound is not the same as connected. To be connected you also need to be
+     * bound. You go from nothing to bound, then to bound and connected. To have a usable transport
+     * binder instance you need to be connected. This method will attempt to connect and return an
+     * usable transport binder regardless of the state of the object, it may already be connected,
+     * or bound but not connected, not bound at all or even unusable.
+     *
+     * <p>So, a {@link Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)} (or
+     * one of its variants) can be called or not depending on the inner state. However, it won't be
+     * called again if we're already bound. For example, if one was already requested but the
+     * framework has not yet returned (meaning we're bound but still trying to connect) it won't
+     * trigger another one, just piggyback on the original request.
+     *
+     * <p>It's guaranteed that you are going to get a call back to {@param listener} after this
+     * call. However, the {@param IBackupTransport} parameter, the transport binder, is not
+     * guaranteed to be non-null, or if it's non-null it's not guaranteed to be usable - i.e. it can
+     * throw {@link DeadObjectException}s on method calls. You should check for both in your code.
+     * The reasons for a null transport binder are:
+     *
+     * <ul>
+     *   <li>Some code called {@link #unbind(String)} before you got a callback.
+     *   <li>The framework had already called {@link
+     *       ServiceConnection#onServiceDisconnected(ComponentName)} or {@link
+     *       ServiceConnection#onBindingDied(ComponentName)} on this object's connection before.
+     *       Check the documentation of those methods for when that happens.
+     *   <li>The framework returns false for {@link Context#bindServiceAsUser(Intent,
+     *       ServiceConnection, int, UserHandle)} (or one of its variants). Check documentation for
+     *       when this happens.
+     * </ul>
+     *
+     * For unusable transport binders check {@link DeadObjectException}.
+     *
+     * @param listener The listener that will be called with the (possibly null or unusable) {@link
+     *     IBackupTransport} instance and this {@link TransportClient} object.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. This
+     *     should be a human-readable short string that is easily identifiable in the logs. Ideally
+     *     TAG.methodName(), where TAG is the one used in logcat. In cases where this is is not very
+     *     descriptive like MyHandler.handleMessage() you should put something that someone reading
+     *     the code would understand, like MyHandler/MSG_FOO.
+     * @see #connect(String)
+     * @see DeadObjectException
+     * @see ServiceConnection#onServiceConnected(ComponentName, IBinder)
+     * @see ServiceConnection#onServiceDisconnected(ComponentName)
+     * @see Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)
+     */
+    public void connectAsync(TransportConnectionListener listener, String caller) {
+        synchronized (mStateLock) {
+            checkStateIntegrityLocked();
+
+            switch (mState) {
+                case State.UNUSABLE:
+                    log(Priority.WARN, caller, "Async connect: UNUSABLE client");
+                    notifyListener(listener, null, caller);
+                    break;
+                case State.IDLE:
+                    boolean hasBound =
+                            mContext.bindServiceAsUser(
+                                    mBindIntent,
+                                    mConnection,
+                                    Context.BIND_AUTO_CREATE,
+                                    UserHandle.of(mUserId));
+                    if (hasBound) {
+                        // We don't need to set a time-out because we are guaranteed to get a call
+                        // back in ServiceConnection, either an onServiceConnected() or
+                        // onBindingDied().
+                        log(Priority.DEBUG, caller, "Async connect: service bound, connecting");
+                        setStateLocked(State.BOUND_AND_CONNECTING, null);
+                        mListeners.put(listener, caller);
+                    } else {
+                        log(Priority.ERROR, "Async connect: bindService returned false");
+                        // mState remains State.IDLE
+                        mContext.unbindService(mConnection);
+                        notifyListener(listener, null, caller);
+                    }
+                    break;
+                case State.BOUND_AND_CONNECTING:
+                    log(
+                            Priority.DEBUG,
+                            caller,
+                            "Async connect: already connecting, adding listener");
+                    mListeners.put(listener, caller);
+                    break;
+                case State.CONNECTED:
+                    log(Priority.DEBUG, caller, "Async connect: reusing transport");
+                    notifyListener(listener, mTransport, caller);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Removes the transport binding.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
+     */
+    public void unbind(String caller) {
+        synchronized (mStateLock) {
+            checkStateIntegrityLocked();
+
+            log(Priority.DEBUG, caller, "Unbind requested (was " + stateToString(mState) + ")");
+            switch (mState) {
+                case State.UNUSABLE:
+                case State.IDLE:
+                    break;
+                case State.BOUND_AND_CONNECTING:
+                    setStateLocked(State.IDLE, null);
+                    // After unbindService() no calls back to mConnection
+                    mContext.unbindService(mConnection);
+                    notifyListenersAndClearLocked(null);
+                    break;
+                case State.CONNECTED:
+                    setStateLocked(State.IDLE, null);
+                    mContext.unbindService(mConnection);
+                    break;
+            }
+        }
+    }
+
+    /** Marks this TransportClient as disposed, allowing it to be GC'ed without warnings. */
+    public void markAsDisposed() {
+        synchronized (mStateLock) {
+            Preconditions.checkState(
+                    mState < State.BOUND_AND_CONNECTING, "Can't mark as disposed if still bound");
+            mCloseGuard.close();
+        }
+    }
+
+    /**
+     * Attempts to connect to the transport (if needed) and returns it.
+     *
+     * <p>Synchronous version of {@link #connectAsync(TransportConnectionListener, String)}. The
+     * same observations about state are valid here. Also, what was said about the {@link
+     * IBackupTransport} parameter of {@link TransportConnectionListener} now apply to the return
+     * value of this method.
+     *
+     * <p>This is a potentially blocking operation, so be sure to call this carefully on the correct
+     * threads. You can't call this from the process main-thread (it throws an exception if you do
+     * so).
+     *
+     * <p>In most cases only the first call to this method will block, the following calls should
+     * return instantly. However, this is not guaranteed.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
+     * @return A {@link IBackupTransport} transport binder instance or null. If it's non-null it can
+     *     still be unusable - throws {@link DeadObjectException} on method calls
+     */
+    @WorkerThread
+    @Nullable
+    public IBackupTransport connect(String caller) {
+        // If called on the main-thread this could deadlock waiting because calls to
+        // ServiceConnection are on the main-thread as well
+        Preconditions.checkState(
+                !Looper.getMainLooper().isCurrentThread(), "Can't call connect() on main thread");
+
+        IBackupTransport transport = mTransport;
+        if (transport != null) {
+            log(Priority.DEBUG, caller, "Sync connect: reusing transport");
+            return transport;
+        }
+
+        // If it's already UNUSABLE we return straight away, no need to go to main-thread
+        synchronized (mStateLock) {
+            if (mState == State.UNUSABLE) {
+                log(Priority.WARN, caller, "Sync connect: UNUSABLE client");
+                return null;
+            }
+        }
+
+        CompletableFuture<IBackupTransport> transportFuture = new CompletableFuture<>();
+        TransportConnectionListener requestListener =
+                (requestedTransport, transportClient) ->
+                        transportFuture.complete(requestedTransport);
+
+        long requestTime = SystemClock.elapsedRealtime();
+        log(Priority.DEBUG, caller, "Sync connect: calling async");
+        connectAsync(requestListener, caller);
+
+        try {
+            transport = transportFuture.get();
+            long time = SystemClock.elapsedRealtime() - requestTime;
+            mTransportStats.registerConnectionTime(mTransportComponent, time);
+            log(Priority.DEBUG, caller, String.format(Locale.US, "Connect took %d ms", time));
+            return transport;
+        } catch (InterruptedException | ExecutionException e) {
+            String error = e.getClass().getSimpleName();
+            log(Priority.ERROR, caller, error + " while waiting for transport: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Tries to connect to the transport, if it fails throws {@link TransportNotAvailableException}.
+     *
+     * <p>Same as {@link #connect(String)} except it throws instead of returning null.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
+     * @return A {@link IBackupTransport} transport binder instance.
+     * @see #connect(String)
+     * @throws TransportNotAvailableException if connection attempt fails.
+     */
+    @WorkerThread
+    public IBackupTransport connectOrThrow(String caller) throws TransportNotAvailableException {
+        IBackupTransport transport = connect(caller);
+        if (transport == null) {
+            log(Priority.ERROR, caller, "Transport connection failed");
+            throw new TransportNotAvailableException();
+        }
+        return transport;
+    }
+
+    /**
+     * If the {@link TransportClient} is already connected to the transport, returns the transport,
+     * otherwise throws {@link TransportNotAvailableException}.
+     *
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link #connectAsync(TransportConnectionListener, String)} for more details.
+     * @return A {@link IBackupTransport} transport binder instance.
+     * @throws TransportNotAvailableException if not connected.
+     */
+    public IBackupTransport getConnectedTransport(String caller)
+            throws TransportNotAvailableException {
+        IBackupTransport transport = mTransport;
+        if (transport == null) {
+            log(Priority.ERROR, caller, "Transport not connected");
+            throw new TransportNotAvailableException();
+        }
+        return transport;
+    }
+
+    @Override
+    public String toString() {
+        return "TransportClient{"
+                + mTransportComponent.flattenToShortString()
+                + "#"
+                + mIdentifier
+                + "}";
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        synchronized (mStateLock) {
+            mCloseGuard.warnIfOpen();
+            if (mState >= State.BOUND_AND_CONNECTING) {
+                String callerLogString = "TransportClient.finalize()";
+                log(
+                        Priority.ERROR,
+                        callerLogString,
+                        "Dangling TransportClient created in [" + mCreatorLogString + "] being "
+                                + "GC'ed. Left bound, unbinding...");
+                try {
+                    unbind(callerLogString);
+                } catch (IllegalStateException e) {
+                    // May throw because there may be a race between this method being called and
+                    // the framework calling any method on the connection with the weak reference
+                    // there already cleared. In this case the connection will unbind before this
+                    // is called. This is fine.
+                }
+            }
+        }
+    }
+
+    private void onServiceConnected(IBinder binder) {
+        IBackupTransport transport = IBackupTransport.Stub.asInterface(binder);
+        synchronized (mStateLock) {
+            checkStateIntegrityLocked();
+
+            if (mState != State.UNUSABLE) {
+                log(Priority.DEBUG, "Transport connected");
+                setStateLocked(State.CONNECTED, transport);
+                notifyListenersAndClearLocked(transport);
+            }
+        }
+    }
+
+    /**
+     * If we are called here the TransportClient becomes UNUSABLE. After one of these calls, if a
+     * binding happen again the new service can be a different instance. Since transports are
+     * stateful, we don't want a new instance responding for an old instance's state.
+     */
+    private void onServiceDisconnected() {
+        synchronized (mStateLock) {
+            log(Priority.ERROR, "Service disconnected: client UNUSABLE");
+            setStateLocked(State.UNUSABLE, null);
+            try {
+                // After unbindService() no calls back to mConnection
+                mContext.unbindService(mConnection);
+            } catch (IllegalArgumentException e) {
+                // TODO: Investigate why this is happening
+                // We're UNUSABLE, so any calls to mConnection will be no-op, so it's safe to
+                // swallow this one
+                log(
+                        Priority.WARN,
+                        "Exception trying to unbind onServiceDisconnected(): " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * If we are called here the TransportClient becomes UNUSABLE for the same reason as in {@link
+     * #onServiceDisconnected()}.
+     */
+    private void onBindingDied() {
+        synchronized (mStateLock) {
+            checkStateIntegrityLocked();
+
+            log(Priority.ERROR, "Binding died: client UNUSABLE");
+            // After unbindService() no calls back to mConnection
+            switch (mState) {
+                case State.UNUSABLE:
+                    break;
+                case State.IDLE:
+                    log(Priority.ERROR, "Unexpected state transition IDLE => UNUSABLE");
+                    setStateLocked(State.UNUSABLE, null);
+                    break;
+                case State.BOUND_AND_CONNECTING:
+                    setStateLocked(State.UNUSABLE, null);
+                    mContext.unbindService(mConnection);
+                    notifyListenersAndClearLocked(null);
+                    break;
+                case State.CONNECTED:
+                    setStateLocked(State.UNUSABLE, null);
+                    mContext.unbindService(mConnection);
+                    break;
+            }
+        }
+    }
+
+    private void notifyListener(
+            TransportConnectionListener listener,
+            @Nullable IBackupTransport transport,
+            String caller) {
+        String transportString = (transport != null) ? "IBackupTransport" : "null";
+        log(Priority.INFO, "Notifying [" + caller + "] transport = " + transportString);
+        mListenerHandler.post(() -> listener.onTransportConnectionResult(transport, this));
+    }
+
+    @GuardedBy("mStateLock")
+    private void notifyListenersAndClearLocked(@Nullable IBackupTransport transport) {
+        for (Map.Entry<TransportConnectionListener, String> entry : mListeners.entrySet()) {
+            TransportConnectionListener listener = entry.getKey();
+            String caller = entry.getValue();
+            notifyListener(listener, transport, caller);
+        }
+        mListeners.clear();
+    }
+
+    @GuardedBy("mStateLock")
+    private void setStateLocked(@State int state, @Nullable IBackupTransport transport) {
+        log(Priority.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state));
+        onStateTransition(mState, state);
+        mState = state;
+        mTransport = transport;
+    }
+
+    private void onStateTransition(int oldState, int newState) {
+        String transport = mTransportComponent.flattenToShortString();
+        int bound = transitionThroughState(oldState, newState, State.BOUND_AND_CONNECTING);
+        int connected = transitionThroughState(oldState, newState, State.CONNECTED);
+        if (bound != Transition.NO_TRANSITION) {
+            int value = (bound == Transition.UP) ? 1 : 0; // 1 is bound, 0 is not bound
+            EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transport, value);
+        }
+        if (connected != Transition.NO_TRANSITION) {
+            int value = (connected == Transition.UP) ? 1 : 0; // 1 is connected, 0 is not connected
+            EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_CONNECTION, transport, value);
+        }
+    }
+
+    /**
+     * Returns:
+     *
+     * <ul>
+     *   <li>{@link Transition#UP}, if oldState < stateReference <= newState
+     *   <li>{@link Transition#DOWN}, if oldState >= stateReference > newState
+     *   <li>{@link Transition#NO_TRANSITION}, otherwise
+     */
+    @Transition
+    private int transitionThroughState(
+            @State int oldState, @State int newState, @State int stateReference) {
+        if (oldState < stateReference && stateReference <= newState) {
+            return Transition.UP;
+        }
+        if (oldState >= stateReference && stateReference > newState) {
+            return Transition.DOWN;
+        }
+        return Transition.NO_TRANSITION;
+    }
+
+    @GuardedBy("mStateLock")
+    private void checkStateIntegrityLocked() {
+        switch (mState) {
+            case State.UNUSABLE:
+                checkState(mListeners.isEmpty(), "Unexpected listeners when state = UNUSABLE");
+                checkState(
+                        mTransport == null, "Transport expected to be null when state = UNUSABLE");
+            case State.IDLE:
+                checkState(mListeners.isEmpty(), "Unexpected listeners when state = IDLE");
+                checkState(mTransport == null, "Transport expected to be null when state = IDLE");
+                break;
+            case State.BOUND_AND_CONNECTING:
+                checkState(
+                        mTransport == null,
+                        "Transport expected to be null when state = BOUND_AND_CONNECTING");
+                break;
+            case State.CONNECTED:
+                checkState(mListeners.isEmpty(), "Unexpected listeners when state = CONNECTED");
+                checkState(
+                        mTransport != null,
+                        "Transport expected to be non-null when state = CONNECTED");
+                break;
+            default:
+                checkState(false, "Unexpected state = " + stateToString(mState));
+        }
+    }
+
+    private void checkState(boolean assertion, String message) {
+        if (!assertion) {
+            log(Priority.ERROR, message);
+        }
+    }
+
+    private String stateToString(@State int state) {
+        switch (state) {
+            case State.UNUSABLE:
+                return "UNUSABLE";
+            case State.IDLE:
+                return "IDLE";
+            case State.BOUND_AND_CONNECTING:
+                return "BOUND_AND_CONNECTING";
+            case State.CONNECTED:
+                return "CONNECTED";
+            default:
+                return "<UNKNOWN = " + state + ">";
+        }
+    }
+
+    private void log(int priority, String message) {
+        TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, null, message));
+        saveLogEntry(formatMessage(null, null, message));
+    }
+
+    private void log(int priority, String caller, String message) {
+        TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, caller, message));
+        saveLogEntry(formatMessage(null, caller, message));
+    }
+
+    private void saveLogEntry(String message) {
+        CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis());
+        message = time + " " + message;
+        synchronized (mLogBufferLock) {
+            if (mLogBuffer.size() == LOG_BUFFER_SIZE) {
+                mLogBuffer.remove(mLogBuffer.size() - 1);
+            }
+            mLogBuffer.add(0, message);
+        }
+    }
+
+    List<String> getLogBuffer() {
+        synchronized (mLogBufferLock) {
+            return Collections.unmodifiableList(mLogBuffer);
+        }
+    }
+
+    @IntDef({Transition.DOWN, Transition.NO_TRANSITION, Transition.UP})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface Transition {
+        int DOWN = -1;
+        int NO_TRANSITION = 0;
+        int UP = 1;
+    }
+
+    @IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface State {
+        // Constant values MUST be in order
+        int UNUSABLE = 0;
+        int IDLE = 1;
+        int BOUND_AND_CONNECTING = 2;
+        int CONNECTED = 3;
+    }
+
+    /**
+     * This class is a proxy to TransportClient methods that doesn't hold a strong reference to the
+     * TransportClient, allowing it to be GC'ed. If the reference was lost it logs a message.
+     */
+    private static class TransportConnection implements ServiceConnection {
+        private final Context mContext;
+        private final WeakReference<TransportClient> mTransportClientRef;
+
+        private TransportConnection(Context context, TransportClient transportClient) {
+            mContext = context;
+            mTransportClientRef = new WeakReference<>(transportClient);
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName transportComponent, IBinder binder) {
+            TransportClient transportClient = mTransportClientRef.get();
+            if (transportClient == null) {
+                referenceLost("TransportConnection.onServiceConnected()");
+                return;
+            }
+            // TODO (b/147705255): Remove when binder calls to IBackupTransport are not blocking
+            // In short-term, blocking calls are OK as the transports come from the whitelist at
+            // {@link SystemConfig#getBackupTransportWhitelist()}
+            Binder.allowBlocking(binder);
+            transportClient.onServiceConnected(binder);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName transportComponent) {
+            TransportClient transportClient = mTransportClientRef.get();
+            if (transportClient == null) {
+                referenceLost("TransportConnection.onServiceDisconnected()");
+                return;
+            }
+            transportClient.onServiceDisconnected();
+        }
+
+        @Override
+        public void onBindingDied(ComponentName transportComponent) {
+            TransportClient transportClient = mTransportClientRef.get();
+            if (transportClient == null) {
+                referenceLost("TransportConnection.onBindingDied()");
+                return;
+            }
+            transportClient.onBindingDied();
+        }
+
+        /** @see TransportClient#finalize() */
+        private void referenceLost(String caller) {
+            mContext.unbindService(this);
+            TransportUtils.log(
+                    Priority.INFO,
+                    TAG,
+                    caller + " called but TransportClient reference has been GC'ed");
+        }
+    }
+}
diff --git a/com/android/server/backup/transport/TransportClientManager.java b/com/android/server/backup/transport/TransportClientManager.java
new file mode 100644
index 0000000..72b1ee7
--- /dev/null
+++ b/com/android/server/backup/transport/TransportClientManager.java
@@ -0,0 +1,206 @@
+/*
+ * 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.transport;
+
+import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST;
+import static com.android.server.backup.transport.TransportUtils.formatMessage;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportUtils.Priority;
+
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Function;
+
+/**
+ * Manages the creation and disposal of {@link TransportClient}s. The only class that should use
+ * this is {@link TransportManager}, all the other usages should go to {@link TransportManager}.
+ */
+public class TransportClientManager {
+    private static final String TAG = "TransportClientManager";
+    private static final String SERVICE_ACTION_ENCRYPTING_TRANSPORT =
+            "android.encryption.BACKUP_ENCRYPTION";
+    private static final ComponentName ENCRYPTING_TRANSPORT = new ComponentName(
+            "com.android.server.backup.encryption",
+            "com.android.server.backup.encryption.BackupEncryptionService");
+    private static final String ENCRYPTING_TRANSPORT_REAL_TRANSPORT_KEY = "transport";
+
+    private final @UserIdInt int mUserId;
+    private final Context mContext;
+    private final TransportStats mTransportStats;
+    private final Object mTransportClientsLock = new Object();
+    private int mTransportClientsCreated = 0;
+    private Map<TransportClient, String> mTransportClientsCallerMap = new WeakHashMap<>();
+    private final Function<ComponentName, Intent> mIntentFunction;
+
+    /**
+     * Return an {@link Intent} which resolves to an intermediate {@link IBackupTransport} that
+     * encrypts (or decrypts) the data when sending it (or receiving it) from the {@link
+     * IBackupTransport} for the given {@link ComponentName}.
+     */
+    public static Intent getEncryptingTransportIntent(ComponentName tranportComponent) {
+        return new Intent(SERVICE_ACTION_ENCRYPTING_TRANSPORT)
+                .setComponent(ENCRYPTING_TRANSPORT)
+                .putExtra(ENCRYPTING_TRANSPORT_REAL_TRANSPORT_KEY, tranportComponent);
+    }
+
+    /**
+     * Return an {@link Intent} which resolves to the {@link IBackupTransport} for the {@link
+     * ComponentName}.
+     */
+    private static Intent getRealTransportIntent(ComponentName transportComponent) {
+        return new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(transportComponent);
+    }
+
+    /**
+     * Given a {@link Intent} originally created by {@link
+     * #getEncryptingTransportIntent(ComponentName)}, returns the {@link Intent} which resolves to
+     * the {@link IBackupTransport} for that {@link ComponentName}.
+     */
+    public static Intent getRealTransportIntent(Intent encryptingTransportIntent) {
+        ComponentName transportComponent = encryptingTransportIntent.getParcelableExtra(
+                ENCRYPTING_TRANSPORT_REAL_TRANSPORT_KEY);
+        Intent intent = getRealTransportIntent(transportComponent)
+                .putExtras(encryptingTransportIntent.getExtras());
+        intent.removeExtra(ENCRYPTING_TRANSPORT_REAL_TRANSPORT_KEY);
+        return intent;
+    }
+
+    /**
+     * Create a {@link TransportClientManager} such that {@link #getTransportClient(ComponentName,
+     * Bundle, String)} returns a {@link TransportClient} which connects to an intermediate {@link
+     * IBackupTransport} that encrypts (or decrypts) the data when sending it (or receiving it) from
+     * the {@link IBackupTransport} for the given {@link ComponentName}.
+     */
+    public static TransportClientManager createEncryptingClientManager(@UserIdInt int userId,
+            Context context, TransportStats transportStats) {
+        return new TransportClientManager(userId, context, transportStats,
+                TransportClientManager::getEncryptingTransportIntent);
+    }
+
+    public TransportClientManager(@UserIdInt int userId, Context context,
+            TransportStats transportStats) {
+        this(userId, context, transportStats, TransportClientManager::getRealTransportIntent);
+    }
+
+    private TransportClientManager(@UserIdInt int userId, Context context,
+            TransportStats transportStats, Function<ComponentName, Intent> intentFunction) {
+        mUserId = userId;
+        mContext = context;
+        mTransportStats = transportStats;
+        mIntentFunction = intentFunction;
+    }
+
+    /**
+     * Retrieves a {@link TransportClient} for the transport identified by {@param
+     * transportComponent}.
+     *
+     * @param transportComponent The {@link ComponentName} of the transport.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient}.
+     */
+    public TransportClient getTransportClient(ComponentName transportComponent, String caller) {
+        return getTransportClient(transportComponent, null, caller);
+    }
+
+    /**
+     * Retrieves a {@link TransportClient} for the transport identified by {@param
+     * transportComponent} whose binding intent will have the {@param extras} extras.
+     *
+     * @param transportComponent The {@link ComponentName} of the transport.
+     * @param extras A {@link Bundle} of extras to pass to the binding intent.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     * @return A {@link TransportClient}.
+     */
+    public TransportClient getTransportClient(
+            ComponentName transportComponent, @Nullable Bundle extras, String caller) {
+        Intent bindIntent = mIntentFunction.apply(transportComponent);
+        if (extras != null) {
+            bindIntent.putExtras(extras);
+        }
+        return getTransportClient(transportComponent, caller, bindIntent);
+    }
+
+    private TransportClient getTransportClient(
+            ComponentName transportComponent, String caller, Intent bindIntent) {
+        synchronized (mTransportClientsLock) {
+            TransportClient transportClient =
+                    new TransportClient(
+                            mUserId,
+                            mContext,
+                            mTransportStats,
+                            bindIntent,
+                            transportComponent,
+                            Integer.toString(mTransportClientsCreated),
+                            caller);
+            mTransportClientsCallerMap.put(transportClient, caller);
+            mTransportClientsCreated++;
+            TransportUtils.log(
+                    Priority.DEBUG,
+                    TAG,
+                    formatMessage(null, caller, "Retrieving " + transportClient));
+            return transportClient;
+        }
+    }
+
+    /**
+     * Disposes of the {@link TransportClient}.
+     *
+     * @param transportClient The {@link TransportClient} to be disposed of.
+     * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
+     *     {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
+     *     details.
+     */
+    public void disposeOfTransportClient(TransportClient transportClient, String caller) {
+        transportClient.unbind(caller);
+        transportClient.markAsDisposed();
+        synchronized (mTransportClientsLock) {
+            TransportUtils.log(
+                    Priority.DEBUG,
+                    TAG,
+                    formatMessage(null, caller, "Disposing of " + transportClient));
+            mTransportClientsCallerMap.remove(transportClient);
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("Transport clients created: " + mTransportClientsCreated);
+        synchronized (mTransportClientsLock) {
+            pw.println("Current transport clients: " + mTransportClientsCallerMap.size());
+            for (TransportClient transportClient : mTransportClientsCallerMap.keySet()) {
+                String caller = mTransportClientsCallerMap.get(transportClient);
+                pw.println("    " + transportClient + " [" + caller + "]");
+                for (String logEntry : transportClient.getLogBuffer()) {
+                    pw.println("        " + logEntry);
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/transport/TransportClientManagerTest.java b/com/android/server/backup/transport/TransportClientManagerTest.java
new file mode 100644
index 0000000..f033af8
--- /dev/null
+++ b/com/android/server/backup/transport/TransportClientManagerTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.transport;
+
+import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+
+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.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TransportClientManagerTest {
+    private static final String PACKAGE_NAME = "random.package.name";
+    private static final String CLASS_NAME = "random.package.name.transport.Transport";
+
+    @Mock private Context mContext;
+    @Mock private TransportConnectionListener mTransportConnectionListener;
+    private @UserIdInt int mUserId;
+    private TransportClientManager mTransportClientManager;
+    private ComponentName mTransportComponent;
+    private Intent mBindIntent;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mUserId = UserHandle.USER_SYSTEM;
+        mTransportClientManager =
+                new TransportClientManager(mUserId, mContext, new TransportStats());
+        mTransportComponent = new ComponentName(PACKAGE_NAME, CLASS_NAME);
+        mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent);
+
+        when(mContext.bindServiceAsUser(
+                        any(Intent.class),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class)))
+                .thenReturn(true);
+    }
+
+    @Test
+    public void testGetTransportClient() {
+        TransportClient transportClient =
+                mTransportClientManager.getTransportClient(mTransportComponent, "caller");
+
+        // Connect to be able to extract the intent
+        transportClient.connectAsync(mTransportConnectionListener, "caller");
+        verify(mContext)
+                .bindServiceAsUser(
+                        argThat(matchesIntentAndExtras(mBindIntent)),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class));
+    }
+
+    @Test
+    public void testGetTransportClient_withExtras_createsTransportClientWithCorrectIntent() {
+        Bundle extras = new Bundle();
+        extras.putBoolean("random_extra", true);
+
+        TransportClient transportClient =
+                mTransportClientManager.getTransportClient(mTransportComponent, extras, "caller");
+
+        transportClient.connectAsync(mTransportConnectionListener, "caller");
+        mBindIntent.putExtras(extras);
+        verify(mContext)
+                .bindServiceAsUser(
+                        argThat(matchesIntentAndExtras(mBindIntent)),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class));
+    }
+
+    @Test
+    public void testDisposeOfTransportClient() {
+        TransportClient transportClient =
+                spy(mTransportClientManager.getTransportClient(mTransportComponent, "caller"));
+
+        mTransportClientManager.disposeOfTransportClient(transportClient, "caller");
+
+        verify(transportClient).unbind(any());
+        verify(transportClient).markAsDisposed();
+    }
+
+    private ArgumentMatcher<Intent> matchesIntentAndExtras(Intent expectedIntent) {
+        return (Intent actualIntent) -> {
+            if (!expectedIntent.filterEquals(actualIntent)) {
+                return false;
+            }
+
+            Bundle expectedExtras = expectedIntent.getExtras();
+            Bundle actualExtras = actualIntent.getExtras();
+
+            if (expectedExtras == null && actualExtras == null) {
+                return true;
+            }
+
+            if (expectedExtras == null || actualExtras == null) {
+                return false;
+            }
+
+            if (expectedExtras.size() != actualExtras.size()) {
+                return false;
+            }
+
+            for (String key : expectedExtras.keySet()) {
+                if (!expectedExtras.get(key).equals(actualExtras.get(key))) {
+                    return false;
+                }
+            }
+
+            return true;
+        };
+    }
+}
diff --git a/com/android/server/backup/transport/TransportClientTest.java b/com/android/server/backup/transport/TransportClientTest.java
new file mode 100644
index 0000000..392f2ca
--- /dev/null
+++ b/com/android/server/backup/transport/TransportClientTest.java
@@ -0,0 +1,549 @@
+/*
+ * 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.transport;
+
+import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST;
+import static com.android.server.backup.testing.TestUtils.assertEventLogged;
+import static com.android.server.backup.testing.TestUtils.assertLogcatAtLeast;
+import static com.android.server.backup.testing.TestUtils.assertLogcatAtMost;
+
+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.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+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 org.testng.Assert.expectThrows;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.EventLogTags;
+import com.android.server.testing.shadows.FrameworkShadowLooper;
+import com.android.server.testing.shadows.ShadowCloseGuard;
+import com.android.server.testing.shadows.ShadowEventLog;
+import com.android.server.testing.shadows.ShadowSlog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+            ShadowEventLog.class,
+            ShadowCloseGuard.class,
+            ShadowSlog.class,
+            FrameworkShadowLooper.class
+        })
+@Presubmit
+public class TransportClientTest {
+    private static final String PACKAGE_NAME = "some.package.name";
+
+    @Mock private Context mContext;
+    @Mock private TransportConnectionListener mTransportConnectionListener;
+    @Mock private TransportConnectionListener mTransportConnectionListener2;
+    @Mock private IBackupTransport.Stub mTransportBinder;
+    @UserIdInt private int mUserId;
+    private TransportStats mTransportStats;
+    private TransportClient mTransportClient;
+    private ComponentName mTransportComponent;
+    private String mTransportString;
+    private Intent mBindIntent;
+    private FrameworkShadowLooper mShadowMainLooper;
+    private ShadowLooper mShadowWorkerLooper;
+    private Handler mWorkerHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mUserId = UserHandle.USER_SYSTEM;
+        Looper mainLooper = Looper.getMainLooper();
+        mShadowMainLooper = extract(mainLooper);
+        mTransportComponent =
+                new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport");
+        mTransportString = mTransportComponent.flattenToShortString();
+        mTransportStats = new TransportStats();
+        mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent);
+        mTransportClient =
+                new TransportClient(
+                        mUserId,
+                        mContext,
+                        mTransportStats,
+                        mBindIntent,
+                        mTransportComponent,
+                        "1",
+                        "caller",
+                        new Handler(mainLooper));
+
+        when(mContext.bindServiceAsUser(
+                        eq(mBindIntent),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class)))
+                .thenReturn(true);
+
+        HandlerThread workerThread = new HandlerThread("worker");
+        workerThread.start();
+        mShadowWorkerLooper = shadowOf(workerThread.getLooper());
+        mWorkerHandler = workerThread.getThreadHandler();
+    }
+
+    @Test
+    public void testGetTransportComponent_returnsTransportComponent() {
+        assertThat(mTransportClient.getTransportComponent()).isEqualTo(mTransportComponent);
+    }
+
+    @Test
+    public void testConnectAsync_callsBindService() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller");
+
+        verify(mContext)
+                .bindServiceAsUser(
+                        eq(mBindIntent),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class));
+    }
+
+    @Test
+    public void testConnectAsync_callsListenerWhenConnected() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener)
+                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_whenPendingConnection_callsAllListenersWhenConnected() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
+
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener)
+                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient));
+        verify(mTransportConnectionListener2)
+                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_whenAlreadyConnected_callsListener() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
+
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener2)
+                .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_whenFrameworkDoesntBind_callsListener() {
+        when(mContext.bindServiceAsUser(
+                        eq(mBindIntent),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class)))
+                .thenReturn(false);
+
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller");
+
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_whenFrameworkDoesNotBind_releasesConnection() {
+        when(mContext.bindServiceAsUser(
+                        eq(mBindIntent),
+                        any(ServiceConnection.class),
+                        anyInt(),
+                        any(UserHandle.class)))
+                .thenReturn(false);
+
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller");
+
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        verify(mContext).unbindService(eq(connection));
+    }
+
+    @Test
+    public void testConnectAsync_afterOnServiceDisconnectedBeforeNewConnection_callsListener() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        connection.onServiceDisconnected(mTransportComponent);
+
+        mTransportClient.connectAsync(mTransportConnectionListener2, "caller1");
+
+        verify(mTransportConnectionListener2)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_afterOnServiceDisconnectedAfterNewConnection_callsListener() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        connection.onServiceDisconnected(mTransportComponent);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        mTransportClient.connectAsync(mTransportConnectionListener2, "caller1");
+
+        // Yes, it should return null because the object became unusable, check design doc
+        verify(mTransportConnectionListener2)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_callsListenerIfBindingDies() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        connection.onBindingDied(mTransportComponent);
+
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_whenPendingConnection_callsListenersIfBindingDies() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
+
+        connection.onBindingDied(mTransportComponent);
+        mShadowMainLooper.runToEndOfTasks();
+        verify(mTransportConnectionListener)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+        verify(mTransportConnectionListener2)
+                .onTransportConnectionResult(isNull(), eq(mTransportClient));
+    }
+
+    @Test
+    public void testConnectAsync_beforeFrameworkCall_logsBoundTransitionEvent() {
+        ShadowEventLog.setUp();
+
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+    }
+
+    @Test
+    public void testConnectAsync_afterOnServiceConnected_logsBoundAndConnectedTransitionEvents() {
+        ShadowEventLog.setUp();
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 1);
+    }
+
+    @Test
+    public void testConnectAsync_afterOnBindingDied_logsBoundAndUnboundTransitionEvents() {
+        ShadowEventLog.setUp();
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+
+        connection.onBindingDied(mTransportComponent);
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+    }
+
+    @Test
+    public void testConnect_whenConnected_returnsTransport() throws Exception {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        IBackupTransport transportBinder =
+                runInWorkerThread(() -> mTransportClient.connect("caller2"));
+
+        assertThat(transportBinder).isNotNull();
+    }
+
+    @Test
+    public void testConnect_afterOnServiceDisconnected_returnsNull() throws Exception {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        connection.onServiceDisconnected(mTransportComponent);
+
+        IBackupTransport transportBinder =
+                runInWorkerThread(() -> mTransportClient.connect("caller2"));
+
+        assertThat(transportBinder).isNull();
+    }
+
+    @Test
+    public void testConnect_afterOnBindingDied_returnsNull() throws Exception {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onBindingDied(mTransportComponent);
+
+        IBackupTransport transportBinder =
+                runInWorkerThread(() -> mTransportClient.connect("caller2"));
+
+        assertThat(transportBinder).isNull();
+    }
+
+    @Test
+    public void testConnect_callsThroughToConnectAsync() throws Exception {
+        // We can't mock bindServiceAsUser() instead of connectAsync() and call the listener inline
+        // because in our code in TransportClient we assume this is NOT run inline, such that the
+        // reentrant lock can't be acquired by the listener at the call-site of bindServiceAsUser(),
+        // which is what would happened if we mocked bindServiceAsUser() to call the listener
+        // inline.
+        TransportClient transportClient = spy(mTransportClient);
+        doAnswer(
+                        invocation -> {
+                            TransportConnectionListener listener = invocation.getArgument(0);
+                            listener.onTransportConnectionResult(mTransportBinder, transportClient);
+                            return null;
+                        })
+                .when(transportClient)
+                .connectAsync(any(), any());
+
+        IBackupTransport transportBinder =
+                runInWorkerThread(() -> transportClient.connect("caller"));
+
+        assertThat(transportBinder).isNotNull();
+    }
+
+    @Test
+    public void testUnbind_whenConnected_logsDisconnectedAndUnboundTransitionEvents() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        ShadowEventLog.setUp();
+
+        mTransportClient.unbind("caller1");
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+    }
+
+    @Test
+    public void
+            testOnServiceDisconnected_whenConnected_logsDisconnectedAndUnboundTransitionEvents() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        ShadowEventLog.setUp();
+
+        connection.onServiceDisconnected(mTransportComponent);
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+    }
+
+    @Test
+    public void testOnBindingDied_whenConnected_logsDisconnectedAndUnboundTransitionEvents() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        ShadowEventLog.setUp();
+
+        connection.onBindingDied(mTransportComponent);
+
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+        assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+    }
+
+    @Test
+    public void testMarkAsDisposed_whenCreated() {
+        mTransportClient.markAsDisposed();
+
+        // No exception thrown
+    }
+
+    @Test
+    public void testMarkAsDisposed_afterOnBindingDied() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onBindingDied(mTransportComponent);
+
+        mTransportClient.markAsDisposed();
+
+        // No exception thrown
+    }
+
+    @Test
+    public void testMarkAsDisposed_whenConnectedAndUnbound() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        mTransportClient.unbind("caller1");
+
+        mTransportClient.markAsDisposed();
+
+        // No exception thrown
+    }
+
+    @Test
+    public void testMarkAsDisposed_afterOnServiceDisconnected() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        connection.onServiceDisconnected(mTransportComponent);
+
+        mTransportClient.markAsDisposed();
+
+        // No exception thrown
+    }
+
+    @Test
+    public void testMarkAsDisposed_whenBound() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+
+        expectThrows(RuntimeException.class, mTransportClient::markAsDisposed);
+    }
+
+    @Test
+    public void testMarkAsDisposed_whenConnected() {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+
+        expectThrows(RuntimeException.class, mTransportClient::markAsDisposed);
+    }
+
+    @Test
+    @SuppressWarnings("FinalizeCalledExplicitly")
+    public void testFinalize_afterCreated() throws Throwable {
+        ShadowLog.reset();
+
+        mTransportClient.finalize();
+
+        assertLogcatAtMost(TransportClient.TAG, Log.INFO);
+    }
+
+    @Test
+    @SuppressWarnings("FinalizeCalledExplicitly")
+    public void testFinalize_whenBound() throws Throwable {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ShadowLog.reset();
+
+        mTransportClient.finalize();
+
+        assertLogcatAtLeast(TransportClient.TAG, Log.ERROR);
+    }
+
+    @Test
+    @SuppressWarnings("FinalizeCalledExplicitly")
+    public void testFinalize_whenConnected() throws Throwable {
+        mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+        ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+        connection.onServiceConnected(mTransportComponent, mTransportBinder);
+        ShadowLog.reset();
+
+        mTransportClient.finalize();
+
+        expectThrows(
+                TransportNotAvailableException.class,
+                () -> mTransportClient.getConnectedTransport("caller1"));
+        assertLogcatAtLeast(TransportClient.TAG, Log.ERROR);
+    }
+
+    @Test
+    @SuppressWarnings("FinalizeCalledExplicitly")
+    public void testFinalize_whenNotMarkedAsDisposed() throws Throwable {
+        ShadowCloseGuard.setUp();
+
+        mTransportClient.finalize();
+
+        assertThat(ShadowCloseGuard.hasReported()).isTrue();
+    }
+
+    @Test
+    @SuppressWarnings("FinalizeCalledExplicitly")
+    public void testFinalize_whenMarkedAsDisposed() throws Throwable {
+        mTransportClient.markAsDisposed();
+        ShadowCloseGuard.setUp();
+
+        mTransportClient.finalize();
+
+        assertThat(ShadowCloseGuard.hasReported()).isFalse();
+    }
+
+    @Nullable
+    private <T> T runInWorkerThread(Supplier<T> supplier) throws Exception {
+        CompletableFuture<T> future = new CompletableFuture<>();
+        mWorkerHandler.post(() -> future.complete(supplier.get()));
+        // Although we are using a separate looper, we are still calling runToEndOfTasks() in the
+        // main thread (Robolectric only *simulates* multi-thread). The only option left is to fool
+        // the caller.
+        mShadowMainLooper.setCurrentThread(false);
+        mShadowWorkerLooper.runToEndOfTasks();
+        mShadowMainLooper.reset();
+        return future.get();
+    }
+
+    private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) {
+        ArgumentCaptor<ServiceConnection> connectionCaptor =
+                ArgumentCaptor.forClass(ServiceConnection.class);
+        verify(context)
+                .bindServiceAsUser(
+                        any(Intent.class),
+                        connectionCaptor.capture(),
+                        anyInt(),
+                        any(UserHandle.class));
+        return connectionCaptor.getValue();
+    }
+}
diff --git a/com/android/server/backup/transport/TransportConnectionListener.java b/com/android/server/backup/transport/TransportConnectionListener.java
new file mode 100644
index 0000000..1ccffd0
--- /dev/null
+++ b/com/android/server/backup/transport/TransportConnectionListener.java
@@ -0,0 +1,37 @@
+/*
+ * 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.transport;
+
+import android.annotation.Nullable;
+
+import com.android.internal.backup.IBackupTransport;
+
+/**
+ * Listener to be called by {@link TransportClient#connectAsync(TransportConnectionListener,
+ * String)}.
+ */
+public interface TransportConnectionListener {
+    /**
+     * Called when {@link TransportClient} has a transport binder available or that it decided it
+     * couldn't obtain one, in which case {@param transport} is null.
+     *
+     * @param transport A {@link IBackupTransport} transport binder or null.
+     * @param transportClient The {@link TransportClient} used to retrieve this transport binder.
+     */
+    void onTransportConnectionResult(
+            @Nullable IBackupTransport transport, TransportClient transportClient);
+}
diff --git a/com/android/server/backup/transport/TransportNotAvailableException.java b/com/android/server/backup/transport/TransportNotAvailableException.java
new file mode 100644
index 0000000..c08eb7f
--- /dev/null
+++ b/com/android/server/backup/transport/TransportNotAvailableException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.transport;
+
+import android.util.AndroidException;
+
+import com.android.internal.backup.IBackupTransport;
+
+/**
+ * Exception thrown when the {@link IBackupTransport} is not available. This happen when a {@link
+ * TransportClient} connection attempt fails. Check {@link
+ * TransportClient#connectAsync(TransportConnectionListener, String)} for when that happens.
+ *
+ * @see TransportClient#connectAsync(TransportConnectionListener, String)
+ */
+public class TransportNotAvailableException extends AndroidException {
+    TransportNotAvailableException() {
+        super("Transport not available");
+    }
+}
diff --git a/com/android/server/backup/transport/TransportNotRegisteredException.java b/com/android/server/backup/transport/TransportNotRegisteredException.java
new file mode 100644
index 0000000..02766de
--- /dev/null
+++ b/com/android/server/backup/transport/TransportNotRegisteredException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.transport;
+
+import android.content.ComponentName;
+import android.util.AndroidException;
+
+import com.android.server.backup.TransportManager;
+
+/**
+ * Exception thrown when the transport is not registered.
+ *
+ * @see TransportManager#getTransportDirName(String)
+ * @see TransportManager#getTransportConfigurationIntent(String)
+ * @see TransportManager#getTransportDataManagementIntent(String)
+ * @see TransportManager#getTransportDataManagementLabel(String)
+ */
+public class TransportNotRegisteredException extends AndroidException {
+    public TransportNotRegisteredException(String transportName) {
+        super("Transport " + transportName + " not registered");
+    }
+
+    public TransportNotRegisteredException(ComponentName transportComponent) {
+        super("Transport for host " + transportComponent + " not registered");
+    }
+}
diff --git a/com/android/server/backup/transport/TransportStats.java b/com/android/server/backup/transport/TransportStats.java
new file mode 100644
index 0000000..bd84782
--- /dev/null
+++ b/com/android/server/backup/transport/TransportStats.java
@@ -0,0 +1,122 @@
+/*
+ * 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.transport;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+/** Responsible for aggregating {@link TransportClient} relevant times. */
+public class TransportStats {
+    private final Object mStatsLock = new Object();
+    private final Map<ComponentName, Stats> mTransportStats = new HashMap<>();
+
+    void registerConnectionTime(ComponentName transportComponent, long timeMs) {
+        synchronized (mStatsLock) {
+            Stats stats = mTransportStats.get(transportComponent);
+            if (stats == null) {
+                stats = new Stats();
+                mTransportStats.put(transportComponent, stats);
+            }
+            stats.register(timeMs);
+        }
+    }
+
+    /** Returns {@link Stats} for transport whose host service is {@code transportComponent}. */
+    @Nullable
+    public Stats getStatsForTransport(ComponentName transportComponent) {
+        synchronized (mStatsLock) {
+            Stats stats = mTransportStats.get(transportComponent);
+            if (stats == null) {
+                return null;
+            }
+            return new Stats(stats);
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        synchronized (mStatsLock) {
+            Optional<Stats> aggregatedStats =
+                    mTransportStats.values().stream().reduce(Stats::merge);
+            if (aggregatedStats.isPresent()) {
+                dumpStats(pw, "", aggregatedStats.get());
+            }
+            if (!mTransportStats.isEmpty()) {
+                pw.println("Per transport:");
+                for (ComponentName transportComponent : mTransportStats.keySet()) {
+                    Stats stats = mTransportStats.get(transportComponent);
+                    pw.println("    " + transportComponent.flattenToShortString());
+                    dumpStats(pw, "        ", stats);
+                }
+            }
+        }
+    }
+
+    private static void dumpStats(PrintWriter pw, String prefix, Stats stats) {
+        pw.println(
+                String.format(
+                        Locale.US, "%sAverage connection time: %.2f ms", prefix, stats.average));
+        pw.println(String.format(Locale.US, "%sMax connection time: %d ms", prefix, stats.max));
+        pw.println(String.format(Locale.US, "%sMin connection time: %d ms", prefix, stats.min));
+        pw.println(String.format(Locale.US, "%sNumber of connections: %d ", prefix, stats.n));
+    }
+
+    public static final class Stats {
+        public static Stats merge(Stats a, Stats b) {
+            return new Stats(
+                    a.n + b.n,
+                    (a.average * a.n + b.average * b.n) / (a.n + b.n),
+                    Math.max(a.max, b.max),
+                    Math.min(a.min, b.min));
+        }
+
+        public int n;
+        public double average;
+        public long max;
+        public long min;
+
+        public Stats() {
+            n = 0;
+            average = 0;
+            max = 0;
+            min = Long.MAX_VALUE;
+        }
+
+        private Stats(int n, double average, long max, long min) {
+            this.n = n;
+            this.average = average;
+            this.max = max;
+            this.min = min;
+        }
+
+        private Stats(Stats original) {
+            this(original.n, original.average, original.max, original.min);
+        }
+
+        private void register(long sample) {
+            average = (average * n + sample) / (n + 1);
+            n++;
+            max = Math.max(max, sample);
+            min = Math.min(min, sample);
+        }
+    }
+}
diff --git a/com/android/server/backup/transport/TransportStatsTest.java b/com/android/server/backup/transport/TransportStatsTest.java
new file mode 100644
index 0000000..f01a6b0
--- /dev/null
+++ b/com/android/server/backup/transport/TransportStatsTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.transport;
+
+import static com.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.TransportData.d2dTransport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.transport.TransportStats.Stats;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class TransportStatsTest {
+    private static final double TOLERANCE = 0.0001;
+
+    private TransportStats mTransportStats;
+    private ComponentName mTransportComponent1;
+    private ComponentName mTransportComponent2;
+
+    @Before
+    public void setUp() throws Exception {
+        mTransportStats = new TransportStats();
+        mTransportComponent1 = backupTransport().getTransportComponent();
+        mTransportComponent2 = d2dTransport().getTransportComponent();
+    }
+
+    @Test
+    public void testRegisterConnectionTime() {
+        mTransportStats.registerConnectionTime(mTransportComponent1, 50L);
+
+        Stats stats = mTransportStats.getStatsForTransport(mTransportComponent1);
+        assertThat(stats.average).isWithin(TOLERANCE).of(50);
+        assertThat(stats.max).isEqualTo(50L);
+        assertThat(stats.min).isEqualTo(50L);
+        assertThat(stats.n).isEqualTo(1);
+    }
+
+    @Test
+    public void testRegisterConnectionTime_whenHasAlreadyOneSample() {
+        mTransportStats.registerConnectionTime(mTransportComponent1, 50L);
+
+        mTransportStats.registerConnectionTime(mTransportComponent1, 100L);
+
+        Stats stats = mTransportStats.getStatsForTransport(mTransportComponent1);
+        assertThat(stats.average).isWithin(TOLERANCE).of(75);
+        assertThat(stats.max).isEqualTo(100L);
+        assertThat(stats.min).isEqualTo(50L);
+        assertThat(stats.n).isEqualTo(2);
+    }
+
+    @Test
+    public void testGetStatsForTransport() {
+        mTransportStats.registerConnectionTime(mTransportComponent1, 10L);
+        mTransportStats.registerConnectionTime(mTransportComponent2, 20L);
+
+        Stats stats = mTransportStats.getStatsForTransport(mTransportComponent1);
+
+        assertThat(stats.average).isWithin(TOLERANCE).of(10);
+        assertThat(stats.max).isEqualTo(10L);
+        assertThat(stats.min).isEqualTo(10L);
+        assertThat(stats.n).isEqualTo(1);
+    }
+
+    @Test
+    public void testMerge() {
+        mTransportStats.registerConnectionTime(mTransportComponent1, 10L);
+        mTransportStats.registerConnectionTime(mTransportComponent2, 20L);
+        Stats stats1 = mTransportStats.getStatsForTransport(mTransportComponent1);
+        Stats stats2 = mTransportStats.getStatsForTransport(mTransportComponent2);
+
+        Stats stats = Stats.merge(stats1, stats2);
+
+        assertThat(stats.average).isWithin(TOLERANCE).of(15);
+        assertThat(stats.max).isEqualTo(20L);
+        assertThat(stats.min).isEqualTo(10L);
+        assertThat(stats.n).isEqualTo(2);
+    }
+}
diff --git a/com/android/server/backup/transport/TransportUtils.java b/com/android/server/backup/transport/TransportUtils.java
new file mode 100644
index 0000000..766d77b
--- /dev/null
+++ b/com/android/server/backup/transport/TransportUtils.java
@@ -0,0 +1,82 @@
+/*
+ * 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.transport;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.DeadObjectException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.backup.IBackupTransport;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility methods for transport-related operations. */
+public class TransportUtils {
+    private static final String TAG = "TransportUtils";
+
+    /**
+     * Throws {@link TransportNotAvailableException} if {@param transport} is null. The semantics is
+     * similar to a {@link DeadObjectException} coming from a dead transport binder.
+     */
+    public static IBackupTransport checkTransportNotNull(@Nullable IBackupTransport transport)
+            throws TransportNotAvailableException {
+        if (transport == null) {
+            log(Priority.ERROR, TAG, "Transport not available");
+            throw new TransportNotAvailableException();
+        }
+        return transport;
+    }
+
+    static void log(@Priority int priority, String tag, String message) {
+        if (priority == Priority.WTF) {
+            Slog.wtf(tag, message);
+        } else if (Log.isLoggable(tag, priority)) {
+            Slog.println(priority, tag, message);
+        }
+    }
+
+    static String formatMessage(@Nullable String prefix, @Nullable String caller, String message) {
+        StringBuilder string = new StringBuilder();
+        if (prefix != null) {
+            string.append(prefix).append(" ");
+        }
+        if (caller != null) {
+            string.append("[").append(caller).append("] ");
+        }
+        return string.append(message).toString();
+    }
+
+    /**
+     * Create our own constants so we can log WTF using the same APIs. Except for {@link
+     * Priority#WTF} all the others have the same value, so can be used directly
+     */
+    @IntDef({Priority.VERBOSE, Priority.DEBUG, Priority.INFO, Priority.WARN, Priority.WTF})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Priority {
+        int VERBOSE = Log.VERBOSE;
+        int DEBUG = Log.DEBUG;
+        int INFO = Log.INFO;
+        int WARN = Log.WARN;
+        int ERROR = Log.ERROR;
+        int WTF = -1;
+    }
+
+    private TransportUtils() {}
+}
diff --git a/com/android/server/backup/utils/AppBackupUtils.java b/com/android/server/backup/utils/AppBackupUtils.java
new file mode 100644
index 0000000..35dfccf
--- /dev/null
+++ b/com/android/server/backup/utils/AppBackupUtils.java
@@ -0,0 +1,294 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
+import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupTransport;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
+import com.android.server.backup.transport.TransportClient;
+
+import com.google.android.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * Utility methods wrapping operations on ApplicationInfo and PackageInfo.
+ */
+public class AppBackupUtils {
+    private static final boolean DEBUG = false;
+    // Whitelist of system packages that are eligible for backup in non-system users.
+    private static final Set<String> systemPackagesWhitelistedForAllUsers =
+            Sets.newArraySet(PACKAGE_MANAGER_SENTINEL, PLATFORM_PACKAGE_NAME);
+
+    /**
+     * Returns whether app is eligible for backup.
+     *
+     * High level policy: apps are generally ineligible for backup if certain conditions apply. The
+     * conditions are:
+     *
+     * <ol>
+     *     <li>their manifest states android:allowBackup="false"
+     *     <li>they run as a system-level uid but do not supply their own backup agent
+     *     <li>it is the special shared-storage backup package used for 'adb backup'
+     * </ol>
+     */
+    public static boolean appIsEligibleForBackup(ApplicationInfo app, int userId) {
+        return appIsEligibleForBackup(
+                app, LocalServices.getService(PackageManagerInternal.class), userId);
+    }
+
+    @VisibleForTesting
+    static boolean appIsEligibleForBackup(
+            ApplicationInfo app, PackageManagerInternal packageManager, int userId) {
+        // 1. their manifest states android:allowBackup="false"
+        if ((app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) {
+            return false;
+        }
+
+        // 2. they run as a system-level uid
+        if (UserHandle.isCore(app.uid)) {
+            // and the backup is happening for non-system user on a non-whitelisted package.
+            if (userId != UserHandle.USER_SYSTEM
+                    && !systemPackagesWhitelistedForAllUsers.contains(app.packageName)) {
+                return false;
+            }
+
+            // or do not supply their own backup agent
+            if (app.backupAgentName == null) {
+                return false;
+            }
+        }
+
+        // 3. it is the special shared-storage backup package used for 'adb backup'
+        if (app.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE)) {
+            return false;
+        }
+
+        // 4. it is an "instant" app
+        if (app.isInstantApp()) {
+            return false;
+        }
+
+        return !appIsDisabled(app, packageManager, userId);
+    }
+
+    /**
+     * Returns whether an app is eligible for backup at runtime. That is, the app has to:
+     * <ol>
+     *     <li>Return true for {@link #appIsEligibleForBackup(ApplicationInfo, int)}
+     *     <li>Return false for {@link #appIsStopped(ApplicationInfo)}
+     *     <li>Return false for {@link #appIsDisabled(ApplicationInfo, int)}
+     *     <li>Be eligible for the transport via
+     *         {@link BackupTransport#isAppEligibleForBackup(PackageInfo, boolean)}
+     * </ol>
+     */
+    public static boolean appIsRunningAndEligibleForBackupWithTransport(
+            @Nullable TransportClient transportClient,
+            String packageName,
+            PackageManager pm,
+            int userId) {
+        try {
+            PackageInfo packageInfo = pm.getPackageInfoAsUser(packageName,
+                    PackageManager.GET_SIGNING_CERTIFICATES, userId);
+            ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+            if (!appIsEligibleForBackup(applicationInfo, userId)
+                    || appIsStopped(applicationInfo)
+                    || appIsDisabled(applicationInfo, userId)) {
+                return false;
+            }
+            if (transportClient != null) {
+                try {
+                    IBackupTransport transport =
+                            transportClient.connectOrThrow(
+                                    "AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport");
+                    return transport.isAppEligibleForBackup(
+                            packageInfo, AppBackupUtils.appGetsFullBackup(packageInfo));
+                } catch (Exception e) {
+                    Slog.e(TAG, "Unable to ask about eligibility: " + e.getMessage());
+                }
+            }
+            // If transport is not present we couldn't tell that the package is not eligible.
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /** Avoid backups of 'disabled' apps. */
+    static boolean appIsDisabled(ApplicationInfo app, int userId) {
+        return appIsDisabled(app, LocalServices.getService(PackageManagerInternal.class), userId);
+    }
+
+    @VisibleForTesting
+    static boolean appIsDisabled(
+            ApplicationInfo app, PackageManagerInternal packageManager, int userId) {
+        int enabledSetting = packageManager.getApplicationEnabledState(app.packageName, userId);
+
+        switch (enabledSetting) {
+            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
+            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER:
+            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED:
+                return true;
+            case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
+                return !app.enabled;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the app is in a stopped state.  This is not part of the general "eligible for
+     * backup?" check because we *do* still need to restore data to apps in this state (e.g.
+     * newly-installing ones).
+     *
+     * <p>Reasons for such state:
+     * <ul>
+     *     <li>The app has been force-stopped.
+     *     <li>The app has been cleared.
+     *     <li>The app has just been installed.
+     * </ul>
+     */
+    public static boolean appIsStopped(ApplicationInfo app) {
+        return ((app.flags & ApplicationInfo.FLAG_STOPPED) != 0);
+    }
+
+    /**
+     * Returns whether the app can get full backup. Does *not* check overall backup eligibility
+     * policy!
+     */
+    public static boolean appGetsFullBackup(PackageInfo pkg) {
+        if (pkg.applicationInfo.backupAgentName != null) {
+            // If it has an agent, it gets full backups only if it says so
+            return (pkg.applicationInfo.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0;
+        }
+
+        // No agent or fullBackupOnly="true" means we do indeed perform full-data backups for it
+        return true;
+    }
+
+    /**
+     * Returns whether the app is only capable of doing key/value. We say it's not if it allows full
+     * backup, and it is otherwise.
+     */
+    public static boolean appIsKeyValueOnly(PackageInfo pkg) {
+        return !appGetsFullBackup(pkg);
+    }
+
+    /**
+     * Returns whether the signatures stored {@param storedSigs}, coming from the source apk, match
+     * the signatures of the apk installed on the device, the target apk. If the target resides in
+     * the system partition we return true. Otherwise it's considered a match if both conditions
+     * hold:
+     *
+     * <ul>
+     *   <li>Source and target have at least one signature each
+     *   <li>Target contains all signatures in source, and nothing more
+     * </ul>
+     *
+     * or if both source and target have exactly one signature, and they don't match, we check
+     * if the app was ever signed with source signature (i.e. app has rotated key)
+     * Note: key rotation is only supported for apps ever signed with one key, and those apps will
+     * not be allowed to be signed by more certificates in the future
+     *
+     * Note that if {@param target} is null we return false.
+     */
+    public static boolean signaturesMatch(Signature[] storedSigs, PackageInfo target,
+            PackageManagerInternal pmi) {
+        if (target == null || target.packageName == null) {
+            return false;
+        }
+
+        // If the target resides on the system partition, we allow it to restore
+        // data from the like-named package in a restore set even if the signatures
+        // do not match.  (Unlike general applications, those flashed to the system
+        // partition will be signed with the device's platform certificate, so on
+        // different phones the same system app will have different signatures.)
+        if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "System app " + target.packageName + " - skipping sig check");
+            }
+            return true;
+        }
+
+        // Don't allow unsigned apps on either end
+        if (ArrayUtils.isEmpty(storedSigs)) {
+            return false;
+        }
+
+        SigningInfo signingInfo = target.signingInfo;
+        if (signingInfo == null) {
+            Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
+                    " PackageManager#GET_SIGNING_CERTIFICATES was not specified");
+            return false;
+        }
+
+        if (DEBUG) {
+            Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device="
+                    + signingInfo.getApkContentsSigners());
+        }
+
+        final int nStored = storedSigs.length;
+        if (nStored == 1) {
+            // if the app is only signed with one sig, it's possible it has rotated its key
+            // (the checks with signing history are delegated to PackageManager)
+            // TODO(b/73988180): address the case that app has declared restoreAnyVersion and is
+            // restoring from higher version to lower after having rotated the key (i.e. higher
+            // version has different sig than lower version that we want to restore to)
+            return pmi.isDataRestoreSafe(storedSigs[0], target.packageName);
+        } else {
+            // the app couldn't have rotated keys, since it was signed with multiple sigs - do
+            // a check to see if we find a match for all stored sigs
+            // since app hasn't rotated key, we only need to check with its current signers
+            Signature[] deviceSigs = signingInfo.getApkContentsSigners();
+            int nDevice = deviceSigs.length;
+
+            // ensure that each stored sig matches an on-device sig
+            for (int i = 0; i < nStored; i++) {
+                boolean match = false;
+                for (int j = 0; j < nDevice; j++) {
+                    if (storedSigs[i].equals(deviceSigs[j])) {
+                        match = true;
+                        break;
+                    }
+                }
+                if (!match) {
+                    return false;
+                }
+            }
+            // we have found a match for all stored sigs
+            return true;
+        }
+    }
+}
diff --git a/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/com/android/server/backup/utils/BackupManagerMonitorUtils.java
new file mode 100644
index 0000000..6f08376
--- /dev/null
+++ b/com/android/server/backup/utils/BackupManagerMonitorUtils.java
@@ -0,0 +1,132 @@
+/*
+ * 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.utils;
+
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.IBackupManagerMonitor;
+import android.content.pm.PackageInfo;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Slog;
+
+/**
+ * Utility methods to communicate with BackupManagerMonitor.
+ */
+public class BackupManagerMonitorUtils {
+    /**
+     * Notifies monitor about the event.
+     *
+     * Calls {@link IBackupManagerMonitor#onEvent(Bundle)} with a bundle representing current event.
+     *
+     * @param monitor - implementation of {@link IBackupManagerMonitor} to notify.
+     * @param id - event id.
+     * @param pkg - package event is related to.
+     * @param category - event category.
+     * @param extras - additional event data.
+     * @return <code>monitor</code> if call succeeded and <code>null</code> otherwise.
+     */
+    @Nullable
+    public static IBackupManagerMonitor monitorEvent(
+            @Nullable IBackupManagerMonitor monitor,
+            int id,
+            PackageInfo pkg,
+            int category,
+            Bundle extras) {
+        if (monitor != null) {
+            try {
+                Bundle bundle = new Bundle();
+                bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
+                bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category);
+                if (pkg != null) {
+                    bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME,
+                            pkg.packageName);
+                    bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION,
+                            pkg.versionCode);
+                    bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION,
+                            pkg.getLongVersionCode());
+                }
+                if (extras != null) {
+                    bundle.putAll(extras);
+                }
+                monitor.onEvent(bundle);
+                return monitor;
+            } catch (RemoteException e) {
+                if (DEBUG) {
+                    Slog.w(TAG, "backup manager monitor went away");
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds given key-value pair in the bundle and returns the bundle. If bundle was null it will
+     * be created.
+     *
+     * @param extras - bundle where to add key-value to, if null a new bundle will be created.
+     * @param key - key.
+     * @param value - value.
+     * @return extras if it was not null and new bundle otherwise.
+     */
+    public static Bundle putMonitoringExtra(Bundle extras, String key, String value) {
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        extras.putString(key, value);
+        return extras;
+    }
+
+    /**
+     * Adds given key-value pair in the bundle and returns the bundle. If bundle was null it will
+     * be created.
+     *
+     * @param extras - bundle where to add key-value to, if null a new bundle will be created.
+     * @param key - key.
+     * @param value - value.
+     * @return extras if it was not null and new bundle otherwise.
+     */
+    public static Bundle putMonitoringExtra(Bundle extras, String key, long value) {
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        extras.putLong(key, value);
+        return extras;
+    }
+
+    /**
+     * Adds given key-value pair in the bundle and returns the bundle. If bundle was null it will
+     * be created.
+     *
+     * @param extras - bundle where to add key-value to, if null a new bundle will be created.
+     * @param key - key.
+     * @param value - value.
+     * @return extras if it was not null and new bundle otherwise.
+     */
+    public static Bundle putMonitoringExtra(Bundle extras, String key, boolean value) {
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        extras.putBoolean(key, value);
+        return extras;
+    }
+}
diff --git a/com/android/server/backup/utils/BackupObserverUtils.java b/com/android/server/backup/utils/BackupObserverUtils.java
new file mode 100644
index 0000000..c0cf2ef
--- /dev/null
+++ b/com/android/server/backup/utils/BackupObserverUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.app.backup.BackupProgress;
+import android.app.backup.IBackupObserver;
+import android.os.RemoteException;
+import android.util.Slog;
+
+/**
+ * Utility methods to communicate with BackupObserver.
+ */
+public class BackupObserverUtils {
+    /**
+     * Wraps {@link IBackupObserver#onUpdate(String, BackupProgress)} to handle RemoteException,
+     * so that the caller doesn't have to.
+     */
+    public static void sendBackupOnUpdate(IBackupObserver observer, String packageName,
+            BackupProgress progress) {
+        if (observer != null) {
+            try {
+                observer.onUpdate(packageName, progress);
+            } catch (RemoteException e) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Backup observer went away: onUpdate");
+                }
+            }
+        }
+    }
+
+    /**
+     * Wraps {@link IBackupObserver#onResult(String, int)} to handle RemoteException, so that the
+     * caller doesn't have to.
+     */
+    public static void sendBackupOnPackageResult(IBackupObserver observer, String packageName,
+            int status) {
+        if (observer != null) {
+            try {
+                observer.onResult(packageName, status);
+            } catch (RemoteException e) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Backup observer went away: onResult");
+                }
+            }
+        }
+    }
+
+    /**
+     * Wraps {@link IBackupObserver#backupFinished(int)} to handle RemoteException, so that the
+     * caller doesn't have to.
+     */
+    public static void sendBackupFinished(IBackupObserver observer, int status) {
+        if (observer != null) {
+            try {
+                observer.backupFinished(status);
+            } catch (RemoteException e) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Backup observer went away: backupFinished");
+                }
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/utils/BytesReadListener.java b/com/android/server/backup/utils/BytesReadListener.java
new file mode 100644
index 0000000..34d85ff
--- /dev/null
+++ b/com/android/server/backup/utils/BytesReadListener.java
@@ -0,0 +1,28 @@
+/*
+ * 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.utils;
+
+/**
+ * Listener for bytes reading.
+ */
+public interface BytesReadListener {
+    /**
+     * Will be called on each read operation.
+     * @param bytesRead - number of bytes read with the most recent read operation.
+     */
+    void onBytesRead(long bytesRead);
+}
diff --git a/com/android/server/backup/utils/DataStreamCodec.java b/com/android/server/backup/utils/DataStreamCodec.java
new file mode 100644
index 0000000..b1e226d
--- /dev/null
+++ b/com/android/server/backup/utils/DataStreamCodec.java
@@ -0,0 +1,40 @@
+/*
+ * 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.utils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Implements how to serialize a {@code T} to a {@link DataOutputStream} and how to deserialize a
+ * {@code T} from a {@link DataInputStream}.
+ *
+ * @param <T> Type of object to be serialized / deserialized.
+ */
+public interface DataStreamCodec<T> {
+    /**
+     * Serializes {@code t} to {@code dataOutputStream}.
+     */
+    void serialize(T t, DataOutputStream dataOutputStream) throws IOException;
+
+    /**
+     * Deserializes {@code t} from {@code dataInputStream}.
+     */
+    T deserialize(DataInputStream dataInputStream) throws IOException;
+}
+
diff --git a/com/android/server/backup/utils/DataStreamFileCodec.java b/com/android/server/backup/utils/DataStreamFileCodec.java
new file mode 100644
index 0000000..7753b03
--- /dev/null
+++ b/com/android/server/backup/utils/DataStreamFileCodec.java
@@ -0,0 +1,78 @@
+/*
+ * 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.utils;
+
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Provides an interface for serializing an object to a file and deserializing it back again.
+ *
+ * <p>Serialization logic is implemented as a {@link DataStreamCodec}.
+ *
+ * @param <T> The type of object to serialize / deserialize.
+ */
+public final class DataStreamFileCodec<T> {
+    private final File mFile;
+    private final DataStreamCodec<T> mCodec;
+
+    /**
+     * Constructs an instance to serialize to or deserialize from the given file, with the given
+     * serialization / deserialization strategy.
+     */
+    public DataStreamFileCodec(File file, DataStreamCodec<T> codec) {
+        mFile = file;
+        mCodec = codec;
+    }
+
+    /**
+     * Deserializes a {@code T} from the file, automatically closing input streams.
+     *
+     * @return The deserialized object.
+     * @throws IOException if an IO error occurred.
+     */
+    public T deserialize() throws IOException {
+        try (
+            FileInputStream fileInputStream = new FileInputStream(mFile);
+            DataInputStream dataInputStream = new DataInputStream(fileInputStream)
+        ) {
+            return mCodec.deserialize(dataInputStream);
+        }
+    }
+
+    /**
+     * Serializes {@code t} to the file, automatically flushing and closing output streams.
+     *
+     * @param t The object to serialize.
+     * @throws IOException if an IO error occurs.
+     */
+    public void serialize(T t) throws IOException {
+        try (
+            FileOutputStream fileOutputStream = new FileOutputStream(mFile);
+            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+            DataOutputStream dataOutputStream = new DataOutputStream(bufferedOutputStream)
+        ) {
+            mCodec.serialize(t, dataOutputStream);
+            dataOutputStream.flush();
+        }
+    }
+}
diff --git a/com/android/server/backup/utils/FileUtils.java b/com/android/server/backup/utils/FileUtils.java
new file mode 100644
index 0000000..00686cb
--- /dev/null
+++ b/com/android/server/backup/utils/FileUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 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.utils;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.util.Slog;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Utility methods useful for working with backup related files. */
+public final class FileUtils {
+    /**
+     * Ensure that the file exists in the file system. If an IOException is thrown, it is ignored.
+     * This method is useful to avoid code duplication of the "try-catch-ignore exception" block.
+     */
+    public static File createNewFile(File file) {
+        try {
+            file.createNewFile();
+        } catch (IOException e) {
+            Slog.w(TAG, "Failed to create file:" + file.getAbsolutePath(), e);
+        }
+        return file;
+    }
+}
diff --git a/com/android/server/backup/utils/FullBackupRestoreObserverUtils.java b/com/android/server/backup/utils/FullBackupRestoreObserverUtils.java
new file mode 100644
index 0000000..fa856ce
--- /dev/null
+++ b/com/android/server/backup/utils/FullBackupRestoreObserverUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.app.backup.IFullBackupRestoreObserver;
+import android.os.RemoteException;
+import android.util.Slog;
+
+/**
+ * Utility methods to communicate with FullBackupRestoreObserver.
+ */
+public class FullBackupRestoreObserverUtils {
+    /**
+     * Wraps {@link IFullBackupRestoreObserver#onStartRestore()} to handle RemoteException, so that
+     * the caller doesn't have to.
+     *
+     * @param observer - IFullBackupRestoreObserver to communicate with.
+     * @return observer if the call worked and null if there was a communication problem.
+     */
+    public static IFullBackupRestoreObserver sendStartRestore(IFullBackupRestoreObserver observer) {
+        if (observer != null) {
+            try {
+                observer.onStartRestore();
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full restore observer went away: startRestore");
+                observer = null;
+            }
+        }
+        return observer;
+    }
+
+    /**
+     * Wraps {@link IFullBackupRestoreObserver#onRestorePackage(String)} to handle RemoteException,
+     * so that the caller doesn't have to.
+     *
+     * @param observer - IFullBackupRestoreObserver to communicate with.
+     * @param name - package name.
+     * @return observer if the call worked and null if there was a communication problem.
+     */
+    public static IFullBackupRestoreObserver sendOnRestorePackage(
+            IFullBackupRestoreObserver observer, String name) {
+        if (observer != null) {
+            try {
+                // TODO: use a more user-friendly name string
+                observer.onRestorePackage(name);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full restore observer went away: restorePackage");
+                observer = null;
+            }
+        }
+        return observer;
+    }
+
+    /**
+     * Wraps {@link IFullBackupRestoreObserver#onEndRestore()} ()} to handle RemoteException, so
+     * that the caller doesn't have to.
+     *
+     * @param observer - IFullBackupRestoreObserver to communicate with.
+     * @return observer if the call worked and null if there was a communication problem.
+     */
+    public static IFullBackupRestoreObserver sendEndRestore(IFullBackupRestoreObserver observer) {
+        if (observer != null) {
+            try {
+                observer.onEndRestore();
+            } catch (RemoteException e) {
+                Slog.w(TAG, "full restore observer went away: endRestore");
+                observer = null;
+            }
+        }
+        return observer;
+    }
+}
diff --git a/com/android/server/backup/utils/FullBackupUtils.java b/com/android/server/backup/utils/FullBackupUtils.java
new file mode 100644
index 0000000..dbe3cd9
--- /dev/null
+++ b/com/android/server/backup/utils/FullBackupUtils.java
@@ -0,0 +1,62 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.os.ParcelFileDescriptor;
+import android.util.Slog;
+
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Low-level utility methods for full backup.
+ */
+public class FullBackupUtils {
+    /**
+     * Reads data from pipe and writes it to the stream in chunks of up to 32KB.
+     *
+     * @param inPipe - pipe to read the data from.
+     * @param out - stream to write the data to.
+     * @throws IOException - in case of an error.
+     */
+    public static void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out)
+            throws IOException {
+        // We do not take close() responsibility for the pipe FD
+        FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
+        DataInputStream in = new DataInputStream(raw);
+
+        byte[] buffer = new byte[32 * 1024];
+        int chunkTotal;
+        while ((chunkTotal = in.readInt()) > 0) {
+            while (chunkTotal > 0) {
+                int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal;
+                int nRead = in.read(buffer, 0, toRead);
+                if (nRead < 0) {
+                    Slog.e(TAG, "Unexpectedly reached end of file while reading data");
+                    throw new EOFException();
+                }
+                out.write(buffer, 0, nRead);
+                chunkTotal -= nRead;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/utils/PasswordUtils.java b/com/android/server/backup/utils/PasswordUtils.java
new file mode 100644
index 0000000..0a7e551
--- /dev/null
+++ b/com/android/server/backup/utils/PasswordUtils.java
@@ -0,0 +1,133 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.util.Slog;
+
+import libcore.util.HexEncoding;
+
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+/**
+ * Passwords related utility methods.
+ */
+public class PasswordUtils {
+    // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys
+    public static final int PBKDF2_HASH_ROUNDS = 10000;
+    private static final int PBKDF2_KEY_SIZE = 256;     // bits
+    public static final int PBKDF2_SALT_SIZE = 512;    // bits
+    public static final String ENCRYPTION_ALGORITHM_NAME = "AES-256";
+
+    /**
+     * Creates {@link SecretKey} instance from given parameters.
+     *
+     * @param algorithm - key generation algorithm.
+     * @param pw - password.
+     * @param salt - salt.
+     * @param rounds - number of rounds to run in key generation.
+     * @return {@link SecretKey} instance or null in case of an error.
+     */
+    public static SecretKey buildPasswordKey(String algorithm, String pw, byte[] salt, int rounds) {
+        return buildCharArrayKey(algorithm, pw.toCharArray(), salt, rounds);
+    }
+
+    /**
+     * Generates {@link SecretKey} instance from given parameters and returns it's hex
+     * representation.
+     *
+     * @param algorithm - key generation algorithm.
+     * @param pw - password.
+     * @param salt - salt.
+     * @param rounds - number of rounds to run in key generation.
+     * @return Hex representation of the generated key, or null if generation failed.
+     */
+    public static String buildPasswordHash(String algorithm, String pw, byte[] salt, int rounds) {
+        SecretKey key = buildPasswordKey(algorithm, pw, salt, rounds);
+        if (key != null) {
+            return byteArrayToHex(key.getEncoded());
+        }
+        return null;
+    }
+
+    /**
+     * Creates hex string representation of the byte array.
+     */
+    public static String byteArrayToHex(byte[] data) {
+        return HexEncoding.encodeToString(data, true);
+    }
+
+    /**
+     * Creates byte array from it's hex string representation.
+     */
+    public static byte[] hexToByteArray(String digits) {
+        final int bytes = digits.length() / 2;
+        if (2 * bytes != digits.length()) {
+            throw new IllegalArgumentException("Hex string must have an even number of digits");
+        }
+
+        byte[] result = new byte[bytes];
+        for (int i = 0; i < digits.length(); i += 2) {
+            result[i / 2] = (byte) Integer.parseInt(digits.substring(i, i + 2), 16);
+        }
+        return result;
+    }
+
+    /**
+     * Generates {@link SecretKey} instance from given parameters and returns it's checksum.
+     *
+     * Current implementation returns the key in its primary encoding format.
+     *
+     * @param algorithm - key generation algorithm.
+     * @param pwBytes - password.
+     * @param salt - salt.
+     * @param rounds - number of rounds to run in key generation.
+     * @return Hex representation of the generated key, or null if generation failed.
+     */
+    public static byte[] makeKeyChecksum(String algorithm, byte[] pwBytes, byte[] salt,
+            int rounds) {
+        char[] mkAsChar = new char[pwBytes.length];
+        for (int i = 0; i < pwBytes.length; i++) {
+            mkAsChar[i] = (char) pwBytes[i];
+        }
+
+        Key checksum = buildCharArrayKey(algorithm, mkAsChar, salt, rounds);
+        return checksum.getEncoded();
+    }
+
+    private static SecretKey buildCharArrayKey(String algorithm, char[] pwArray, byte[] salt,
+            int rounds) {
+        try {
+            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
+            KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
+            return keyFactory.generateSecret(ks);
+        } catch (InvalidKeySpecException e) {
+            Slog.e(TAG, "Invalid key spec for PBKDF2!");
+        } catch (NoSuchAlgorithmException e) {
+            Slog.e(TAG, "PBKDF2 unavailable!");
+        }
+        return null;
+    }
+}
diff --git a/com/android/server/backup/utils/RandomAccessFileUtils.java b/com/android/server/backup/utils/RandomAccessFileUtils.java
new file mode 100644
index 0000000..abf906a
--- /dev/null
+++ b/com/android/server/backup/utils/RandomAccessFileUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 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.utils;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.util.Slog;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/** Utility methods useful for working with backup related RandomAccessFiles. */
+public final class RandomAccessFileUtils {
+    private static RandomAccessFile getRandomAccessFile(File file) throws FileNotFoundException {
+        return new RandomAccessFile(file, "rwd");
+    }
+
+    /** Write a boolean to a File by wrapping it using a RandomAccessFile. */
+    public static void writeBoolean(File file, boolean b) {
+        try (RandomAccessFile af = getRandomAccessFile(file)) {
+            af.writeBoolean(b);
+        } catch (IOException e) {
+            Slog.w(TAG, "Error writing file:" + file.getAbsolutePath(), e);
+        }
+    }
+
+    /** Read a boolean from a File by wrapping it using a RandomAccessFile. */
+    public static boolean readBoolean(File file, boolean def) {
+        try (RandomAccessFile af = getRandomAccessFile(file)) {
+            return af.readBoolean();
+        } catch (IOException e) {
+            Slog.w(TAG, "Error reading file:" + file.getAbsolutePath(), e);
+        }
+        return def;
+    }
+}
diff --git a/com/android/server/backup/utils/RestoreUtils.java b/com/android/server/backup/utils/RestoreUtils.java
new file mode 100644
index 0000000..97bde9c
--- /dev/null
+++ b/com/android/server/backup/utils/RestoreUtils.java
@@ -0,0 +1,238 @@
+/*
+ * 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.utils;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.Session;
+import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.backup.FileMetadata;
+import com.android.server.backup.restore.RestoreDeleteObserver;
+import com.android.server.backup.restore.RestorePolicy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+
+/**
+ * Utility methods used by {@link com.android.server.backup.restore.PerformAdbRestoreTask} and
+ * {@link com.android.server.backup.restore.FullRestoreEngine}.
+ */
+public class RestoreUtils {
+    /**
+     * Reads apk contents from input stream and installs the apk.
+     *
+     * @param instream - input stream to read apk data from.
+     * @param context - installing context
+     * @param deleteObserver - {@link RestoreDeleteObserver} instance.
+     * @param manifestSignatures - manifest signatures.
+     * @param packagePolicies - package policies.
+     * @param info - backup file info.
+     * @param installerPackageName - package name of installer.
+     * @param bytesReadListener - listener to be called for counting bytes read.
+     * @return true if apk was successfully read and installed and false otherwise.
+     */
+    // TODO: Refactor to get rid of unneeded params.
+    public static boolean installApk(InputStream instream, Context context,
+            RestoreDeleteObserver deleteObserver,
+            HashMap<String, Signature[]> manifestSignatures,
+            HashMap<String, RestorePolicy> packagePolicies,
+            FileMetadata info,
+            String installerPackageName,
+            BytesReadListener bytesReadListener,
+            int userId) {
+        boolean okay = true;
+
+        if (DEBUG) {
+            Slog.d(TAG, "Installing from backup: " + info.packageName);
+        }
+
+        try {
+            LocalIntentReceiver receiver = new LocalIntentReceiver();
+            PackageManager packageManager = context.getPackageManager();
+            PackageInstaller installer = packageManager.getPackageInstaller();
+
+            SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+            params.setInstallerPackageName(installerPackageName);
+            int sessionId = installer.createSession(params);
+            try {
+                try (Session session = installer.openSession(sessionId)) {
+                    try (OutputStream apkStream = session.openWrite(info.packageName, 0,
+                            info.size)) {
+                        byte[] buffer = new byte[32 * 1024];
+                        long size = info.size;
+                        while (size > 0) {
+                            long toRead = (buffer.length < size) ? buffer.length : size;
+                            int didRead = instream.read(buffer, 0, (int) toRead);
+                            if (didRead >= 0) {
+                                bytesReadListener.onBytesRead(didRead);
+                            }
+                            apkStream.write(buffer, 0, didRead);
+                            size -= didRead;
+                        }
+                    }
+
+                    // Installation is current disabled
+                    session.abandon();
+                    // session.commit(receiver.getIntentSender());
+                }
+            } catch (Exception t) {
+                installer.abandonSession(sessionId);
+
+                throw t;
+            }
+
+            // Installation is current disabled
+            Intent result = null;
+            // Intent result = receiver.getResult();
+
+            // Installation is current disabled
+            int status = PackageInstaller.STATUS_FAILURE;
+            // int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+            //        PackageInstaller.STATUS_FAILURE);
+
+            if (status != PackageInstaller.STATUS_SUCCESS) {
+                // The only time we continue to accept install of data even if the
+                // apk install failed is if we had already determined that we could
+                // accept the data regardless.
+                if (packagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) {
+                    okay = false;
+                }
+            } else {
+                // Okay, the install succeeded.  Make sure it was the right app.
+                boolean uninstall = false;
+                final String installedPackageName = result.getStringExtra(
+                        PackageInstaller.EXTRA_PACKAGE_NAME);
+                if (!installedPackageName.equals(info.packageName)) {
+                    Slog.w(TAG, "Restore stream claimed to include apk for "
+                            + info.packageName + " but apk was really "
+                            + installedPackageName);
+                    // delete the package we just put in place; it might be fraudulent
+                    okay = false;
+                    uninstall = true;
+                } else {
+                    try {
+                        PackageInfo pkg = packageManager.getPackageInfoAsUser(info.packageName,
+                                PackageManager.GET_SIGNING_CERTIFICATES, userId);
+                        if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP)
+                                == 0) {
+                            Slog.w(TAG, "Restore stream contains apk of package "
+                                    + info.packageName
+                                    + " but it disallows backup/restore");
+                            okay = false;
+                        } else {
+                            // So far so good -- do the signatures match the manifest?
+                            Signature[] sigs = manifestSignatures.get(info.packageName);
+                            PackageManagerInternal pmi = LocalServices.getService(
+                                    PackageManagerInternal.class);
+                            if (AppBackupUtils.signaturesMatch(sigs, pkg, pmi)) {
+                                // If this is a system-uid app without a declared backup agent,
+                                // don't restore any of the file data.
+                                if (UserHandle.isCore(pkg.applicationInfo.uid)
+                                        && (pkg.applicationInfo.backupAgentName == null)) {
+                                    Slog.w(TAG, "Installed app " + info.packageName
+                                            + " has restricted uid and no agent");
+                                    okay = false;
+                                }
+                            } else {
+                                Slog.w(TAG, "Installed app " + info.packageName
+                                        + " signatures do not match restore manifest");
+                                okay = false;
+                                uninstall = true;
+                            }
+                        }
+                    } catch (PackageManager.NameNotFoundException e) {
+                        Slog.w(TAG, "Install of package " + info.packageName
+                                + " succeeded but now not found");
+                        okay = false;
+                    }
+                }
+
+                // If we're not okay at this point, we need to delete the package
+                // that we just installed.
+                if (uninstall) {
+                    deleteObserver.reset();
+                    packageManager.deletePackage(
+                            installedPackageName,
+                            deleteObserver, 0);
+                    deleteObserver.waitForCompletion();
+                }
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to transcribe restored apk for install");
+            okay = false;
+        }
+
+        return okay;
+    }
+
+    private static class LocalIntentReceiver {
+        private final Object mLock = new Object();
+
+        @GuardedBy("mLock")
+        private Intent mResult = null;
+
+        private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
+            @Override
+            public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
+                    IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+                synchronized (mLock) {
+                    mResult = intent;
+                    mLock.notifyAll();
+                }
+            }
+        };
+
+        public IntentSender getIntentSender() {
+            return new IntentSender((IIntentSender) mLocalSender);
+        }
+
+        public Intent getResult() {
+            synchronized (mLock) {
+                while (mResult == null) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException e) {
+                        // ignored
+                    }
+                }
+
+                return mResult;
+            }
+        }
+    }
+}
diff --git a/com/android/server/backup/utils/SparseArrayUtils.java b/com/android/server/backup/utils/SparseArrayUtils.java
new file mode 100644
index 0000000..954d714
--- /dev/null
+++ b/com/android/server/backup/utils/SparseArrayUtils.java
@@ -0,0 +1,49 @@
+/*
+ * 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.utils;
+
+import android.util.SparseArray;
+
+import java.util.HashSet;
+
+/**
+ * Helper functions for manipulating instances of {@link SparseArray}.
+ */
+public final class SparseArrayUtils {
+    // Statics only
+    private SparseArrayUtils() {}
+
+    /**
+     * Given a {@link SparseArray<HashSet>}, returns a new {@link HashSet} containing every element
+     * from every set in the array.
+     *
+     * @param sets The array of sets from which to take the union.
+     * @param <V> The type of element contained in the set.
+     * @return The complete set.
+     */
+    public static<V> HashSet<V> union(SparseArray<HashSet<V>> sets) {
+        HashSet<V> unionSet = new HashSet<>();
+        int n = sets.size();
+        for (int i = 0; i < n; i++) {
+            HashSet<V> ithSet = sets.valueAt(i);
+            if (ithSet != null) {
+                unionSet.addAll(ithSet);
+            }
+        }
+        return unionSet;
+    }
+}
diff --git a/com/android/server/backup/utils/TarBackupReader.java b/com/android/server/backup/utils/TarBackupReader.java
new file mode 100644
index 0000000..d2d382d
--- /dev/null
+++ b/com/android/server/backup/utils/TarBackupReader.java
@@ -0,0 +1,800 @@
+/*
+ * 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.utils;
+
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION;
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_MANIFEST_PACKAGE_NAME;
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_OLD_VERSION;
+import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_POLICY_ALLOW_APKS;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_APK_NOT_INSTALLED;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;
+
+import static com.android.server.backup.BackupManagerService.DEBUG;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
+import static com.android.server.backup.UserBackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
+import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.FullBackup;
+import android.app.backup.IBackupManagerMonitor;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.server.backup.FileMetadata;
+import com.android.server.backup.restore.RestorePolicy;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Utility methods to read backup tar file.
+ */
+public class TarBackupReader {
+    private static final int TAR_HEADER_OFFSET_TYPE_CHAR = 156;
+    private static final int TAR_HEADER_LENGTH_PATH = 100;
+    private static final int TAR_HEADER_OFFSET_PATH = 0;
+    private static final int TAR_HEADER_LENGTH_PATH_PREFIX = 155;
+    private static final int TAR_HEADER_OFFSET_PATH_PREFIX = 345;
+    private static final int TAR_HEADER_LENGTH_MODE = 8;
+    private static final int TAR_HEADER_OFFSET_MODE = 100;
+    private static final int TAR_HEADER_LENGTH_MODTIME = 12;
+    private static final int TAR_HEADER_OFFSET_MODTIME = 136;
+    private static final int TAR_HEADER_LENGTH_FILESIZE = 12;
+    private static final int TAR_HEADER_OFFSET_FILESIZE = 124;
+    private static final int TAR_HEADER_LONG_RADIX = 8;
+
+    private final InputStream mInputStream;
+    private final BytesReadListener mBytesReadListener;
+
+    private IBackupManagerMonitor mMonitor;
+
+    // Widget blob to be restored out-of-band.
+    private byte[] mWidgetData = null;
+
+    public TarBackupReader(InputStream inputStream, BytesReadListener bytesReadListener,
+            IBackupManagerMonitor monitor) {
+        mInputStream = inputStream;
+        mBytesReadListener = bytesReadListener;
+        mMonitor = monitor;
+    }
+
+    /**
+     * Consumes a tar file header block [sequence] and accumulates the relevant metadata.
+     */
+    public FileMetadata readTarHeaders() throws IOException {
+        byte[] block = new byte[512];
+        FileMetadata info = null;
+
+        boolean gotHeader = readTarHeader(block);
+        if (gotHeader) {
+            try {
+                // okay, presume we're okay, and extract the various metadata
+                info = new FileMetadata();
+                info.size = extractRadix(block,
+                        TAR_HEADER_OFFSET_FILESIZE,
+                        TAR_HEADER_LENGTH_FILESIZE,
+                        TAR_HEADER_LONG_RADIX);
+                info.mtime = extractRadix(block,
+                        TAR_HEADER_OFFSET_MODTIME,
+                        TAR_HEADER_LENGTH_MODTIME,
+                        TAR_HEADER_LONG_RADIX);
+                info.mode = extractRadix(block,
+                        TAR_HEADER_OFFSET_MODE,
+                        TAR_HEADER_LENGTH_MODE,
+                        TAR_HEADER_LONG_RADIX);
+
+                info.path = extractString(block,
+                        TAR_HEADER_OFFSET_PATH_PREFIX,
+                        TAR_HEADER_LENGTH_PATH_PREFIX);
+                String path = extractString(block,
+                        TAR_HEADER_OFFSET_PATH,
+                        TAR_HEADER_LENGTH_PATH);
+                if (path.length() > 0) {
+                    if (info.path.length() > 0) {
+                        info.path += '/';
+                    }
+                    info.path += path;
+                }
+
+                // tar link indicator field: 1 byte at offset 156 in the header.
+                int typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
+                if (typeChar == 'x') {
+                    // pax extended header, so we need to read that
+                    gotHeader = readPaxExtendedHeader(info);
+                    if (gotHeader) {
+                        // and after a pax extended header comes another real header -- read
+                        // that to find the real file type
+                        gotHeader = readTarHeader(block);
+                    }
+                    if (!gotHeader) {
+                        throw new IOException("Bad or missing pax header");
+                    }
+
+                    typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
+                }
+
+                switch (typeChar) {
+                    case '0':
+                        info.type = BackupAgent.TYPE_FILE;
+                        break;
+                    case '5': {
+                        info.type = BackupAgent.TYPE_DIRECTORY;
+                        if (info.size != 0) {
+                            Slog.w(TAG, "Directory entry with nonzero size in header");
+                            info.size = 0;
+                        }
+                        break;
+                    }
+                    case 0: {
+                        // presume EOF
+                        if (MORE_DEBUG) {
+                            Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
+                        }
+                        return null;
+                    }
+                    default: {
+                        Slog.e(TAG, "Unknown tar entity type: " + typeChar);
+                        throw new IOException("Unknown entity type " + typeChar);
+                    }
+                }
+
+                // Parse out the path
+                //
+                // first: apps/shared/unrecognized
+                if (FullBackup.SHARED_PREFIX.regionMatches(0,
+                        info.path, 0, FullBackup.SHARED_PREFIX.length())) {
+                    // File in shared storage.  !!! TODO: implement this.
+                    info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
+                    info.packageName = SHARED_BACKUP_AGENT_PACKAGE;
+                    info.domain = FullBackup.SHARED_STORAGE_TOKEN;
+                    if (DEBUG) {
+                        Slog.i(TAG, "File in shared storage: " + info.path);
+                    }
+                } else if (FullBackup.APPS_PREFIX.regionMatches(0,
+                        info.path, 0, FullBackup.APPS_PREFIX.length())) {
+                    // App content!  Parse out the package name and domain
+
+                    // strip the apps/ prefix
+                    info.path = info.path.substring(FullBackup.APPS_PREFIX.length());
+
+                    // extract the package name
+                    int slash = info.path.indexOf('/');
+                    if (slash < 0) {
+                        throw new IOException("Illegal semantic path in " + info.path);
+                    }
+                    info.packageName = info.path.substring(0, slash);
+                    info.path = info.path.substring(slash + 1);
+
+                    // if it's a manifest or metadata payload we're done, otherwise parse
+                    // out the domain into which the file will be restored
+                    if (!info.path.equals(BACKUP_MANIFEST_FILENAME) &&
+                            !info.path.equals(BACKUP_METADATA_FILENAME)) {
+                        slash = info.path.indexOf('/');
+                        if (slash < 0) {
+                            throw new IOException("Illegal semantic path in non-manifest "
+                                    + info.path);
+                        }
+                        info.domain = info.path.substring(0, slash);
+                        info.path = info.path.substring(slash + 1);
+                    }
+                }
+            } catch (IOException e) {
+                if (DEBUG) {
+                    Slog.e(TAG, "Parse error in header: " + e.getMessage());
+                    if (MORE_DEBUG) {
+                        hexLog(block);
+                    }
+                }
+                throw e;
+            }
+        }
+        return info;
+    }
+
+    /**
+     * Tries to read exactly the given number of bytes into a buffer at the stated offset.
+     *
+     * @param in - input stream to read bytes from..
+     * @param buffer - where to write bytes to.
+     * @param offset - offset in buffer to write bytes to.
+     * @param size - number of bytes to read.
+     * @return number of bytes actually read.
+     * @throws IOException in case of an error.
+     */
+    private static int readExactly(InputStream in, byte[] buffer, int offset, int size)
+            throws IOException {
+        if (size <= 0) {
+            throw new IllegalArgumentException("size must be > 0");
+        }
+        if (MORE_DEBUG) {
+            Slog.i(TAG, "  ... readExactly(" + size + ") called");
+        }
+        int soFar = 0;
+        while (soFar < size) {
+            int nRead = in.read(buffer, offset + soFar, size - soFar);
+            if (nRead <= 0) {
+                if (MORE_DEBUG) {
+                    Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar);
+                }
+                break;
+            }
+            soFar += nRead;
+            if (MORE_DEBUG) {
+                Slog.v(TAG, "   + got " + nRead + "; now wanting " + (size - soFar));
+            }
+        }
+        return soFar;
+    }
+
+    /**
+     * Reads app manifest, filling version and hasApk fields in the metadata, and returns array of
+     * signatures.
+     *
+     * @param info - file metadata.
+     * @return array of signatures or null, in case of an error.
+     * @throws IOException in case of an error.
+     */
+    public Signature[] readAppManifestAndReturnSignatures(FileMetadata info)
+            throws IOException {
+        // Fail on suspiciously large manifest files
+        if (info.size > 64 * 1024) {
+            throw new IOException("Restore manifest too big; corrupt? size=" + info.size);
+        }
+
+        byte[] buffer = new byte[(int) info.size];
+        if (MORE_DEBUG) {
+            Slog.i(TAG,
+                    "   readAppManifestAndReturnSignatures() looking for " + info.size + " bytes");
+        }
+        if (readExactly(mInputStream, buffer, 0, (int) info.size) == info.size) {
+            mBytesReadListener.onBytesRead(info.size);
+        } else {
+            throw new IOException("Unexpected EOF in manifest");
+        }
+
+        String[] str = new String[1];
+        int offset = 0;
+
+        try {
+            offset = extractLine(buffer, offset, str);
+            int version = Integer.parseInt(str[0]);
+            if (version == BACKUP_MANIFEST_VERSION) {
+                offset = extractLine(buffer, offset, str);
+                String manifestPackage = str[0];
+                // TODO: handle <original-package>
+                if (manifestPackage.equals(info.packageName)) {
+                    offset = extractLine(buffer, offset, str);
+                    info.version = Integer.parseInt(str[0]);  // app version
+                    offset = extractLine(buffer, offset, str);
+                    // This is the platform version, which we don't use, but we parse it
+                    // as a safety against corruption in the manifest.
+                    Integer.parseInt(str[0]);
+                    offset = extractLine(buffer, offset, str);
+                    info.installerPackageName = (str[0].length() > 0) ? str[0] : null;
+                    offset = extractLine(buffer, offset, str);
+                    info.hasApk = str[0].equals("1");
+                    offset = extractLine(buffer, offset, str);
+                    int numSigs = Integer.parseInt(str[0]);
+                    if (numSigs > 0) {
+                        Signature[] sigs = new Signature[numSigs];
+                        for (int i = 0; i < numSigs; i++) {
+                            offset = extractLine(buffer, offset, str);
+                            sigs[i] = new Signature(str[0]);
+                        }
+                        return sigs;
+                    } else {
+                        Slog.i(TAG, "Missing signature on backed-up package " + info.packageName);
+                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                                mMonitor,
+                                LOG_EVENT_ID_MISSING_SIGNATURE,
+                                null,
+                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                BackupManagerMonitorUtils.putMonitoringExtra(null,
+                                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
+                    }
+                } else {
+                    Slog.i(TAG, "Expected package " + info.packageName
+                            + " but restore manifest claims " + manifestPackage);
+                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                            EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                            monitoringExtras,
+                            EXTRA_LOG_MANIFEST_PACKAGE_NAME, manifestPackage);
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                            mMonitor,
+                            LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE,
+                            null,
+                            LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                            monitoringExtras);
+                }
+            } else {
+                Slog.i(TAG, "Unknown restore manifest version " + version
+                        + " for package " + info.packageName);
+                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
+                        EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION,
+                        null,
+                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        monitoringExtras);
+
+            }
+        } catch (NumberFormatException e) {
+            Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                    mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST,
+                    null,
+                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
+                            info.packageName));
+        } catch (IllegalArgumentException e) {
+            Slog.w(TAG, e.getMessage());
+        }
+
+        return null;
+    }
+
+    /**
+     * Chooses restore policy.
+     *
+     * @param packageManager - PackageManager instance.
+     * @param allowApks - allow restore set to include apks.
+     * @param info - file metadata.
+     * @param signatures - array of signatures parsed from backup file.
+     * @param userId - ID of the user for which restore is performed.
+     * @return a restore policy constant.
+     */
+    public RestorePolicy chooseRestorePolicy(PackageManager packageManager,
+            boolean allowApks, FileMetadata info, Signature[] signatures,
+            PackageManagerInternal pmi, int userId) {
+        if (signatures == null) {
+            return RestorePolicy.IGNORE;
+        }
+
+        RestorePolicy policy = RestorePolicy.IGNORE;
+
+        // Okay, got the manifest info we need...
+        try {
+            PackageInfo pkgInfo = packageManager.getPackageInfoAsUser(
+                    info.packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId);
+            // Fall through to IGNORE if the app explicitly disallows backup
+            final int flags = pkgInfo.applicationInfo.flags;
+            if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) {
+                // Restore system-uid-space packages only if they have
+                // defined a custom backup agent
+                if (!UserHandle.isCore(pkgInfo.applicationInfo.uid)
+                        || (pkgInfo.applicationInfo.backupAgentName != null)) {
+                    // Verify signatures against any installed version; if they
+                    // don't match, then we fall though and ignore the data.  The
+                    // signatureMatch() method explicitly ignores the signature
+                    // check for packages installed on the system partition, because
+                    // such packages are signed with the platform cert instead of
+                    // the app developer's cert, so they're different on every
+                    // device.
+                    if (AppBackupUtils.signaturesMatch(signatures, pkgInfo, pmi)) {
+                        if ((pkgInfo.applicationInfo.flags
+                                & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) {
+                            Slog.i(TAG, "Package has restoreAnyVersion; taking data");
+                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                                    mMonitor,
+                                    LOG_EVENT_ID_RESTORE_ANY_VERSION,
+                                    pkgInfo,
+                                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                    null);
+                            policy = RestorePolicy.ACCEPT;
+                        } else if (pkgInfo.getLongVersionCode() >= info.version) {
+                            Slog.i(TAG, "Sig + version match; taking data");
+                            policy = RestorePolicy.ACCEPT;
+                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                                    mMonitor,
+                                    LOG_EVENT_ID_VERSIONS_MATCH,
+                                    pkgInfo,
+                                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                    null);
+                        } else {
+                            // The data is from a newer version of the app than
+                            // is presently installed.  That means we can only
+                            // use it if the matching apk is also supplied.
+                            if (allowApks) {
+                                Slog.i(TAG, "Data version " + info.version
+                                        + " is newer than installed "
+                                        + "version "
+                                        + pkgInfo.getLongVersionCode()
+                                        + " - requiring apk");
+                                policy = RestorePolicy.ACCEPT_IF_APK;
+                            } else {
+                                Slog.i(TAG, "Data requires newer version "
+                                        + info.version + "; ignoring");
+                                mMonitor = BackupManagerMonitorUtils
+                                        .monitorEvent(mMonitor,
+                                                LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER,
+                                                pkgInfo,
+                                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                                BackupManagerMonitorUtils
+                                                        .putMonitoringExtra(
+                                                                null,
+                                                                EXTRA_LOG_OLD_VERSION,
+                                                                info.version));
+
+                                policy = RestorePolicy.IGNORE;
+                            }
+                        }
+                    } else {
+                        Slog.w(TAG, "Restore manifest signatures do not match "
+                                + "installed application for "
+                                + info.packageName);
+                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                                mMonitor,
+                                LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH,
+                                pkgInfo,
+                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                null);
+                    }
+                } else {
+                    Slog.w(TAG, "Package " + info.packageName
+                            + " is system level with no agent");
+                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                            mMonitor,
+                            LOG_EVENT_ID_SYSTEM_APP_NO_AGENT,
+                            pkgInfo,
+                            LOG_EVENT_CATEGORY_AGENT,
+                            null);
+                }
+            } else {
+                if (DEBUG) {
+                    Slog.i(TAG,
+                            "Restore manifest from " + info.packageName + " but allowBackup=false");
+                }
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE,
+                        pkgInfo,
+                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        null);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // Okay, the target app isn't installed.  We can process
+            // the restore properly only if the dataset provides the
+            // apk file and we can successfully install it.
+            if (allowApks) {
+                if (DEBUG) {
+                    Slog.i(TAG, "Package " + info.packageName
+                            + " not installed; requiring apk in dataset");
+                }
+                policy = RestorePolicy.ACCEPT_IF_APK;
+            } else {
+                policy = RestorePolicy.IGNORE;
+            }
+            Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                    null,
+                    EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                    monitoringExtras,
+                    EXTRA_LOG_POLICY_ALLOW_APKS, allowApks);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                    mMonitor,
+                    LOG_EVENT_ID_APK_NOT_INSTALLED,
+                    null,
+                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    monitoringExtras);
+        }
+
+        if (policy == RestorePolicy.ACCEPT_IF_APK && !info.hasApk) {
+            Slog.i(TAG, "Cannot restore package " + info.packageName
+                    + " without the matching .apk");
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                    mMonitor,
+                    LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK,
+                    null,
+                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    BackupManagerMonitorUtils.putMonitoringExtra(null,
+                            EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
+        }
+
+        return policy;
+    }
+
+    // Given an actual file content size, consume the post-content padding mandated
+    // by the tar format.
+    public void skipTarPadding(long size) throws IOException {
+        long partial = (size + 512) % 512;
+        if (partial > 0) {
+            final int needed = 512 - (int) partial;
+            if (MORE_DEBUG) {
+                Slog.i(TAG, "Skipping tar padding: " + needed + " bytes");
+            }
+            byte[] buffer = new byte[needed];
+            if (readExactly(mInputStream, buffer, 0, needed) == needed) {
+                mBytesReadListener.onBytesRead(needed);
+            } else {
+                throw new IOException("Unexpected EOF in padding");
+            }
+        }
+    }
+
+    /**
+     * Read a widget metadata file, returning the restored blob.
+     */
+    public void readMetadata(FileMetadata info) throws IOException {
+        // Fail on suspiciously large widget dump files
+        if (info.size > 64 * 1024) {
+            throw new IOException("Metadata too big; corrupt? size=" + info.size);
+        }
+
+        byte[] buffer = new byte[(int) info.size];
+        if (readExactly(mInputStream, buffer, 0, (int) info.size) == info.size) {
+            mBytesReadListener.onBytesRead(info.size);
+        } else {
+            throw new IOException("Unexpected EOF in widget data");
+        }
+
+        String[] str = new String[1];
+        int offset = extractLine(buffer, 0, str);
+        int version = Integer.parseInt(str[0]);
+        if (version == BACKUP_MANIFEST_VERSION) {
+            offset = extractLine(buffer, offset, str);
+            final String pkg = str[0];
+            if (info.packageName.equals(pkg)) {
+                // Data checks out -- the rest of the buffer is a concatenation of
+                // binary blobs as described in the comment at writeAppWidgetData()
+                ByteArrayInputStream bin = new ByteArrayInputStream(buffer,
+                        offset, buffer.length - offset);
+                DataInputStream in = new DataInputStream(bin);
+                while (bin.available() > 0) {
+                    int token = in.readInt();
+                    int size = in.readInt();
+                    if (size > 64 * 1024) {
+                        throw new IOException("Datum " + Integer.toHexString(token)
+                                + " too big; corrupt? size=" + info.size);
+                    }
+                    switch (token) {
+                        case BACKUP_WIDGET_METADATA_TOKEN: {
+                            if (MORE_DEBUG) {
+                                Slog.i(TAG, "Got widget metadata for " + info.packageName);
+                            }
+                            mWidgetData = new byte[size];
+                            in.read(mWidgetData);
+                            break;
+                        }
+                        default: {
+                            if (DEBUG) {
+                                Slog.i(TAG, "Ignoring metadata blob " + Integer.toHexString(token)
+                                        + " for " + info.packageName);
+                            }
+                            in.skipBytes(size);
+                            break;
+                        }
+                    }
+                }
+            } else {
+                Slog.w(TAG,
+                        "Metadata mismatch: package " + info.packageName + " but widget data for "
+                                + pkg);
+
+                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
+                        BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg);
+                mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                        mMonitor,
+                        BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH,
+                        null,
+                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        monitoringExtras);
+            }
+        } else {
+            Slog.w(TAG, "Unsupported metadata version " + version);
+
+            Bundle monitoringExtras = BackupManagerMonitorUtils
+                    .putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
+                            info.packageName);
+            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
+                    EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
+            mMonitor = BackupManagerMonitorUtils.monitorEvent(
+                    mMonitor,
+                    BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION,
+                    null,
+                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    monitoringExtras);
+        }
+    }
+
+    /**
+     * Builds a line from a byte buffer starting at 'offset'.
+     *
+     * @param buffer - where to read a line from.
+     * @param offset - offset in buffer to read a line from.
+     * @param outStr - an output parameter, the result will be put in outStr.
+     * @return the index of the next unconsumed data in the buffer.
+     * @throws IOException in case of an error.
+     */
+    private static int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException {
+        final int end = buffer.length;
+        if (offset >= end) {
+            throw new IOException("Incomplete data");
+        }
+
+        int pos;
+        for (pos = offset; pos < end; pos++) {
+            byte c = buffer[pos];
+            // at LF we declare end of line, and return the next char as the
+            // starting point for the next time through
+            if (c == '\n') {
+                break;
+            }
+        }
+        outStr[0] = new String(buffer, offset, pos - offset);
+        pos++;  // may be pointing an extra byte past the end but that's okay
+        return pos;
+    }
+
+    private boolean readTarHeader(byte[] block) throws IOException {
+        final int got = readExactly(mInputStream, block, 0, 512);
+        if (got == 0) {
+            return false;     // Clean EOF
+        }
+        if (got < 512) {
+            throw new IOException("Unable to read full block header");
+        }
+        mBytesReadListener.onBytesRead(512);
+        return true;
+    }
+
+    // overwrites 'info' fields based on the pax extended header
+    private boolean readPaxExtendedHeader(FileMetadata info)
+            throws IOException {
+        // We should never see a pax extended header larger than this
+        if (info.size > 32 * 1024) {
+            Slog.w(TAG, "Suspiciously large pax header size " + info.size + " - aborting");
+            throw new IOException("Sanity failure: pax header size " + info.size);
+        }
+
+        // read whole blocks, not just the content size
+        int numBlocks = (int) ((info.size + 511) >> 9);
+        byte[] data = new byte[numBlocks * 512];
+        if (readExactly(mInputStream, data, 0, data.length) < data.length) {
+            throw new IOException("Unable to read full pax header");
+        }
+        mBytesReadListener.onBytesRead(data.length);
+
+        final int contentSize = (int) info.size;
+        int offset = 0;
+        do {
+            // extract the line at 'offset'
+            int eol = offset + 1;
+            while (eol < contentSize && data[eol] != ' ') {
+                eol++;
+            }
+            if (eol >= contentSize) {
+                // error: we just hit EOD looking for the end of the size field
+                throw new IOException("Invalid pax data");
+            }
+            // eol points to the space between the count and the key
+            int linelen = (int) extractRadix(data, offset, eol - offset, 10);
+            int key = eol + 1;  // start of key=value
+            eol = offset + linelen - 1; // trailing LF
+            int value;
+            for (value = key + 1; data[value] != '=' && value <= eol; value++) {
+                ;
+            }
+            if (value > eol) {
+                throw new IOException("Invalid pax declaration");
+            }
+
+            // pax requires that key/value strings be in UTF-8
+            String keyStr = new String(data, key, value - key, "UTF-8");
+            // -1 to strip the trailing LF
+            String valStr = new String(data, value + 1, eol - value - 1, "UTF-8");
+
+            if ("path".equals(keyStr)) {
+                info.path = valStr;
+            } else if ("size".equals(keyStr)) {
+                info.size = Long.parseLong(valStr);
+            } else {
+                if (DEBUG) {
+                    Slog.i(TAG, "Unhandled pax key: " + key);
+                }
+            }
+
+            offset += linelen;
+        } while (offset < contentSize);
+
+        return true;
+    }
+
+    private static long extractRadix(byte[] data, int offset, int maxChars, int radix)
+            throws IOException {
+        long value = 0;
+        final int end = offset + maxChars;
+        for (int i = offset; i < end; i++) {
+            final byte b = data[i];
+            // Numeric fields in tar can terminate with either NUL or SPC
+            if (b == 0 || b == ' ') {
+                break;
+            }
+            if (b < '0' || b > ('0' + radix - 1)) {
+                throw new IOException("Invalid number in header: '" + (char) b
+                        + "' for radix " + radix);
+            }
+            value = radix * value + (b - '0');
+        }
+        return value;
+    }
+
+    private static String extractString(byte[] data, int offset, int maxChars) throws IOException {
+        final int end = offset + maxChars;
+        int eos = offset;
+        // tar string fields terminate early with a NUL
+        while (eos < end && data[eos] != 0) {
+            eos++;
+        }
+        return new String(data, offset, eos - offset, "US-ASCII");
+    }
+
+    private static void hexLog(byte[] block) {
+        int offset = 0;
+        int remaining = block.length;
+        StringBuilder buf = new StringBuilder(64);
+        while (remaining > 0) {
+            buf.append(String.format("%04x   ", offset));
+            int numThisLine = (remaining > 16) ? 16 : remaining;
+            for (int i = 0; i < numThisLine; i++) {
+                buf.append(String.format("%02x ", block[offset + i]));
+            }
+            Slog.i("hexdump", buf.toString());
+            buf.setLength(0);
+            remaining -= numThisLine;
+            offset += numThisLine;
+        }
+    }
+
+    public IBackupManagerMonitor getMonitor() {
+        return mMonitor;
+    }
+
+    public byte[] getWidgetData() {
+        return mWidgetData;
+    }
+}