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