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 import org.robolectric.fakes.RoboSharedPreferences; 30 31 /** 32 * Shadow class for end-to-end testing of {@link BackupAgentHelper} subclasses in unit tests. 33 * 34 * <p>This class currently supports <b>key-value backups only</b>. In other words, it does 35 * <b>not</b> support Dolly. In addition, the testing framework has the following two limitations 36 * with regards to backup/restore of {@link SharedPreferences}: 37 * 38 * <ol> 39 * <li>Preferences are normally backed by xml files in the app's shared_prefs directory, but 40 * Robolectric replaces them with {@link RoboSharedPreferences}, which are backed by an in-memory 41 * {@link Map}. Therefore, modifying the relevant xml files will have no effect on the preferences 42 * (and vice versa). 43 * <li>For the same reason, the testing framework cannot easily determine whether the underlying 44 * xml file for given shared preferences would have been empty or missing upon backup. The latter 45 * is assumed to ensure that apps don't rely on restore to implicitly clear data (potentially 46 * PII). 47 * </ol> 48 */ 49 @Implements(BackupAgentHelper.class) 50 public class BackupAgentHelperShadow { 51 private static final String TAG = "BackupAgentHelperShadow"; 52 53 /** 54 * Temporarily stores the backup data generated in {@link #onBackup} so that it could be returned 55 * by {@link #simulateBackup}. 56 */ 57 private static final AtomicReference<Map<String, Object>> backupDataMapToBackup = 58 new AtomicReference<>(); 59 60 /** 61 * Temporarily stores the backed up data passed to {@link #simulateRestore} so that it could be 62 * used in {@link #onRestore}. 63 */ 64 private static final AtomicReference<Map<String, Object>> backupDataMapToRestore = 65 new AtomicReference<>(); 66 67 /** 68 * Simulates key-value backup for the provided agent all the way from {@link 69 * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). 70 */ simulateBackup(BackupAgentHelper agent)71 public static Map<String, Object> simulateBackup(BackupAgentHelper agent) { 72 Map<String, Object> backupDataMap; 73 attachBaseContextToAgentIfNecessary(agent); 74 agent.onCreate(); 75 try { 76 agent.onBackup(null, null, null); 77 backupDataMap = backupDataMapToBackup.getAndSet(null); 78 } catch (IOException e) { 79 backupDataMapToBackup.set(null); 80 throw new IllegalStateException(e); 81 } 82 agent.onDestroy(); 83 return backupDataMap; 84 } 85 86 /** 87 * Simulates key-value restore for the provided agent all the way from {@link 88 * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). 89 * 90 * <p>Note: To make end-to-end tests more realistic, <b>different {@link BackupAgentHelper} 91 * instances</b> should be used in {@link #simulateBackup} and {@link #simulateRestore}. 92 */ simulateRestore( BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode)93 public static void simulateRestore( 94 BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode) { 95 attachBaseContextToAgentIfNecessary(agent); 96 agent.onCreate(); 97 assertTrue(backupDataMapToRestore.compareAndSet(null, backupDataMap)); 98 try { 99 agent.onRestore(null, appVersionCode, null); 100 } catch (IOException e) { 101 throw new IllegalStateException(e); 102 } finally { 103 backupDataMapToRestore.set(null); 104 } 105 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 106 agent.onRestoreFinished(); 107 } 108 agent.onDestroy(); 109 } 110 attachBaseContextToAgentIfNecessary(BackupAgentHelper agent)111 private static void attachBaseContextToAgentIfNecessary(BackupAgentHelper agent) { 112 if (agent.getBaseContext() != null) { 113 return; 114 } 115 try { 116 // {@link BackupAgent#attach} is a hidden method, so we need to call it via reflection. 117 Method method = BackupAgent.class.getMethod("attach", Context.class); 118 method.invoke(agent, RuntimeEnvironment.application); 119 } catch (ReflectiveOperationException e) { 120 throw new IllegalStateException(e); 121 } 122 } 123 124 private final Map<String, BackupHelperSimulator> helperSimulators; 125 BackupAgentHelperShadow()126 public BackupAgentHelperShadow() { 127 // Use a {@link TreeMap} to mirror the internal implementation of {@link BackupHelperDispatcher} 128 // as closely as possible. 129 helperSimulators = new TreeMap<>(); 130 } 131 132 @RealObject private BackupAgentHelper realHelper; 133 134 @Implementation addHelper(String keyPrefix, BackupHelper helper)135 public void addHelper(String keyPrefix, BackupHelper helper) { 136 Class<? extends BackupHelper> helperClass = helper.getClass(); 137 final BackupHelperSimulator simulator; 138 if (helperClass == SharedPreferencesBackupHelper.class) { 139 simulator = SharedPreferencesBackupHelperSimulator.fromHelper( 140 keyPrefix, (SharedPreferencesBackupHelper) helper); 141 } else if (helperClass == FileBackupHelper.class) { 142 simulator = FileBackupHelperSimulator.fromHelper(keyPrefix, (FileBackupHelper) helper); 143 } else { 144 throw new UnsupportedOperationException( 145 "Unknown backup helper class for key prefix \"" + keyPrefix + "\": " + helperClass); 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