blob: 5615723d7429ce3ce1db6f298900fd5c48b8793c [file] [log] [blame]
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);
}
}
}