summaryrefslogtreecommitdiffstats
path: root/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
blob: a10728169534454c30ea43a5648d936e0cee9196 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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) {}
}