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