1 package com.google.android.libraries.backup; 2 3 import android.app.backup.BackupAgentHelper; 4 import android.app.backup.BackupDataInput; 5 import android.app.backup.BackupDataOutput; 6 import android.app.backup.SharedPreferencesBackupHelper; 7 import android.content.SharedPreferences; 8 import android.content.SharedPreferences.Editor; 9 import android.os.ParcelFileDescriptor; 10 import android.support.annotation.VisibleForTesting; 11 import android.util.Log; 12 import java.io.File; 13 import java.io.IOException; 14 import java.util.HashMap; 15 import java.util.Map; 16 import java.util.Set; 17 18 /** 19 * A {@link BackupAgentHelper} that contains the following improvements: 20 * 21 * <p>1) All backed-up shared preference files will automatically be restored; the app does not need 22 * to know the list of files in advance at restore time. This is important for apps that generate 23 * files dynamically, and it's also important for all apps that use restoreAnyVersion because 24 * additional files could have been added. 25 * 26 * <p>2) Only the requested keys will be backed up from each shared preference file. All keys that 27 * were backed up will be restored. 28 * 29 * <p>These benefits apply only to shared preference files. Other file helpers can be added in the 30 * normal way for a {@link BackupAgentHelper}. 31 * 32 * <p>This class works by creating a separate shared preference file named 33 * {@link #RESERVED_SHARED_PREFERENCES} that it backs up and restores. Before backing up, this file 34 * is populated based on the requested shared preference files and keys. After restoring, the data 35 * is copied back into the original files. 36 */ 37 public abstract class PersistentBackupAgentHelper extends BackupAgentHelper { 38 39 /** 40 * The name of the shared preferences file reserved for use by the 41 * {@link PersistentBackupAgentHelper}. Files with this name cannot be backed up by this helper. 42 */ 43 protected static final String RESERVED_SHARED_PREFERENCES = "persistent_backup_agent_helper"; 44 45 private static final String TAG = "PersistentBackupAgentHe"; // The max tag length is 23. 46 private static final String BACKUP_KEY = RESERVED_SHARED_PREFERENCES + "_prefs"; 47 private static final String BACKUP_DELIMITER = "/"; 48 49 @Override onCreate()50 public void onCreate() { 51 addHelper(BACKUP_KEY, new SharedPreferencesBackupHelper(this, RESERVED_SHARED_PREFERENCES)); 52 } 53 54 @Override onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)55 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 56 ParcelFileDescriptor newState) throws IOException { 57 writeFromPreferenceFilesToBackupFile(); 58 super.onBackup(oldState, data, newState); 59 clearBackupFile(); 60 } 61 62 @VisibleForTesting writeFromPreferenceFilesToBackupFile()63 void writeFromPreferenceFilesToBackupFile() { 64 Map<String, BackupKeyPredicate> fileBackupKeyPredicates = getBackupSpecification(); 65 Editor backupEditor = getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit(); 66 backupEditor.clear(); 67 for (Map.Entry<String, BackupKeyPredicate> entry : fileBackupKeyPredicates.entrySet()) { 68 writeToBackupFile(entry.getKey(), backupEditor, entry.getValue()); 69 } 70 backupEditor.apply(); 71 } 72 73 /** 74 * Returns the predicate that decides which keys should be backed up for each shared preference 75 * file name. 76 * 77 * <p>There must be no files with the same name as {@link #RESERVED_SHARED_PREFERENCES}. This 78 * method assumes that all shared preference file names are valid: they must not contain path 79 * separators ("/"). 80 * 81 * <p>This method will only be called at backup time. At restore time, everything that was backed 82 * up is restored. 83 * 84 * @see #isSupportedSharedPreferencesName 85 * @see BackupKeyPredicates 86 */ getBackupSpecification()87 protected abstract Map<String, BackupKeyPredicate> getBackupSpecification(); 88 89 /** 90 * Adds data from the given file name for keys that pass the given predicate. 91 * {@link Editor#apply()} is not called. 92 */ writeToBackupFile( String srcFileName, Editor editor, BackupKeyPredicate backupKeyPredicate)93 private void writeToBackupFile( 94 String srcFileName, Editor editor, BackupKeyPredicate backupKeyPredicate) { 95 if (!isSupportedSharedPreferencesName(srcFileName)) { 96 throw new IllegalArgumentException( 97 "Unsupported shared preferences file name \"" + srcFileName + "\""); 98 } 99 SharedPreferences srcSharedPreferences = getSharedPreferences(srcFileName, MODE_PRIVATE); 100 Map<String, ?> srcMap = srcSharedPreferences.getAll(); 101 for (Map.Entry<String, ?> entry : srcMap.entrySet()) { 102 String key = entry.getKey(); 103 Object value = entry.getValue(); 104 if (backupKeyPredicate.shouldBeBackedUp(key)) { 105 putSharedPreference(editor, buildBackupKey(srcFileName, key), value); 106 } 107 } 108 } 109 buildBackupKey(String fileName, String key)110 private static String buildBackupKey(String fileName, String key) { 111 return fileName + BACKUP_DELIMITER + key; 112 } 113 114 /** 115 * Puts the given value into the given editor for the given key. {@link Editor#apply()} is not 116 * called. 117 */ 118 @SuppressWarnings("unchecked") // There are no unchecked casts - the Set<String> cast IS checked. putSharedPreference(Editor editor, String key, Object value)119 public static void putSharedPreference(Editor editor, String key, Object value) { 120 if (value instanceof Boolean) { 121 editor.putBoolean(key, (Boolean) value); 122 } else if (value instanceof Float) { 123 editor.putFloat(key, (Float) value); 124 } else if (value instanceof Integer) { 125 editor.putInt(key, (Integer) value); 126 } else if (value instanceof Long) { 127 editor.putLong(key, (Long) value); 128 } else if (value instanceof String) { 129 editor.putString(key, (String) value); 130 } else if (value instanceof Set) { 131 for (Object object : (Set) value) { 132 if (!(object instanceof String)) { 133 // If a new type of shared preference set is added in the future, it can't be correctly 134 // restored on this version. 135 Log.w(TAG, "Skipping restore of key " + key + " because its value is a set containing" 136 + " an object of type " + (value == null ? null : value.getClass()) + "."); 137 return; 138 } 139 } 140 editor.putStringSet(key, (Set<String>) value); 141 } else { 142 // If a new type of shared preference is added in the future, it can't be correctly restored 143 // on this version. 144 Log.w(TAG, "Skipping restore of key " + key + " because its value is the unrecognized type " 145 + (value == null ? null : value.getClass()) + "."); 146 return; 147 } 148 } 149 clearBackupFile()150 private void clearBackupFile() { 151 // We don't currently delete the file because of a lack of a supported way to do it and because 152 // of the concerns of synchronously doing so. 153 getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit().clear().apply(); 154 } 155 156 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor stateFile)157 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor stateFile) 158 throws IOException { 159 super.onRestore(data, appVersionCode, stateFile); 160 writeFromBackupFileToPreferenceFiles(appVersionCode); 161 clearBackupFile(); 162 } 163 164 @VisibleForTesting writeFromBackupFileToPreferenceFiles(int appVersionCode)165 void writeFromBackupFileToPreferenceFiles(int appVersionCode) { 166 SharedPreferences backupSharedPreferences = 167 getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE); 168 Map<String, Editor> editors = new HashMap<>(); 169 for (Map.Entry<String, ?> entry : backupSharedPreferences.getAll().entrySet()) { 170 // We restore all files and keys, including those that this version doesn't know about or 171 // wouldn't have backed up. This ensures forward-compatibility. 172 String backupKey = entry.getKey(); 173 Object value = entry.getValue(); 174 int backupDelimiterIndex = backupKey.indexOf(BACKUP_DELIMITER); 175 if (backupDelimiterIndex < 0 || backupDelimiterIndex >= backupKey.length() - 1) { 176 Log.w(TAG, "Format of key \"" + backupKey + "\" not understood, so skipping its restore."); 177 continue; 178 } 179 String fileName = backupKey.substring(0, backupDelimiterIndex); 180 String preferenceKey = backupKey.substring(backupDelimiterIndex + 1); 181 Editor editor = editors.get(fileName); 182 if (editor == null) { 183 if (!isSupportedSharedPreferencesName(fileName)) { 184 Log.w(TAG, "Skipping unsupported shared preferences file name \"" + fileName + "\""); 185 continue; 186 } 187 // #apply is called once for each editor later. 188 editor = getSharedPreferences(fileName, MODE_PRIVATE).edit(); 189 editors.put(fileName, editor); 190 } 191 putSharedPreference(editor, preferenceKey, value); 192 } 193 for (Editor editor : editors.values()) { 194 editor.apply(); 195 } 196 onPreferencesRestored(editors.keySet(), appVersionCode); 197 } 198 199 /** 200 * This method is called when the preferences have been restored. It can be overridden to apply 201 * processing to the restored preferences. However, this is not recommended to be used in 202 * conjunction with restoreAnyVersion unless the following problems are considered: 203 * 204 * <p>1) Once the processing is live, it could be applied to any data that ever gets backed up by 205 * the app, not just the types of data that were available when the processing was originally 206 * added. 207 * 208 * <p>2) Older versions of the app (that use restoreAnyVersion) will restore data without applying 209 * the processing. For first-party apps pre-installed on the device, this could be the case for 210 * every new user. 211 * 212 * @param names The list of files restored. 213 * @param appVersionCode The app version code from {@link #onRestore}. 214 */ 215 @SuppressWarnings({"unused"}) onPreferencesRestored(Set<String> names, int appVersionCode)216 protected void onPreferencesRestored(Set<String> names, int appVersionCode) {} 217 218 /** 219 * Returns whether the provided shared preferences file name is supported by this class. 220 * 221 * <p>The following file names are NOT supported: 222 * <ul> 223 * <li>{@link #RESERVED_SHARED_PREFERENCES} 224 * <li>file names containing path separators ("/") 225 * </ul> 226 */ isSupportedSharedPreferencesName(String fileName)227 public static boolean isSupportedSharedPreferencesName(String fileName) { 228 return !fileName.contains(File.separator) 229 && !fileName.contains(BACKUP_DELIMITER) // Same as File.separator. Better safe than sorry. 230 && !RESERVED_SHARED_PREFERENCES.equals(fileName); 231 } 232 } 233