• 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 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