| package com.google.android.libraries.backup.shadow; |
| |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.app.backup.BackupAgent; |
| import android.app.backup.BackupAgentHelper; |
| import android.app.backup.BackupDataInput; |
| import android.app.backup.BackupDataOutput; |
| import android.app.backup.BackupHelper; |
| import android.app.backup.FileBackupHelper; |
| import android.app.backup.SharedPreferencesBackupHelper; |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.os.ParcelFileDescriptor; |
| import android.util.Log; |
| import com.google.common.collect.ImmutableMap; |
| import java.io.IOException; |
| import java.lang.reflect.Method; |
| import java.util.Map; |
| import java.util.TreeMap; |
| import java.util.concurrent.atomic.AtomicReference; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| |
| /** |
| * Shadow class for end-to-end testing of {@link BackupAgentHelper} subclasses in unit tests. |
| * |
| * <p>This class currently supports <b>key-value backups only</b>. In other words, it does |
| * <b>not</b> support Dolly. In addition, the testing framework has the following two limitations |
| * with regards to backup/restore of {@link SharedPreferences}: |
| * |
| * <ol> |
| * <li>Preferences are normally backed by xml files in the app's shared_prefs directory, but |
| * Robolectric replaces them with {@link RoboSharedPreferences}, which are backed by an in-memory |
| * {@link Map}. Therefore, modifying the relevant xml files will have no effect on the preferences |
| * (and vice versa). |
| * <li>For the same reason, the testing framework cannot easily determine whether the underlying |
| * xml file for given shared preferences would have been empty or missing upon backup. The latter |
| * is assumed to ensure that apps don't rely on restore to implicitly clear data (potentially |
| * PII). |
| * </ol> |
| */ |
| @Implements(BackupAgentHelper.class) |
| public class BackupAgentHelperShadow { |
| private static final String TAG = "BackupAgentHelperShadow"; |
| |
| /** |
| * Temporarily stores the backup data generated in {@link #onBackup} so that it could be returned |
| * by {@link #simulateBackup}. |
| */ |
| private static final AtomicReference<Map<String, Object>> backupDataMapToBackup = |
| new AtomicReference<>(); |
| |
| /** |
| * Temporarily stores the backed up data passed to {@link #simulateRestore} so that it could be |
| * used in {@link #onRestore}. |
| */ |
| private static final AtomicReference<Map<String, Object>> backupDataMapToRestore = |
| new AtomicReference<>(); |
| |
| /** |
| * Simulates key-value backup for the provided agent all the way from {@link |
| * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). |
| */ |
| public static Map<String, Object> simulateBackup(BackupAgentHelper agent) { |
| Map<String, Object> backupDataMap; |
| attachBaseContextToAgentIfNecessary(agent); |
| agent.onCreate(); |
| try { |
| agent.onBackup(null, null, null); |
| backupDataMap = backupDataMapToBackup.getAndSet(null); |
| } catch (IOException e) { |
| backupDataMapToBackup.set(null); |
| throw new IllegalStateException(e); |
| } |
| agent.onDestroy(); |
| return backupDataMap; |
| } |
| |
| /** |
| * Simulates key-value restore for the provided agent all the way from {@link |
| * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive). |
| * |
| * <p>Note: To make end-to-end tests more realistic, <b>different {@link BackupAgentHelper} |
| * instances</b> should be used in {@link #simulateBackup} and {@link #simulateRestore}. |
| */ |
| public static void simulateRestore( |
| BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode) { |
| attachBaseContextToAgentIfNecessary(agent); |
| agent.onCreate(); |
| assertTrue(backupDataMapToRestore.compareAndSet(null, backupDataMap)); |
| try { |
| agent.onRestore(null, appVersionCode, null); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } finally { |
| backupDataMapToRestore.set(null); |
| } |
| if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { |
| agent.onRestoreFinished(); |
| } |
| agent.onDestroy(); |
| } |
| |
| private static void attachBaseContextToAgentIfNecessary(BackupAgentHelper agent) { |
| if (agent.getBaseContext() != null) { |
| return; |
| } |
| try { |
| // {@link BackupAgent#attach} is a hidden method, so we need to call it via reflection. |
| Method method = BackupAgent.class.getMethod("attach", Context.class); |
| method.invoke(agent, RuntimeEnvironment.application); |
| } catch (ReflectiveOperationException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| private final Map<String, BackupHelperSimulator> helperSimulators; |
| |
| public BackupAgentHelperShadow() { |
| // Use a {@link TreeMap} to mirror the internal implementation of {@link BackupHelperDispatcher} |
| // as closely as possible. |
| helperSimulators = new TreeMap<>(); |
| } |
| |
| @RealObject private BackupAgentHelper realHelper; |
| |
| @Implementation |
| public void addHelper(String keyPrefix, BackupHelper helper) { |
| Class<? extends BackupHelper> helperClass = helper.getClass(); |
| final BackupHelperSimulator simulator; |
| if (helperClass == SharedPreferencesBackupHelper.class) { |
| simulator = SharedPreferencesBackupHelperSimulator.fromHelper( |
| keyPrefix, (SharedPreferencesBackupHelper) helper); |
| } else if (helperClass == FileBackupHelper.class) { |
| simulator = FileBackupHelperSimulator.fromHelper(keyPrefix, (FileBackupHelper) helper); |
| } else { |
| Log.w( |
| TAG, "Unknown backup helper class for key prefix \"" + keyPrefix + "\": " + helperClass); |
| simulator = new UnsupportedBackupHelperSimulator(keyPrefix, helper); |
| } |
| helperSimulators.put(keyPrefix, simulator); |
| } |
| |
| @Implementation |
| public void onBackup( |
| ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) |
| throws IOException { |
| ImmutableMap.Builder<String, Object> backupDataMapBuilder = ImmutableMap.builder(); |
| for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) { |
| String keyPrefix = simulatorEntry.getKey(); |
| BackupHelperSimulator simulator = simulatorEntry.getValue(); |
| backupDataMapBuilder.put(keyPrefix, simulator.backup(realHelper)); |
| } |
| |
| assertTrue(backupDataMapToBackup.compareAndSet(null, backupDataMapBuilder.build())); |
| } |
| |
| @Implementation |
| public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) |
| throws IOException { |
| Map<String, Object> backupDataMap = backupDataMapToRestore.getAndSet(null); |
| assertNotNull(backupDataMap); |
| |
| for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) { |
| String keyPrefix = simulatorEntry.getKey(); |
| Object dataToRestore = backupDataMap.get(keyPrefix); |
| if (dataToRestore == null) { |
| Log.w(TAG, "No data to restore for key prefix: \"" + keyPrefix + "\"."); |
| continue; |
| } |
| BackupHelperSimulator simulator = simulatorEntry.getValue(); |
| simulator.restore(realHelper, dataToRestore); |
| } |
| } |
| } |