diff options
Diffstat (limited to 'src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java')
-rw-r--r-- | src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java | 212 |
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) {} +} |