summaryrefslogtreecommitdiffstats
path: root/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java')
-rw-r--r--src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java212
1 files changed, 212 insertions, 0 deletions
diff --git a/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java b/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
new file mode 100644
index 0000000..a107281
--- /dev/null
+++ b/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
@@ -0,0 +1,212 @@
+package com.google.android.libraries.backup;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link BackupAgentHelper} that contains the following improvements:
+ *
+ * <p>1) All backed-up shared preference files will automatically be restored; the app does not need
+ * to know the list of files in advance at restore time. This is important for apps that generate
+ * files dynamically, and it's also important for all apps that use restoreAnyVersion because
+ * additional files could have been added.
+ *
+ * <p>2) Only the requested keys will be backed up from each shared preference file. All keys that
+ * were backed up will be restored.
+ *
+ * <p>These benefits apply only to shared preference files. Other file helpers can be added in the
+ * normal way for a {@link BackupAgentHelper}.
+ *
+ * <p>This class works by creating a separate shared preference file named
+ * {@link #RESERVED_SHARED_PREFERENCES} that it backs up and restores. Before backing up, this file
+ * is populated based on the requested shared preference files and keys. After restoring, the data
+ * is copied back into the original files.
+ */
+public abstract class PersistentBackupAgentHelper extends BackupAgentHelper {
+
+ /**
+ * The name of the shared preferences file reserved for use by the
+ * {@link PersistentBackupAgentHelper}. Files with this name cannot be backed up by this helper.
+ */
+ protected static final String RESERVED_SHARED_PREFERENCES = "persistent_backup_agent_helper";
+
+ private static final String TAG = "PersistentBackupAgentHe"; // The max tag length is 23.
+ private static final String BACKUP_KEY = RESERVED_SHARED_PREFERENCES + "_prefs";
+ private static final String BACKUP_DELIMITER = "/";
+
+ @Override
+ public void onCreate() {
+ addHelper(BACKUP_KEY, new SharedPreferencesBackupHelper(this, RESERVED_SHARED_PREFERENCES));
+ }
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ writeFromPreferenceFilesToBackupFile();
+ super.onBackup(oldState, data, newState);
+ clearBackupFile();
+ }
+
+ @VisibleForTesting
+ void writeFromPreferenceFilesToBackupFile() {
+ Map<String, BackupKeyPredicate> fileBackupKeyPredicates = getBackupSpecification();
+ Editor backupEditor = getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit();
+ backupEditor.clear();
+ for (Map.Entry<String, BackupKeyPredicate> entry : fileBackupKeyPredicates.entrySet()) {
+ writeToBackupFile(entry.getKey(), backupEditor, entry.getValue());
+ }
+ backupEditor.apply();
+ }
+
+ /**
+ * Returns the predicate that decides which keys should be backed up for each shared preference
+ * file name. There must be no files with the same name as {@link #RESERVED_SHARED_PREFERENCES}.
+ * Assumes all shared preference file names are valid.
+ *
+ * <p>This method will only be called at backup time. At restore time, everything that was backed
+ * up is restored.
+ *
+ * @see BackupKeyPredicates
+ */
+ protected abstract Map<String, BackupKeyPredicate> getBackupSpecification();
+
+ /**
+ * Adds data from the given file name for keys that pass the given predicate.
+ * {@link Editor#apply()} is not called.
+ */
+ private void writeToBackupFile(
+ String srcFileName, Editor editor, BackupKeyPredicate backupKeyPredicate) {
+ if (srcFileName.equals(RESERVED_SHARED_PREFERENCES)) {
+ throw new IllegalStateException("Backup file name \"" + RESERVED_SHARED_PREFERENCES + "\" is "
+ + "reserved by PersistentBackupAgentHelper and cannot be used.");
+ }
+ if (srcFileName.contains(BACKUP_DELIMITER)) {
+ throw new IllegalStateException("Backup file name \"" + srcFileName + "\" cannot contain "
+ + "delimiter \"" + BACKUP_DELIMITER + "\".");
+ }
+ SharedPreferences srcSharedPreferences = getSharedPreferences(srcFileName, MODE_PRIVATE);
+ Map<String, ?> srcMap = srcSharedPreferences.getAll();
+ for (Map.Entry<String, ?> entry : srcMap.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ if (backupKeyPredicate.shouldBeBackedUp(key)) {
+ putSharedPreference(editor, buildBackupKey(srcFileName, key), value);
+ }
+ }
+ }
+
+ private static String buildBackupKey(String fileName, String key) {
+ return fileName + BACKUP_DELIMITER + key;
+ }
+
+ /**
+ * Puts the given value into the given editor for the given key. {@link Editor#apply()} is not
+ * called.
+ */
+ @SuppressWarnings("unchecked") // There are no unchecked casts - the Set<String> cast IS checked.
+ public static void putSharedPreference(Editor editor, String key, Object value) {
+ if (value instanceof Boolean) {
+ editor.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Float) {
+ editor.putFloat(key, (Float) value);
+ } else if (value instanceof Integer) {
+ editor.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ editor.putLong(key, (Long) value);
+ } else if (value instanceof String) {
+ editor.putString(key, (String) value);
+ } else if (value instanceof Set) {
+ for (Object object : (Set) value) {
+ if (!(object instanceof String)) {
+ // If a new type of shared preference set is added in the future, it can't be correctly
+ // restored on this version.
+ Log.w(TAG, "Skipping restore of key " + key + " because its value is a set containing"
+ + " an object of type " + (value == null ? null : value.getClass()) + ".");
+ return;
+ }
+ }
+ editor.putStringSet(key, (Set<String>) value);
+ } else {
+ // If a new type of shared preference is added in the future, it can't be correctly restored
+ // on this version.
+ Log.w(TAG, "Skipping restore of key " + key + " because its value is the unrecognized type "
+ + (value == null ? null : value.getClass()) + ".");
+ return;
+ }
+ }
+
+ private void clearBackupFile() {
+ // We don't currently delete the file because of a lack of a supported way to do it and because
+ // of the concerns of synchronously doing so.
+ getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit().clear().apply();
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor stateFile)
+ throws IOException {
+ super.onRestore(data, appVersionCode, stateFile);
+ writeFromBackupFileToPreferenceFiles(appVersionCode);
+ clearBackupFile();
+ }
+
+ @VisibleForTesting
+ void writeFromBackupFileToPreferenceFiles(int appVersionCode) {
+ SharedPreferences backupSharedPreferences =
+ getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE);
+ Map<String, Editor> editors = new HashMap<>();
+ for (Map.Entry<String, ?> entry : backupSharedPreferences.getAll().entrySet()) {
+ // We restore all files and keys, including those that this version doesn't know about or
+ // wouldn't have backed up. This ensures forward-compatibility.
+ String backupKey = entry.getKey();
+ Object value = entry.getValue();
+ int backupDelimiterIndex = backupKey.indexOf(BACKUP_DELIMITER);
+ if (backupDelimiterIndex < 1 || backupDelimiterIndex >= backupKey.length() - 1) {
+ Log.w(TAG, "Format of key \"" + backupKey + "\" not understood, so skipping its restore.");
+ continue;
+ }
+ String fileName = backupKey.substring(0, backupDelimiterIndex);
+ String preferenceKey = backupKey.substring(backupDelimiterIndex + 1);
+ Editor editor = editors.get(fileName);
+ if (editor == null) {
+ // #apply is called once for each editor later.
+ editor = getSharedPreferences(fileName, MODE_PRIVATE).edit();
+ editors.put(fileName, editor);
+ }
+ putSharedPreference(editor, preferenceKey, value);
+ }
+ for (Editor editor : editors.values()) {
+ editor.apply();
+ }
+ onPreferencesRestored(editors.keySet(), appVersionCode);
+ }
+
+ /**
+ * This method is called when the preferences have been restored. It can be overridden to apply
+ * processing to the restored preferences. However, this is not recommended to be used in
+ * conjunction with restoreAnyVersion unless the following problems are considered:
+ *
+ * <p>1) Once the processing is live, it could be applied to any data that ever gets backed up by
+ * the app, not just the types of data that were available when the processing was originally
+ * added.
+ *
+ * <p>2) Older versions of the app (that use restoreAnyVersion) will restore data without applying
+ * the processing. For first-party apps pre-installed on the device, this could be the case for
+ * every new user.
+ *
+ * @param names The list of files restored.
+ * @param appVersionCode The app version code from {@link #onRestore}.
+ */
+ @SuppressWarnings({"unused"})
+ protected void onPreferencesRestored(Set<String> names, int appVersionCode) {}
+}