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 }