Implemented backup and restore for account sync settings.
Bug: 18506992
Parent Bug: 17967106
Change-Id: Iecf5ce3a24ac5f12192e65aa61f1560bc76d75a8
diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
new file mode 100644
index 0000000..3f4b980
--- /dev/null
+++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
@@ -0,0 +1,380 @@
+/*
+ * 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) {
+
+ }
+}
diff --git a/core/java/com/android/server/backup/SystemBackupAgent.java b/core/java/com/android/server/backup/SystemBackupAgent.java
index 35a1a5a..b5f2f37 100644
--- a/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -86,6 +86,8 @@
}
addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, files, keys));
addHelper("recents", new RecentsBackupHelper(SystemBackupAgent.this));
+ addHelper("account_sync_settings",
+ new AccountSyncSettingsBackupHelper(SystemBackupAgent.this));
super.onBackup(oldState, data, newState);
}
@@ -118,6 +120,8 @@
new String[] { WALLPAPER_IMAGE },
new String[] { WALLPAPER_IMAGE_KEY} ));
addHelper("recents", new RecentsBackupHelper(SystemBackupAgent.this));
+ addHelper("account_sync_settings",
+ new AccountSyncSettingsBackupHelper(SystemBackupAgent.this));
try {
super.onRestore(data, appVersionCode, newState);
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index 0d5f240..f154c73 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -18,6 +18,7 @@
import android.accounts.Account;
import android.accounts.AccountAndUser;
+import android.app.backup.BackupManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@@ -670,6 +671,7 @@
new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+ queueBackup();
}
public int getIsSyncable(Account account, int userId, String providerName) {
@@ -1035,6 +1037,7 @@
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
mContext.sendBroadcast(ContentResolver.ACTION_SYNC_CONN_STATUS_CHANGED);
+ queueBackup();
}
public boolean getMasterSyncAutomatically(int userId) {
@@ -2810,4 +2813,12 @@
.append(")\n");
}
}
+
+ /**
+ * Let the BackupManager know that account sync settings have changed. This will trigger
+ * {@link com.android.server.backup.SystemBackupAgent} to run.
+ */
+ public void queueBackup() {
+ BackupManager.dataChanged("android");
+ }
}