1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.backup;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.app.backup.BackupDataInputStream;
22 import android.app.backup.BackupDataOutput;
23 import android.app.backup.BackupHelper;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.SyncAdapterType;
27 import android.os.Environment;
28 import android.os.ParcelFileDescriptor;
29 import android.os.UserHandle;
30 import android.util.Log;
31 
32 import org.json.JSONArray;
33 import org.json.JSONException;
34 import org.json.JSONObject;
35 
36 import java.io.BufferedOutputStream;
37 import java.io.DataInputStream;
38 import java.io.DataOutputStream;
39 import java.io.EOFException;
40 import java.io.File;
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.security.MessageDigest;
46 import java.security.NoSuchAlgorithmException;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Set;
53 
54 /**
55  * Helper for backing up account sync settings (whether or not a service should be synced). The
56  * sync settings are backed up as a JSON object containing all the necessary information for
57  * restoring the sync settings later.
58  */
59 public class AccountSyncSettingsBackupHelper implements BackupHelper {
60 
61     private static final String TAG = "AccountSyncSettingsBackupHelper";
62     private static final boolean DEBUG = false;
63 
64     private static final int STATE_VERSION = 1;
65     private static final int MD5_BYTE_SIZE = 16;
66     private static final int SYNC_REQUEST_LATCH_TIMEOUT_SECONDS = 1;
67 
68     private static final String JSON_FORMAT_HEADER_KEY = "account_data";
69     private static final String JSON_FORMAT_ENCODING = "UTF-8";
70     private static final int JSON_FORMAT_VERSION = 1;
71 
72     private static final String KEY_VERSION = "version";
73     private static final String KEY_MASTER_SYNC_ENABLED = "masterSyncEnabled";
74     private static final String KEY_ACCOUNTS = "accounts";
75     private static final String KEY_ACCOUNT_NAME = "name";
76     private static final String KEY_ACCOUNT_TYPE = "type";
77     private static final String KEY_ACCOUNT_AUTHORITIES = "authorities";
78     private static final String KEY_AUTHORITY_NAME = "name";
79     private static final String KEY_AUTHORITY_SYNC_STATE = "syncState";
80     private static final String KEY_AUTHORITY_SYNC_ENABLED = "syncEnabled";
81     private static final String STASH_FILE = "/backup/unadded_account_syncsettings.json";
82 
83     private Context mContext;
84     private AccountManager mAccountManager;
85     private final int mUserId;
86 
AccountSyncSettingsBackupHelper(Context context, int userId)87     public AccountSyncSettingsBackupHelper(Context context, int userId) {
88         mContext = context;
89         mAccountManager = AccountManager.get(mContext);
90 
91         mUserId = userId;
92     }
93 
94     /**
95      * Take a snapshot of the current account sync settings and write them to the given output.
96      */
97     @Override
performBackup(ParcelFileDescriptor oldState, BackupDataOutput output, ParcelFileDescriptor newState)98     public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput output,
99             ParcelFileDescriptor newState) {
100         try {
101             JSONObject dataJSON = serializeAccountSyncSettingsToJSON(mUserId);
102 
103             if (DEBUG) {
104                 Log.d(TAG, "Account sync settings JSON: " + dataJSON);
105             }
106 
107             // Encode JSON data to bytes.
108             byte[] dataBytes = dataJSON.toString().getBytes(JSON_FORMAT_ENCODING);
109             byte[] oldMd5Checksum = readOldMd5Checksum(oldState);
110             byte[] newMd5Checksum = generateMd5Checksum(dataBytes);
111             if (!Arrays.equals(oldMd5Checksum, newMd5Checksum)) {
112                 int dataSize = dataBytes.length;
113                 output.writeEntityHeader(JSON_FORMAT_HEADER_KEY, dataSize);
114                 output.writeEntityData(dataBytes, dataSize);
115 
116                 Log.i(TAG, "Backup successful.");
117             } else {
118                 Log.i(TAG, "Old and new MD5 checksums match. Skipping backup.");
119             }
120 
121             writeNewMd5Checksum(newState, newMd5Checksum);
122         } catch (JSONException | IOException | NoSuchAlgorithmException e) {
123             Log.e(TAG, "Couldn't backup account sync settings\n" + e);
124         }
125     }
126 
127     /**
128      * Fetch and serialize Account and authority information as a JSON Array.
129      */
serializeAccountSyncSettingsToJSON(int userId)130     private JSONObject serializeAccountSyncSettingsToJSON(int userId) throws JSONException {
131         Account[] accounts = mAccountManager.getAccountsAsUser(userId);
132         SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
133 
134         // Create a map of Account types to authorities. Later this will make it easier for us to
135         // generate our JSON.
136         HashMap<String, List<String>> accountTypeToAuthorities = new HashMap<String,
137                 List<String>>();
138         for (SyncAdapterType syncAdapter : syncAdapters) {
139             // Skip adapters that aren’t visible to the user.
140             if (!syncAdapter.isUserVisible()) {
141                 continue;
142             }
143             if (!accountTypeToAuthorities.containsKey(syncAdapter.accountType)) {
144                 accountTypeToAuthorities.put(syncAdapter.accountType, new ArrayList<String>());
145             }
146             accountTypeToAuthorities.get(syncAdapter.accountType).add(syncAdapter.authority);
147         }
148 
149         // Generate JSON.
150         JSONObject backupJSON = new JSONObject();
151         backupJSON.put(KEY_VERSION, JSON_FORMAT_VERSION);
152         backupJSON.put(KEY_MASTER_SYNC_ENABLED, ContentResolver.getMasterSyncAutomaticallyAsUser(
153                 userId));
154 
155         JSONArray accountJSONArray = new JSONArray();
156         for (Account account : accounts) {
157             List<String> authorities = accountTypeToAuthorities.get(account.type);
158 
159             // We ignore Accounts that don't have any authorities because there would be no sync
160             // settings for us to restore.
161             if (authorities == null || authorities.isEmpty()) {
162                 continue;
163             }
164 
165             JSONObject accountJSON = new JSONObject();
166             accountJSON.put(KEY_ACCOUNT_NAME, account.name);
167             accountJSON.put(KEY_ACCOUNT_TYPE, account.type);
168 
169             // Add authorities for this Account type and check whether or not sync is enabled.
170             JSONArray authoritiesJSONArray = new JSONArray();
171             for (String authority : authorities) {
172                 int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId);
173                 boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
174                         userId);
175 
176                 JSONObject authorityJSON = new JSONObject();
177                 authorityJSON.put(KEY_AUTHORITY_NAME, authority);
178                 authorityJSON.put(KEY_AUTHORITY_SYNC_STATE, syncState);
179                 authorityJSON.put(KEY_AUTHORITY_SYNC_ENABLED, syncEnabled);
180                 authoritiesJSONArray.put(authorityJSON);
181             }
182             accountJSON.put(KEY_ACCOUNT_AUTHORITIES, authoritiesJSONArray);
183 
184             accountJSONArray.put(accountJSON);
185         }
186         backupJSON.put(KEY_ACCOUNTS, accountJSONArray);
187 
188         return backupJSON;
189     }
190 
191     /**
192      * Read the MD5 checksum from the old state.
193      *
194      * @return the old MD5 checksum
195      */
readOldMd5Checksum(ParcelFileDescriptor oldState)196     private byte[] readOldMd5Checksum(ParcelFileDescriptor oldState) throws IOException {
197         DataInputStream dataInput = new DataInputStream(
198                 new FileInputStream(oldState.getFileDescriptor()));
199 
200         byte[] oldMd5Checksum = new byte[MD5_BYTE_SIZE];
201         try {
202             int stateVersion = dataInput.readInt();
203             if (stateVersion <= STATE_VERSION) {
204                 // If the state version is a version we can understand then read the MD5 sum,
205                 // otherwise we return an empty byte array for the MD5 sum which will force a
206                 // backup.
207                 for (int i = 0; i < MD5_BYTE_SIZE; i++) {
208                     oldMd5Checksum[i] = dataInput.readByte();
209                 }
210             } else {
211                 Log.i(TAG, "Backup state version is: " + stateVersion
212                         + " (support only up to version " + STATE_VERSION + ")");
213             }
214         } catch (EOFException eof) {
215             // Initial state may be empty.
216         }
217         // We explicitly don't close 'dataInput' because we must not close the backing fd.
218         return oldMd5Checksum;
219     }
220 
221     /**
222      * Write the given checksum to the file descriptor.
223      */
writeNewMd5Checksum(ParcelFileDescriptor newState, byte[] md5Checksum)224     private void writeNewMd5Checksum(ParcelFileDescriptor newState, byte[] md5Checksum)
225             throws IOException {
226         DataOutputStream dataOutput = new DataOutputStream(
227                 new BufferedOutputStream(new FileOutputStream(newState.getFileDescriptor())));
228 
229         dataOutput.writeInt(STATE_VERSION);
230         dataOutput.write(md5Checksum);
231 
232         // We explicitly don't close 'dataOutput' because we must not close the backing fd.
233         // The FileOutputStream will not close it implicitly.
234 
235     }
236 
generateMd5Checksum(byte[] data)237     private byte[] generateMd5Checksum(byte[] data) throws NoSuchAlgorithmException {
238         if (data == null) {
239             return null;
240         }
241 
242         MessageDigest md5 = MessageDigest.getInstance("MD5");
243         return md5.digest(data);
244     }
245 
246     /**
247      * Restore account sync settings from the given data input stream.
248      */
249     @Override
restoreEntity(BackupDataInputStream data)250     public void restoreEntity(BackupDataInputStream data) {
251         byte[] dataBytes = new byte[data.size()];
252         try {
253             // Read the data and convert it to a String.
254             data.read(dataBytes);
255             String dataString = new String(dataBytes, JSON_FORMAT_ENCODING);
256 
257             // Convert data to a JSON object.
258             JSONObject dataJSON = new JSONObject(dataString);
259             boolean masterSyncEnabled = dataJSON.getBoolean(KEY_MASTER_SYNC_ENABLED);
260             JSONArray accountJSONArray = dataJSON.getJSONArray(KEY_ACCOUNTS);
261 
262             boolean currentMasterSyncEnabled = ContentResolver.getMasterSyncAutomaticallyAsUser(
263                     mUserId);
264             if (currentMasterSyncEnabled) {
265                 // Disable master sync to prevent any syncs from running.
266                 ContentResolver.setMasterSyncAutomaticallyAsUser(false, mUserId);
267             }
268 
269             try {
270                 restoreFromJsonArray(accountJSONArray, mUserId);
271             } finally {
272                 // Set the master sync preference to the value from the backup set.
273                 ContentResolver.setMasterSyncAutomaticallyAsUser(masterSyncEnabled, mUserId);
274             }
275             Log.i(TAG, "Restore successful.");
276         } catch (IOException | JSONException e) {
277             Log.e(TAG, "Couldn't restore account sync settings\n" + e);
278         }
279     }
280 
restoreFromJsonArray(JSONArray accountJSONArray, int userId)281     private void restoreFromJsonArray(JSONArray accountJSONArray, int userId)
282             throws JSONException {
283         Set<Account> currentAccounts = getAccounts(userId);
284         JSONArray unaddedAccountsJSONArray = new JSONArray();
285         for (int i = 0; i < accountJSONArray.length(); i++) {
286             JSONObject accountJSON = (JSONObject) accountJSONArray.get(i);
287             String accountName = accountJSON.getString(KEY_ACCOUNT_NAME);
288             String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE);
289 
290             Account account = null;
291             try {
292                 account = new Account(accountName, accountType);
293             } catch (IllegalArgumentException iae) {
294                 continue;
295             }
296 
297             // Check if the account already exists. Accounts that don't exist on the device
298             // yet won't be restored.
299             if (currentAccounts.contains(account)) {
300                 if (DEBUG) Log.i(TAG, "Restoring Sync Settings for" + accountName);
301                 restoreExistingAccountSyncSettingsFromJSON(accountJSON, userId);
302             } else {
303                 unaddedAccountsJSONArray.put(accountJSON);
304             }
305         }
306 
307         if (unaddedAccountsJSONArray.length() > 0) {
308             try (FileOutputStream fOutput = new FileOutputStream(getStashFile(userId))) {
309                 String jsonString = unaddedAccountsJSONArray.toString();
310                 DataOutputStream out = new DataOutputStream(fOutput);
311                 out.writeUTF(jsonString);
312             } catch (IOException ioe) {
313                 // Error in writing to stash file
314                 Log.e(TAG, "unable to write the sync settings to the stash file", ioe);
315             }
316         } else {
317             File stashFile = getStashFile(userId);
318             if (stashFile.exists()) {
319                 stashFile.delete();
320             }
321         }
322     }
323 
324     /**
325      * Restore SyncSettings for all existing accounts from a stashed backup-set
326      */
accountAddedInternal(int userId)327     private void accountAddedInternal(int userId) {
328         String jsonString;
329 
330         try (FileInputStream fIn = new FileInputStream(getStashFile(userId))) {
331             DataInputStream in = new DataInputStream(fIn);
332             jsonString = in.readUTF();
333         } catch (FileNotFoundException fnfe) {
334             // This is expected to happen when there is no accounts info stashed
335             if (DEBUG) Log.d(TAG, "unable to find the stash file", fnfe);
336             return;
337         } catch (IOException ioe) {
338             if (DEBUG) Log.d(TAG, "could not read sync settings from stash file", ioe);
339             return;
340         }
341 
342         try {
343             JSONArray unaddedAccountsJSONArray = new JSONArray(jsonString);
344             restoreFromJsonArray(unaddedAccountsJSONArray, userId);
345         } catch (JSONException jse) {
346             // Malformed jsonString
347             Log.e(TAG, "there was an error with the stashed sync settings", jse);
348         }
349     }
350 
351     /**
352      * Restore SyncSettings for all existing accounts from a stashed backup-set
353      */
accountAdded(Context context, int userId)354     public static void accountAdded(Context context, int userId) {
355         AccountSyncSettingsBackupHelper helper = new AccountSyncSettingsBackupHelper(context,
356                 userId);
357         helper.accountAddedInternal(userId);
358     }
359 
360     /**
361      * Helper method - fetch accounts and return them as a HashSet.
362      *
363      * @return Accounts in a HashSet.
364      */
getAccounts(int userId)365     private Set<Account> getAccounts(int userId) {
366         Account[] accounts = mAccountManager.getAccountsAsUser(userId);
367         Set<Account> accountHashSet = new HashSet<Account>();
368         for (Account account : accounts) {
369             accountHashSet.add(account);
370         }
371         return accountHashSet;
372     }
373 
374     /**
375      * Restore account sync settings using the given JSON. This function won't work if the account
376      * doesn't exist yet.
377      * This function will only be called during Setup Wizard, where we are guaranteed that there
378      * are no active syncs.
379      * There are 2 pieces of data to restore -
380      *      isSyncable (corresponds to {@link ContentResolver#getIsSyncable(Account, String)}
381      *      syncEnabled (corresponds to {@link ContentResolver#getSyncAutomatically(Account, String)}
382      * <strong>The restore favours adapters that were enabled on the old device, and doesn't care
383      * about adapters that were disabled.</strong>
384      *
385      * syncEnabled=true in restore data.
386      * syncEnabled will be true on this device. isSyncable will be left as the default in order to
387      * give the enabled adapter the chance to run an initialization sync.
388      *
389      * syncEnabled=false in restore data.
390      * syncEnabled will be false on this device. isSyncable will be set to 2, unless it was 0 on the
391      * old device in which case it will be set to 0 on this device. This is because isSyncable=0 is
392      * a rare state and was probably set to 0 for good reason (historically isSyncable is a way by
393      * which adapters control their own sync state independently of sync settings which is
394      * toggleable by the user).
395      * isSyncable=2 is a new isSyncable state we introduced specifically to allow adapters that are
396      * disabled after a restore to run initialization logic when the adapter is later enabled.
397      * See com.android.server.content.SyncStorageEngine#setSyncAutomatically
398      *
399      * The end result is that an adapter that the user had on will be turned on and get an
400      * initialization sync, while an adapter that the user had off will be off until the user
401      * enables it on this device at which point it will get an initialization sync.
402      */
restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON, int userId)403     private void restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON, int userId)
404             throws JSONException {
405         // Restore authorities.
406         JSONArray authorities = accountJSON.getJSONArray(KEY_ACCOUNT_AUTHORITIES);
407         String accountName = accountJSON.getString(KEY_ACCOUNT_NAME);
408         String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE);
409 
410         final Account account = new Account(accountName, accountType);
411         for (int i = 0; i < authorities.length(); i++) {
412             JSONObject authority = (JSONObject) authorities.get(i);
413             final String authorityName = authority.getString(KEY_AUTHORITY_NAME);
414             boolean wasSyncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED);
415             int wasSyncable = authority.getInt(KEY_AUTHORITY_SYNC_STATE);
416 
417             ContentResolver.setSyncAutomaticallyAsUser(
418                     account, authorityName, wasSyncEnabled, userId);
419 
420             if (!wasSyncEnabled) {
421                 ContentResolver.setIsSyncableAsUser(
422                         account,
423                         authorityName,
424                         wasSyncable == 0 ?
425                                 0 /* not syncable */ : 2 /* syncable but needs initialization */,
426                         userId);
427             }
428         }
429     }
430 
431     @Override
writeNewStateDescription(ParcelFileDescriptor newState)432     public void writeNewStateDescription(ParcelFileDescriptor newState) {
433 
434     }
435 
getStashFile(int userId)436     private static File getStashFile(int userId) {
437         File baseDir = userId == UserHandle.USER_SYSTEM ? Environment.getDataDirectory()
438                 : Environment.getDataSystemCeDirectory(userId);
439         return new File(baseDir, STASH_FILE);
440     }
441 }
442