Home | History | Annotate | Download | only in backup
      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 
     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 arent 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 }