1 package com.google.android.libraries.backup.shadow;
2 
3 import static android.content.Context.MODE_PRIVATE;
4 
5 import android.app.backup.SharedPreferencesBackupHelper;
6 import android.content.Context;
7 import android.content.SharedPreferences.Editor;
8 import android.util.Log;
9 import com.google.android.libraries.backup.PersistentBackupAgentHelper;
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.ImmutableMap;
13 import com.google.common.collect.ImmutableSet;
14 import java.lang.reflect.Field;
15 import java.util.Map;
16 import java.util.Set;
17 
18 /**
19  * Representation of {@link SharedPreferencesBackupHelper} configuration used for testing. This
20  * class simulates backing up and restoring shared preferences by storing them in memory.
21  *
22  * <p>{@see BackupAgentHelperShadow}
23  */
24 public class SharedPreferencesBackupHelperSimulator extends BackupHelperSimulator {
25   private static final String TAG = "SharedPreferencesBackup";
26 
27   /** Shared preferences file names which should be backed up/restored. */
28   private final Set<String> prefGroups;
29 
SharedPreferencesBackupHelperSimulator(String keyPrefix, Set<String> prefGroups)30   private SharedPreferencesBackupHelperSimulator(String keyPrefix, Set<String> prefGroups) {
31     super(keyPrefix);
32     this.prefGroups = Preconditions.checkNotNull(prefGroups);
33   }
34 
fromPreferenceGroups( String keyPrefix, Set<String> prefGroups)35   public static SharedPreferencesBackupHelperSimulator fromPreferenceGroups(
36       String keyPrefix, Set<String> prefGroups) {
37     return new SharedPreferencesBackupHelperSimulator(keyPrefix, prefGroups);
38   }
39 
fromHelper( String keyPrefix, SharedPreferencesBackupHelper helper)40   public static SharedPreferencesBackupHelperSimulator fromHelper(
41       String keyPrefix, SharedPreferencesBackupHelper helper) {
42     return new SharedPreferencesBackupHelperSimulator(
43         keyPrefix, extractPreferenceGroupsFromHelper(helper));
44   }
45 
46   @VisibleForTesting
extractPreferenceGroupsFromHelper(SharedPreferencesBackupHelper helper)47   static Set<String> extractPreferenceGroupsFromHelper(SharedPreferencesBackupHelper helper) {
48     try {
49       Field prefGroupsField = SharedPreferencesBackupHelper.class.getDeclaredField("mPrefGroups");
50       prefGroupsField.setAccessible(true);
51       return ImmutableSet.copyOf((String[]) prefGroupsField.get(helper));
52     } catch (ReflectiveOperationException e) {
53       throw new IllegalStateException(
54           "Failed to construct SharedPreferencesBackupHelperSimulator", e);
55     }
56   }
57 
58   /** Collection of backed up shared preferences. */
59   public static class SharedPreferencesBackupData {
60     /** Map from shared preferences file names to key-value preference maps. */
61     private final Map<String, Map<String, ?>> preferences;
62 
SharedPreferencesBackupData(Map<String, Map<String, ?>> data)63     public SharedPreferencesBackupData(Map<String, Map<String, ?>> data) {
64       this.preferences = Preconditions.checkNotNull(data);
65     }
66 
67     @Override
equals(Object obj)68     public boolean equals(Object obj) {
69       return obj instanceof SharedPreferencesBackupData
70           && preferences.equals(((SharedPreferencesBackupData) obj).preferences);
71     }
72 
73     @Override
hashCode()74     public int hashCode() {
75       return preferences.hashCode();
76     }
77 
getPreferences()78     public Map<String, Map<String, ?>> getPreferences() {
79       return preferences;
80     }
81   }
82 
83   @Override
backup(Context context)84   public Object backup(Context context) {
85     ImmutableMap.Builder<String, Map<String, ?>> dataToBackupBuilder = ImmutableMap.builder();
86     for (String prefGroup : prefGroups) {
87       Map<String, ?> prefs = context.getSharedPreferences(prefGroup, MODE_PRIVATE).getAll();
88       if (prefs.isEmpty()) {
89         Log.w(TAG, "Shared prefs \"" + prefGroup + "\" are empty. The helper \"" + keyPrefix
90             + "\" assumes this is due to a missing (rather than empty) shared preferences file.");
91         continue;
92       }
93       ImmutableMap.Builder<String, Object> prefsData = ImmutableMap.builder();
94       for (Map.Entry<String, ?> prefEntry : prefs.entrySet()) {
95         String key = prefEntry.getKey();
96         Object value = prefEntry.getValue();
97         if (value instanceof Set) {
98           value = ImmutableSet.copyOf((Set<?>) value);
99         }
100         prefsData.put(key, value);
101       }
102       dataToBackupBuilder.put(prefGroup, prefsData.build());
103     }
104     return new SharedPreferencesBackupData(dataToBackupBuilder.build());
105   }
106 
107   @Override
restore(Context context, Object data)108   public void restore(Context context, Object data) {
109     if (!(data instanceof SharedPreferencesBackupData)) {
110       throw new IllegalArgumentException("Invalid type of files to restore in helper \""
111           + keyPrefix + "\": " + data.getClass());
112     }
113 
114     Map<String, Map<String, ?>> prefsToRestore =
115         ((SharedPreferencesBackupData) data).getPreferences();
116 
117     // Display a warning when missing/empty preferences are restored onto non-empty preferences.
118     for (String prefGroup : prefGroups) {
119       if (context.getSharedPreferences(prefGroup, MODE_PRIVATE).getAll().isEmpty()) {
120         continue;
121       }
122       Map<String, ?> prefsData = prefsToRestore.get(prefGroup);
123       if (prefsData == null) {
124         Log.w(TAG, "Non-empty shared prefs \"" + prefGroup + "\" will NOT be cleared by helper \""
125             + keyPrefix + "\" because the corresponding file is missing in the restored data.");
126       } else if (prefsData.isEmpty()) {
127         Log.w(TAG, "Non-empty shared prefs \"" + prefGroup + "\" will be cleared by helper \""
128             + keyPrefix + "\" because the corresponding file is empty in the restored data.");
129       }
130     }
131 
132     for (Map.Entry<String, Map<String, ?>> restoreEntry : prefsToRestore.entrySet()) {
133       String prefGroup = restoreEntry.getKey();
134       if (!prefGroups.contains(prefGroup)) {
135         Log.w(TAG, "Shared prefs \"" + prefGroup + "\" ignored by helper \"" + keyPrefix + "\".");
136         continue;
137       }
138       Map<String, ?> prefsData = restoreEntry.getValue();
139       Editor editor = context.getSharedPreferences(prefGroup, MODE_PRIVATE).edit().clear();
140       for (Map.Entry<String, ?> prefEntry : prefsData.entrySet()) {
141         PersistentBackupAgentHelper.putSharedPreference(
142             editor, prefEntry.getKey(), prefEntry.getValue());
143       }
144       editor.apply();
145     }
146   }
147 }
148