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