Home | History | Annotate | Download | only in backup
      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
     50   public void onCreate() {
     51     addHelper(BACKUP_KEY, new SharedPreferencesBackupHelper(this, RESERVED_SHARED_PREFERENCES));
     52   }
     53 
     54   @Override
     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
     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    */
     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    */
     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 
    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.
    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 
    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
    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
    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"})
    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    */
    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