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