• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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