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