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.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * A {@link BackupAgentHelper} that contains the following improvements: * *

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. * *

2) Only the requested keys will be backed up from each shared preference file. All keys that * were backed up will be restored. * *

These benefits apply only to shared preference files. Other file helpers can be added in the * normal way for a {@link BackupAgentHelper}. * *

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 fileBackupKeyPredicates = getBackupSpecification(); Editor backupEditor = getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit(); backupEditor.clear(); for (Map.Entry 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}. This * method assumes that all shared preference file names are valid: they must not contain path * separators ("/"). * *

This method will only be called at backup time. At restore time, everything that was backed * up is restored. * * @see #isSupportedSharedPreferencesName * @see BackupKeyPredicates */ protected abstract Map 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 (!isSupportedSharedPreferencesName(srcFileName)) { throw new IllegalArgumentException( "Unsupported shared preferences file name \"" + srcFileName + "\""); } SharedPreferences srcSharedPreferences = getSharedPreferences(srcFileName, MODE_PRIVATE); Map srcMap = srcSharedPreferences.getAll(); for (Map.Entry 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 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) 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 editors = new HashMap<>(); for (Map.Entry 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 < 0 || 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) { if (!isSupportedSharedPreferencesName(fileName)) { Log.w(TAG, "Skipping unsupported shared preferences file name \"" + fileName + "\""); continue; } // #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: * *

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. * *

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 names, int appVersionCode) {} /** * Returns whether the provided shared preferences file name is supported by this class. * *

The following file names are NOT supported: *

*/ public static boolean isSupportedSharedPreferencesName(String fileName) { return !fileName.contains(File.separator) && !fileName.contains(BACKUP_DELIMITER) // Same as File.separator. Better safe than sorry. && !RESERVED_SHARED_PREFERENCES.equals(fileName); } }