|  | /* | 
|  | * Copyright (C) 2014 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package com.android.server.backup; | 
|  |  | 
|  | import android.accounts.Account; | 
|  | import android.accounts.AccountManager; | 
|  | import android.app.backup.BackupDataInputStream; | 
|  | import android.app.backup.BackupDataOutput; | 
|  | import android.app.backup.BackupHelper; | 
|  | import android.content.ContentResolver; | 
|  | import android.content.Context; | 
|  | import android.content.SyncAdapterType; | 
|  | import android.os.Environment; | 
|  | import android.os.ParcelFileDescriptor; | 
|  | import android.util.Log; | 
|  |  | 
|  | import org.json.JSONArray; | 
|  | import org.json.JSONException; | 
|  | import org.json.JSONObject; | 
|  |  | 
|  | import java.io.BufferedOutputStream; | 
|  | import java.io.DataInputStream; | 
|  | import java.io.DataOutputStream; | 
|  | import java.io.EOFException; | 
|  | import java.io.File; | 
|  | import java.io.FileInputStream; | 
|  | import java.io.FileNotFoundException; | 
|  | import java.io.FileOutputStream; | 
|  | import java.io.IOException; | 
|  | import java.security.MessageDigest; | 
|  | import java.security.NoSuchAlgorithmException; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.HashMap; | 
|  | import java.util.HashSet; | 
|  | import java.util.List; | 
|  |  | 
|  | /** | 
|  | * Helper for backing up account sync settings (whether or not a service should be synced). The | 
|  | * sync settings are backed up as a JSON object containing all the necessary information for | 
|  | * restoring the sync settings later. | 
|  | */ | 
|  | public class AccountSyncSettingsBackupHelper implements BackupHelper { | 
|  |  | 
|  | private static final String TAG = "AccountSyncSettingsBackupHelper"; | 
|  | private static final boolean DEBUG = false; | 
|  |  | 
|  | private static final int STATE_VERSION = 1; | 
|  | private static final int MD5_BYTE_SIZE = 16; | 
|  | private static final int SYNC_REQUEST_LATCH_TIMEOUT_SECONDS = 1; | 
|  |  | 
|  | private static final String JSON_FORMAT_HEADER_KEY = "account_data"; | 
|  | private static final String JSON_FORMAT_ENCODING = "UTF-8"; | 
|  | private static final int JSON_FORMAT_VERSION = 1; | 
|  |  | 
|  | private static final String KEY_VERSION = "version"; | 
|  | private static final String KEY_MASTER_SYNC_ENABLED = "masterSyncEnabled"; | 
|  | private static final String KEY_ACCOUNTS = "accounts"; | 
|  | private static final String KEY_ACCOUNT_NAME = "name"; | 
|  | private static final String KEY_ACCOUNT_TYPE = "type"; | 
|  | private static final String KEY_ACCOUNT_AUTHORITIES = "authorities"; | 
|  | private static final String KEY_AUTHORITY_NAME = "name"; | 
|  | private static final String KEY_AUTHORITY_SYNC_STATE = "syncState"; | 
|  | private static final String KEY_AUTHORITY_SYNC_ENABLED = "syncEnabled"; | 
|  | private static final String STASH_FILE = Environment.getDataDirectory() | 
|  | + "/backup/unadded_account_syncsettings.json"; | 
|  |  | 
|  | private Context mContext; | 
|  | private AccountManager mAccountManager; | 
|  |  | 
|  | public AccountSyncSettingsBackupHelper(Context context) { | 
|  | mContext = context; | 
|  | mAccountManager = AccountManager.get(mContext); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Take a snapshot of the current account sync settings and write them to the given output. | 
|  | */ | 
|  | @Override | 
|  | public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput output, | 
|  | ParcelFileDescriptor newState) { | 
|  | try { | 
|  | JSONObject dataJSON = serializeAccountSyncSettingsToJSON(); | 
|  |  | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "Account sync settings JSON: " + dataJSON); | 
|  | } | 
|  |  | 
|  | // Encode JSON data to bytes. | 
|  | byte[] dataBytes = dataJSON.toString().getBytes(JSON_FORMAT_ENCODING); | 
|  | byte[] oldMd5Checksum = readOldMd5Checksum(oldState); | 
|  | byte[] newMd5Checksum = generateMd5Checksum(dataBytes); | 
|  | if (!Arrays.equals(oldMd5Checksum, newMd5Checksum)) { | 
|  | int dataSize = dataBytes.length; | 
|  | output.writeEntityHeader(JSON_FORMAT_HEADER_KEY, dataSize); | 
|  | output.writeEntityData(dataBytes, dataSize); | 
|  |  | 
|  | Log.i(TAG, "Backup successful."); | 
|  | } else { | 
|  | Log.i(TAG, "Old and new MD5 checksums match. Skipping backup."); | 
|  | } | 
|  |  | 
|  | writeNewMd5Checksum(newState, newMd5Checksum); | 
|  | } catch (JSONException | IOException | NoSuchAlgorithmException e) { | 
|  | Log.e(TAG, "Couldn't backup account sync settings\n" + e); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Fetch and serialize Account and authority information as a JSON Array. | 
|  | */ | 
|  | private JSONObject serializeAccountSyncSettingsToJSON() throws JSONException { | 
|  | Account[] accounts = mAccountManager.getAccounts(); | 
|  | SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( | 
|  | mContext.getUserId()); | 
|  |  | 
|  | // Create a map of Account types to authorities. Later this will make it easier for us to | 
|  | // generate our JSON. | 
|  | HashMap<String, List<String>> accountTypeToAuthorities = new HashMap<String, | 
|  | List<String>>(); | 
|  | for (SyncAdapterType syncAdapter : syncAdapters) { | 
|  | // Skip adapters that aren’t visible to the user. | 
|  | if (!syncAdapter.isUserVisible()) { | 
|  | continue; | 
|  | } | 
|  | if (!accountTypeToAuthorities.containsKey(syncAdapter.accountType)) { | 
|  | accountTypeToAuthorities.put(syncAdapter.accountType, new ArrayList<String>()); | 
|  | } | 
|  | accountTypeToAuthorities.get(syncAdapter.accountType).add(syncAdapter.authority); | 
|  | } | 
|  |  | 
|  | // Generate JSON. | 
|  | JSONObject backupJSON = new JSONObject(); | 
|  | backupJSON.put(KEY_VERSION, JSON_FORMAT_VERSION); | 
|  | backupJSON.put(KEY_MASTER_SYNC_ENABLED, ContentResolver.getMasterSyncAutomatically()); | 
|  |  | 
|  | JSONArray accountJSONArray = new JSONArray(); | 
|  | for (Account account : accounts) { | 
|  | List<String> authorities = accountTypeToAuthorities.get(account.type); | 
|  |  | 
|  | // We ignore Accounts that don't have any authorities because there would be no sync | 
|  | // settings for us to restore. | 
|  | if (authorities == null || authorities.isEmpty()) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | JSONObject accountJSON = new JSONObject(); | 
|  | accountJSON.put(KEY_ACCOUNT_NAME, account.name); | 
|  | accountJSON.put(KEY_ACCOUNT_TYPE, account.type); | 
|  |  | 
|  | // Add authorities for this Account type and check whether or not sync is enabled. | 
|  | JSONArray authoritiesJSONArray = new JSONArray(); | 
|  | for (String authority : authorities) { | 
|  | int syncState = ContentResolver.getIsSyncable(account, authority); | 
|  | boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); | 
|  |  | 
|  | JSONObject authorityJSON = new JSONObject(); | 
|  | authorityJSON.put(KEY_AUTHORITY_NAME, authority); | 
|  | authorityJSON.put(KEY_AUTHORITY_SYNC_STATE, syncState); | 
|  | authorityJSON.put(KEY_AUTHORITY_SYNC_ENABLED, syncEnabled); | 
|  | authoritiesJSONArray.put(authorityJSON); | 
|  | } | 
|  | accountJSON.put(KEY_ACCOUNT_AUTHORITIES, authoritiesJSONArray); | 
|  |  | 
|  | accountJSONArray.put(accountJSON); | 
|  | } | 
|  | backupJSON.put(KEY_ACCOUNTS, accountJSONArray); | 
|  |  | 
|  | return backupJSON; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Read the MD5 checksum from the old state. | 
|  | * | 
|  | * @return the old MD5 checksum | 
|  | */ | 
|  | private byte[] readOldMd5Checksum(ParcelFileDescriptor oldState) throws IOException { | 
|  | DataInputStream dataInput = new DataInputStream( | 
|  | new FileInputStream(oldState.getFileDescriptor())); | 
|  |  | 
|  | byte[] oldMd5Checksum = new byte[MD5_BYTE_SIZE]; | 
|  | try { | 
|  | int stateVersion = dataInput.readInt(); | 
|  | if (stateVersion <= STATE_VERSION) { | 
|  | // If the state version is a version we can understand then read the MD5 sum, | 
|  | // otherwise we return an empty byte array for the MD5 sum which will force a | 
|  | // backup. | 
|  | for (int i = 0; i < MD5_BYTE_SIZE; i++) { | 
|  | oldMd5Checksum[i] = dataInput.readByte(); | 
|  | } | 
|  | } else { | 
|  | Log.i(TAG, "Backup state version is: " + stateVersion | 
|  | + " (support only up to version " + STATE_VERSION + ")"); | 
|  | } | 
|  | } catch (EOFException eof) { | 
|  | // Initial state may be empty. | 
|  | } | 
|  | // We explicitly don't close 'dataInput' because we must not close the backing fd. | 
|  | return oldMd5Checksum; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Write the given checksum to the file descriptor. | 
|  | */ | 
|  | private void writeNewMd5Checksum(ParcelFileDescriptor newState, byte[] md5Checksum) | 
|  | throws IOException { | 
|  | DataOutputStream dataOutput = new DataOutputStream( | 
|  | new BufferedOutputStream(new FileOutputStream(newState.getFileDescriptor()))); | 
|  |  | 
|  | dataOutput.writeInt(STATE_VERSION); | 
|  | dataOutput.write(md5Checksum); | 
|  |  | 
|  | // We explicitly don't close 'dataOutput' because we must not close the backing fd. | 
|  | // The FileOutputStream will not close it implicitly. | 
|  |  | 
|  | } | 
|  |  | 
|  | private byte[] generateMd5Checksum(byte[] data) throws NoSuchAlgorithmException { | 
|  | if (data == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | MessageDigest md5 = MessageDigest.getInstance("MD5"); | 
|  | return md5.digest(data); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Restore account sync settings from the given data input stream. | 
|  | */ | 
|  | @Override | 
|  | public void restoreEntity(BackupDataInputStream data) { | 
|  | byte[] dataBytes = new byte[data.size()]; | 
|  | try { | 
|  | // Read the data and convert it to a String. | 
|  | data.read(dataBytes); | 
|  | String dataString = new String(dataBytes, JSON_FORMAT_ENCODING); | 
|  |  | 
|  | // Convert data to a JSON object. | 
|  | JSONObject dataJSON = new JSONObject(dataString); | 
|  | boolean masterSyncEnabled = dataJSON.getBoolean(KEY_MASTER_SYNC_ENABLED); | 
|  | JSONArray accountJSONArray = dataJSON.getJSONArray(KEY_ACCOUNTS); | 
|  |  | 
|  | boolean currentMasterSyncEnabled = ContentResolver.getMasterSyncAutomatically(); | 
|  | if (currentMasterSyncEnabled) { | 
|  | // Disable master sync to prevent any syncs from running. | 
|  | ContentResolver.setMasterSyncAutomatically(false); | 
|  | } | 
|  |  | 
|  | try { | 
|  | restoreFromJsonArray(accountJSONArray); | 
|  | } finally { | 
|  | // Set the master sync preference to the value from the backup set. | 
|  | ContentResolver.setMasterSyncAutomatically(masterSyncEnabled); | 
|  | } | 
|  | Log.i(TAG, "Restore successful."); | 
|  | } catch (IOException | JSONException e) { | 
|  | Log.e(TAG, "Couldn't restore account sync settings\n" + e); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void restoreFromJsonArray(JSONArray accountJSONArray) | 
|  | throws JSONException { | 
|  | HashSet<Account> currentAccounts = getAccounts(); | 
|  | JSONArray unaddedAccountsJSONArray = new JSONArray(); | 
|  | for (int i = 0; i < accountJSONArray.length(); i++) { | 
|  | JSONObject accountJSON = (JSONObject) accountJSONArray.get(i); | 
|  | String accountName = accountJSON.getString(KEY_ACCOUNT_NAME); | 
|  | String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE); | 
|  |  | 
|  | Account account = null; | 
|  | try { | 
|  | account = new Account(accountName, accountType); | 
|  | } catch (IllegalArgumentException iae) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Check if the account already exists. Accounts that don't exist on the device | 
|  | // yet won't be restored. | 
|  | if (currentAccounts.contains(account)) { | 
|  | if (DEBUG) Log.i(TAG, "Restoring Sync Settings for" + accountName); | 
|  | restoreExistingAccountSyncSettingsFromJSON(accountJSON); | 
|  | } else { | 
|  | unaddedAccountsJSONArray.put(accountJSON); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (unaddedAccountsJSONArray.length() > 0) { | 
|  | try (FileOutputStream fOutput = new FileOutputStream(STASH_FILE)) { | 
|  | String jsonString = unaddedAccountsJSONArray.toString(); | 
|  | DataOutputStream out = new DataOutputStream(fOutput); | 
|  | out.writeUTF(jsonString); | 
|  | } catch (IOException ioe) { | 
|  | // Error in writing to stash file | 
|  | Log.e(TAG, "unable to write the sync settings to the stash file", ioe); | 
|  | } | 
|  | } else { | 
|  | File stashFile = new File(STASH_FILE); | 
|  | if (stashFile.exists()) stashFile.delete(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Restore SyncSettings for all existing accounts from a stashed backup-set | 
|  | */ | 
|  | private void accountAddedInternal() { | 
|  | String jsonString; | 
|  |  | 
|  | try (FileInputStream fIn = new FileInputStream(new File(STASH_FILE))) { | 
|  | DataInputStream in = new DataInputStream(fIn); | 
|  | jsonString = in.readUTF(); | 
|  | } catch (FileNotFoundException fnfe) { | 
|  | // This is expected to happen when there is no accounts info stashed | 
|  | if (DEBUG) Log.d(TAG, "unable to find the stash file", fnfe); | 
|  | return; | 
|  | } catch (IOException ioe) { | 
|  | if (DEBUG) Log.d(TAG, "could not read sync settings from stash file", ioe); | 
|  | return; | 
|  | } | 
|  |  | 
|  | try { | 
|  | JSONArray unaddedAccountsJSONArray = new JSONArray(jsonString); | 
|  | restoreFromJsonArray(unaddedAccountsJSONArray); | 
|  | } catch (JSONException jse) { | 
|  | // Malformed jsonString | 
|  | Log.e(TAG, "there was an error with the stashed sync settings", jse); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Restore SyncSettings for all existing accounts from a stashed backup-set | 
|  | */ | 
|  | public static void accountAdded(Context context) { | 
|  | AccountSyncSettingsBackupHelper helper = new AccountSyncSettingsBackupHelper(context); | 
|  | helper.accountAddedInternal(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Helper method - fetch accounts and return them as a HashSet. | 
|  | * | 
|  | * @return Accounts in a HashSet. | 
|  | */ | 
|  | private HashSet<Account> getAccounts() { | 
|  | Account[] accounts = mAccountManager.getAccounts(); | 
|  | HashSet<Account> accountHashSet = new HashSet<Account>(); | 
|  | for (Account account : accounts) { | 
|  | accountHashSet.add(account); | 
|  | } | 
|  | return accountHashSet; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Restore account sync settings using the given JSON. This function won't work if the account | 
|  | * doesn't exist yet. | 
|  | * This function will only be called during Setup Wizard, where we are guaranteed that there | 
|  | * are no active syncs. | 
|  | * There are 2 pieces of data to restore - | 
|  | *      isSyncable (corresponds to {@link ContentResolver#getIsSyncable(Account, String)} | 
|  | *      syncEnabled (corresponds to {@link ContentResolver#getSyncAutomatically(Account, String)} | 
|  | * <strong>The restore favours adapters that were enabled on the old device, and doesn't care | 
|  | * about adapters that were disabled.</strong> | 
|  | * | 
|  | * syncEnabled=true in restore data. | 
|  | * syncEnabled will be true on this device. isSyncable will be left as the default in order to | 
|  | * give the enabled adapter the chance to run an initialization sync. | 
|  | * | 
|  | * syncEnabled=false in restore data. | 
|  | * syncEnabled will be false on this device. isSyncable will be set to 2, unless it was 0 on the | 
|  | * old device in which case it will be set to 0 on this device. This is because isSyncable=0 is | 
|  | * a rare state and was probably set to 0 for good reason (historically isSyncable is a way by | 
|  | * which adapters control their own sync state independently of sync settings which is | 
|  | * toggleable by the user). | 
|  | * isSyncable=2 is a new isSyncable state we introduced specifically to allow adapters that are | 
|  | * disabled after a restore to run initialization logic when the adapter is later enabled. | 
|  | * See com.android.server.content.SyncStorageEngine#setSyncAutomatically | 
|  | * | 
|  | * The end result is that an adapter that the user had on will be turned on and get an | 
|  | * initialization sync, while an adapter that the user had off will be off until the user | 
|  | * enables it on this device at which point it will get an initialization sync. | 
|  | */ | 
|  | private void restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON) | 
|  | throws JSONException { | 
|  | // Restore authorities. | 
|  | JSONArray authorities = accountJSON.getJSONArray(KEY_ACCOUNT_AUTHORITIES); | 
|  | String accountName = accountJSON.getString(KEY_ACCOUNT_NAME); | 
|  | String accountType = accountJSON.getString(KEY_ACCOUNT_TYPE); | 
|  |  | 
|  | final Account account = new Account(accountName, accountType); | 
|  | for (int i = 0; i < authorities.length(); i++) { | 
|  | JSONObject authority = (JSONObject) authorities.get(i); | 
|  | final String authorityName = authority.getString(KEY_AUTHORITY_NAME); | 
|  | boolean wasSyncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED); | 
|  | int wasSyncable = authority.getInt(KEY_AUTHORITY_SYNC_STATE); | 
|  |  | 
|  | ContentResolver.setSyncAutomaticallyAsUser( | 
|  | account, authorityName, wasSyncEnabled, 0 /* user Id */); | 
|  |  | 
|  | if (!wasSyncEnabled) { | 
|  | ContentResolver.setIsSyncable( | 
|  | account, | 
|  | authorityName, | 
|  | wasSyncable == 0 ? | 
|  | 0 /* not syncable */ : 2 /* syncable but needs initialization */); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void writeNewStateDescription(ParcelFileDescriptor newState) { | 
|  |  | 
|  | } | 
|  | } |