Home | History | Annotate | Download | only in shadow
      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    */
     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    */
     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 
    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 
    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
    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
    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
    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