1 package com.google.android.libraries.backup.shadow; 2 3 import static org.junit.Assert.assertNotNull; 4 import static org.junit.Assert.assertTrue; 5 6 import android.app.backup.BackupAgent; 7 import android.app.backup.BackupAgentHelper; 8 import android.app.backup.BackupDataInput; 9 import android.app.backup.BackupDataOutput; 10 import android.app.backup.BackupHelper; 11 import android.app.backup.FileBackupHelper; 12 import android.app.backup.SharedPreferencesBackupHelper; 13 import android.content.Context; 14 import android.content.SharedPreferences; 15 import android.os.Build.VERSION; 16 import android.os.Build.VERSION_CODES; 17 import android.os.ParcelFileDescriptor; 18 import android.util.Log; 19 import com.google.common.collect.ImmutableMap; 20 import java.io.IOException; 21 import java.lang.reflect.Method; 22 import java.util.Map; 23 import java.util.TreeMap; 24 import java.util.concurrent.atomic.AtomicReference; 25 import org.robolectric.RuntimeEnvironment; 26 import org.robolectric.annotation.Implementation; 27 import org.robolectric.annotation.Implements; 28 import org.robolectric.annotation.RealObject; 29 30 /** 31 * Shadow class for end-to-end testing of {@link BackupAgentHelper} subclasses in unit tests. 32 * 33 * <p>This class currently supports <b>key-value backups only</b>. In other words, it does 34 * <b>not</b> support Dolly. In addition, the testing framework has the following two limitations 35 * with regards to backup/restore of {@link SharedPreferences}: 36 * 37 * <ol> 38 * <li>Preferences are normally backed by xml files in the app's shared_prefs directory, but 39 * Robolectric replaces them with {@link RoboSharedPreferences}, which are backed by an in-memory 40 * {@link Map}. Therefore, modifying the relevant xml files will have no effect on the preferences 41 * (and vice versa). 42 * <li>For the same reason, the testing framework cannot easily determine whether the underlying 43 * xml file for given shared preferences would have been empty or missing upon backup. The latter 44 * is assumed to ensure that apps don't rely on restore to implicitly clear data (potentially 45 * PII). 46 * </ol> 47 */ 48 @Implements(BackupAgentHelper.class) 49 public class BackupAgentHelperShadow { 50 private static final String TAG = "BackupAgentHelperShadow"; 51 52 /** 53 * Temporarily stores the backup data generated in {@link #onBackup} so that it could be returned 54 * by {@link #simulateBackup}. 55 */ 56 private static final AtomicReference<Map<String, Object>> backupDataMapToBackup = 57 new AtomicReference<>(); 58 59 /** 60 * Temporarily stores the backed up data passed to {@link #simulateRestore} so that it could be 61 * used in {@link #onRestore}. 62 */ 63 private static final AtomicReference<Map<String, Object>> backupDataMapToRestore = 64 new AtomicReference<>(); 65 66 /** 67 * Simulates key-value backup for the provided agent all the way from {@link 68 * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). 69 */ simulateBackup(BackupAgentHelper agent)70 public static Map<String, Object> simulateBackup(BackupAgentHelper agent) { 71 Map<String, Object> backupDataMap; 72 attachBaseContextToAgentIfNecessary(agent); 73 agent.onCreate(); 74 try { 75 agent.onBackup(null, null, null); 76 backupDataMap = backupDataMapToBackup.getAndSet(null); 77 } catch (IOException e) { 78 backupDataMapToBackup.set(null); 79 throw new IllegalStateException(e); 80 } 81 agent.onDestroy(); 82 return backupDataMap; 83 } 84 85 /** 86 * Simulates key-value restore for the provided agent all the way from {@link 87 * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). 88 * 89 * <p>Note: To make end-to-end tests more realistic, <b>different {@link BackupAgentHelper} 90 * instances</b> should be used in {@link #simulateBackup} and {@link #simulateRestore}. 91 */ simulateRestore( BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode)92 public static void simulateRestore( 93 BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode) { 94 attachBaseContextToAgentIfNecessary(agent); 95 agent.onCreate(); 96 assertTrue(backupDataMapToRestore.compareAndSet(null, backupDataMap)); 97 try { 98 agent.onRestore(null, appVersionCode, null); 99 } catch (IOException e) { 100 throw new IllegalStateException(e); 101 } finally { 102 backupDataMapToRestore.set(null); 103 } 104 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 105 agent.onRestoreFinished(); 106 } 107 agent.onDestroy(); 108 } 109 attachBaseContextToAgentIfNecessary(BackupAgentHelper agent)110 private static void attachBaseContextToAgentIfNecessary(BackupAgentHelper agent) { 111 if (agent.getBaseContext() != null) { 112 return; 113 } 114 try { 115 // {@link BackupAgent#attach} is a hidden method, so we need to call it via reflection. 116 Method method = BackupAgent.class.getMethod("attach", Context.class); 117 method.invoke(agent, RuntimeEnvironment.application); 118 } catch (ReflectiveOperationException e) { 119 throw new IllegalStateException(e); 120 } 121 } 122 123 private final Map<String, BackupHelperSimulator> helperSimulators; 124 BackupAgentHelperShadow()125 public BackupAgentHelperShadow() { 126 // Use a {@link TreeMap} to mirror the internal implementation of {@link BackupHelperDispatcher} 127 // as closely as possible. 128 helperSimulators = new TreeMap<>(); 129 } 130 131 @RealObject private BackupAgentHelper realHelper; 132 133 @Implementation addHelper(String keyPrefix, BackupHelper helper)134 public void addHelper(String keyPrefix, BackupHelper helper) { 135 Class<? extends BackupHelper> helperClass = helper.getClass(); 136 final BackupHelperSimulator simulator; 137 if (helperClass == SharedPreferencesBackupHelper.class) { 138 simulator = SharedPreferencesBackupHelperSimulator.fromHelper( 139 keyPrefix, (SharedPreferencesBackupHelper) helper); 140 } else if (helperClass == FileBackupHelper.class) { 141 simulator = FileBackupHelperSimulator.fromHelper(keyPrefix, (FileBackupHelper) helper); 142 } else { 143 Log.w( 144 TAG, "Unknown backup helper class for key prefix \"" + keyPrefix + "\": " + helperClass); 145 simulator = new UnsupportedBackupHelperSimulator(keyPrefix, helper); 146 } 147 helperSimulators.put(keyPrefix, simulator); 148 } 149 150 @Implementation onBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)151 public void onBackup( 152 ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) 153 throws IOException { 154 ImmutableMap.Builder<String, Object> backupDataMapBuilder = ImmutableMap.builder(); 155 for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) { 156 String keyPrefix = simulatorEntry.getKey(); 157 BackupHelperSimulator simulator = simulatorEntry.getValue(); 158 backupDataMapBuilder.put(keyPrefix, simulator.backup(realHelper)); 159 } 160 161 assertTrue(backupDataMapToBackup.compareAndSet(null, backupDataMapBuilder.build())); 162 } 163 164 @Implementation onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)165 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 166 throws IOException { 167 Map<String, Object> backupDataMap = backupDataMapToRestore.getAndSet(null); 168 assertNotNull(backupDataMap); 169 170 for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) { 171 String keyPrefix = simulatorEntry.getKey(); 172 Object dataToRestore = backupDataMap.get(keyPrefix); 173 if (dataToRestore == null) { 174 Log.w(TAG, "No data to restore for key prefix: \"" + keyPrefix + "\"."); 175 continue; 176 } 177 BackupHelperSimulator simulator = simulatorEntry.getValue(); 178 simulator.restore(realHelper, dataToRestore); 179 } 180 } 181 } 182