• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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