| 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) {} |
| } |