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()}/<transport>
+ * <li>State file: {@link
+ * UserBackupManagerService#getBaseStateDir()}/<transport>/<package><br>
+ * Represents the state of the backup data for a specific package in the current dataset.
+ * <li>Stage directory: {@link UserBackupManagerService#getDataDir()}
+ * <li>Stage file: {@link UserBackupManagerService#getDataDir()}/<package>.data<br>
+ * Contains staged data that the agents wrote via {@link BackupDataOutput}, to be transmitted
+ * to the transport.
+ * </ul>
+ *
+ * If there is no PackageManager (PM) pseudo-package state file in the state directory, the
+ * specified transport will be initialized with {@link IBackupTransport#initializeDevice()}.
+ *
+ * <p>The PM pseudo-package is the first package to be backed-up and sent to the transport in case
+ * of incremental choice. If non-incremental, PM will only be backed-up if specified in the queue,
+ * and if it's the case it will be re-positioned at the head of the queue.
+ *
+ * <p>Before starting, this task will register itself in {@link UserBackupManagerService} current
+ * operations.
+ *
+ * <p>In summary, this task will for each package:
+ *
+ * <ul>
+ * <li>Bind to its {@link IBackupAgent}.
+ * <li>Request transport quota and flags.
+ * <li>Call {@link IBackupAgent#doBackup(ParcelFileDescriptor, ParcelFileDescriptor,
+ * ParcelFileDescriptor, long, IBackupCallback, int)} via {@link RemoteCall} passing the
+ * old state file descriptor (read), the backup data file descriptor (write), the new state
+ * file descriptor (write), the quota and the transport flags. This will call {@link
+ * BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)} with
+ * the old state file to be read, a {@link BackupDataOutput} object to write the backup data
+ * and the new state file to write. By writing to {@link BackupDataOutput}, the agent will
+ * write data to the stage file. The task will block waiting for either:
+ * <ul>
+ * <li>Agent response.
+ * <li>Agent time-out (specified via {@link
+ * UserBackupManagerService#getAgentTimeoutParameters()}.
+ * <li>External cancellation or thread interrupt.
+ * </ul>
+ * <li>Unbind the agent.
+ * <li>Assuming agent response, send the staged data that the agent wrote to disk to the transport
+ * via {@link IBackupTransport#performBackup(PackageInfo, ParcelFileDescriptor, int)}.
+ * <li>Call {@link IBackupTransport#finishBackup()} if previous call was successful.
+ * <li>Save the new state in the state file. During the agent call it was being written to
+ * <state file>.new, here we rename it and replace the old one.
+ * <li>Delete the stage file.
+ * </ul>
+ *
+ * In the end, this task will:
+ *
+ * <ul>
+ * <li>Mark data-changed for the remaining packages in the queue (skipped packages).
+ * <li>Delete the {@link DataChangedJournal} provided. Note that this should not be the current
+ * journal.
+ * <li>Set {@link UserBackupManagerService} current token as {@link
+ * IBackupTransport#getCurrentRestoreSet()}, if applicable.
+ * <li>Add the transport to the list of transports pending initialization ({@link
+ * UserBackupManagerService#getPendingInits()}) and kick-off initialization if the transport
+ * ever returned {@link BackupTransport#TRANSPORT_NOT_INITIALIZED}.
+ * <li>Unregister the task in current operations.
+ * <li>Release the wakelock.
+ * <li>Kick-off {@link PerformFullTransportBackupTask} if a list of full-backup packages was
+ * provided.
+ * </ul>
+ *
+ * The caller can specify whether this should be an incremental or non-incremental backup. In the
+ * case of non-incremental the agents will be passed an empty old state file, which signals that a
+ * complete backup should be performed.
+ *
+ * <p>This task is designed to run on a dedicated thread, with the exception of the {@link
+ * #handleCancel(boolean)} method, which can be called from any thread.
+ */
+// TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks)
+// TODO: Consider having the caller responsible for some clean-up (like resetting state)
+// TODO: Distinguish between cancel and time-out where possible for logging/monitoring/observing
+public class KeyValueBackupTask implements BackupRestoreTask, Runnable {
+ private static final String TAG = "KVBT";
+
+ private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND;
+ private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
+ private static final String BLANK_STATE_FILE_NAME = "blank_state";
+ private static final String PM_PACKAGE = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
+ private static final String SUCCESS_STATE_SUBDIR = "backing-up";
+ @VisibleForTesting static final String NO_DATA_END_SENTINEL = "@end@";
+ @VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data";
+ @VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new";
+
+ /**
+ * Creates a new {@link KeyValueBackupTask} for key-value backup operation, spins up a new
+ * dedicated thread and kicks off the operation in it.
+ *
+ * @param backupManagerService The {@link UserBackupManagerService} instance.
+ * @param transportClient The {@link TransportClient} that contains the transport used for the
+ * operation.
+ * @param transportDirName The value of {@link IBackupTransport#transportDirName()} for the
+ * transport whose {@link TransportClient} was provided above.
+ * @param queue The list of package names that will be backed-up.
+ * @param dataChangedJournal The old data-changed journal file that will be deleted when the
+ * operation finishes (successfully or not) or {@code null}.
+ * @param observer A {@link IBackupObserver}.
+ * @param monitor A {@link IBackupManagerMonitor}.
+ * @param listener A {@link OnTaskFinishedListener} or {@code null}.
+ * @param pendingFullBackups The list of packages that will be passed for a new {@link
+ * PerformFullTransportBackupTask} operation, which will be started when this finishes.
+ * @param userInitiated Whether this was user-initiated or not.
+ * @param nonIncremental If {@code true}, this will be a complete backup for each package,
+ * otherwise it will be just an incremental one over the current dataset.
+ * @return The {@link KeyValueBackupTask} that was started.
+ */
+ public static KeyValueBackupTask start(
+ UserBackupManagerService backupManagerService,
+ TransportClient transportClient,
+ String transportDirName,
+ List<String> queue,
+ @Nullable DataChangedJournal dataChangedJournal,
+ IBackupObserver observer,
+ @Nullable IBackupManagerMonitor monitor,
+ OnTaskFinishedListener listener,
+ List<String> pendingFullBackups,
+ boolean userInitiated,
+ boolean nonIncremental) {
+ 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;
+ }
+}