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