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
17package com.android.server.backup;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.app.backup.BackupDataInputStream;
22import android.app.backup.BackupDataOutput;
23import android.app.backup.BackupHelper;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.SyncAdapterType;
27import android.os.Environment;
28import android.os.ParcelFileDescriptor;
29import android.util.Log;
30
31import org.json.JSONArray;
32import org.json.JSONException;
33import org.json.JSONObject;
34
35import java.io.BufferedOutputStream;
36import java.io.DataInputStream;
37import java.io.DataOutputStream;
38import java.io.EOFException;
39import java.io.File;
40import java.io.FileInputStream;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.security.MessageDigest;
45import java.security.NoSuchAlgorithmException;
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.HashMap;
49import java.util.HashSet;
50import 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 */
57public 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
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
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     */
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     */
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     */
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
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
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
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     */
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     */
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     */
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     */
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
422    public void writeNewStateDescription(ParcelFileDescriptor newState) {
423
424    }
425}