blob: 3f4b9808e114b220ac5d8ae3100cbb4b92e3d893 [file] [log] [blame]
/*
* 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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
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.content.SyncStatusObserver;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
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;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 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 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.
} finally {
dataInput.close();
}
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);
dataOutput.close();
}
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 {
HashSet<Account> currentAccounts = getAccountsHashSet();
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 = new Account(accountName, accountType);
// Check if the account already exists. Accounts that don't exist on the device
// yet won't be restored.
if (currentAccounts.contains(account)) {
restoreExistingAccountSyncSettingsFromJSON(accountJSON);
}
}
} 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);
}
}
/**
* Helper method - fetch accounts and return them as a HashSet.
*
* @return Accounts in a HashSet.
*/
private HashSet<Account> getAccountsHashSet() {
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.
*/
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 syncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED);
// Cancel any active syncs.
if (ContentResolver.isSyncActive(account, authorityName)) {
ContentResolver.cancelSync(account, authorityName);
}
boolean overwriteSync = true;
Bundle initializationExtras = createSyncInitializationBundle();
int currentSyncState = ContentResolver.getIsSyncable(account, authorityName);
if (currentSyncState < 0) {
// Requesting a sync is an asynchronous operation, so we setup a countdown latch to
// wait for it to finish. Initialization syncs are generally very brief and
// shouldn't take too much time to finish.
final CountDownLatch latch = new CountDownLatch(1);
Object syncStatusObserverHandle = ContentResolver.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
if (!ContentResolver.isSyncActive(account, authorityName)) {
latch.countDown();
}
}
});
// If we set sync settings for a sync that hasn't been initialized yet, we run the
// risk of having our changes overwritten later on when the sync gets initialized.
// To prevent this from happening we will manually initiate the sync adapter. We
// also explicitly pass in a Bundle with SYNC_EXTRAS_INITIALIZE to prevent a data
// sync from running after the initialization sync. Two syncs will be scheduled, but
// the second one (data sync) will override the first one (initialization sync) and
// still behave as an initialization sync because of the Bundle.
ContentResolver.requestSync(account, authorityName, initializationExtras);
boolean done = false;
try {
done = latch.await(SYNC_REQUEST_LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.e(TAG, "CountDownLatch interrupted\n" + e);
done = false;
}
if (!done) {
overwriteSync = false;
Log.i(TAG, "CountDownLatch timed out, skipping '" + authorityName
+ "' authority.");
}
ContentResolver.removeStatusChangeListener(syncStatusObserverHandle);
}
if (overwriteSync) {
ContentResolver.setSyncAutomatically(account, authorityName, syncEnabled);
Log.i(TAG, "Set sync automatically for '" + authorityName + "': " + syncEnabled);
}
}
}
private Bundle createSyncInitializationBundle() {
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
return extras;
}
@Override
public void writeNewStateDescription(ParcelFileDescriptor newState) {
}
}