443 lines
18 KiB
Java
443 lines
18 KiB
Java
![]() |
/*
|
|||
|
* 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 android.accounts.Account;
|
|||
|
import android.accounts.AccountManager;
|
|||
|
import android.app.backup.BackupDataInputStream;
|
|||
|
import android.app.backup.BackupDataOutput;
|
|||
|
import android.app.backup.BackupHelper;
|
|||
|
import android.app.backup.BackupHelperWithLogger;
|
|||
|
import android.content.ContentResolver;
|
|||
|
import android.content.Context;
|
|||
|
import android.content.SyncAdapterType;
|
|||
|
import android.os.Environment;
|
|||
|
import android.os.ParcelFileDescriptor;
|
|||
|
import android.os.UserHandle;
|
|||
|
import android.util.Log;
|
|||
|
|
|||
|
import org.json.JSONArray;
|
|||
|
import org.json.JSONException;
|
|||
|
import org.json.JSONObject;
|
|||
|
|
|||
|
import java.io.BufferedOutputStream;
|
|||
|
import java.io.DataInputStream;
|
|||
|
import java.io.DataOutputStream;
|
|||
|
import java.io.EOFException;
|
|||
|
import java.io.File;
|
|||
|
import java.io.FileInputStream;
|
|||
|
import java.io.FileNotFoundException;
|
|||
|
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.Set;
|
|||
|
|
|||
|
/**
|
|||
|
* 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 extends BackupHelperWithLogger {
|
|||
|
|
|||
|
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 static final String STASH_FILE = "/backup/unadded_account_syncsettings.json";
|
|||
|
|
|||
|
private Context mContext;
|
|||
|
private AccountManager mAccountManager;
|
|||
|
private final int mUserId;
|
|||
|
|
|||
|
public AccountSyncSettingsBackupHelper(Context context, int userId) {
|
|||
|
mContext = context;
|
|||
|
mAccountManager = AccountManager.get(mContext);
|
|||
|
|
|||
|
mUserId = userId;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 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(mUserId);
|
|||
|
|
|||
|
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(int userId) throws JSONException {
|
|||
|
Account[] accounts = mAccountManager.getAccountsAsUser(userId);
|
|||
|
SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
|
|||
|
|
|||
|
// 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.getMasterSyncAutomaticallyAsUser(
|
|||
|
userId));
|
|||
|
|
|||
|
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.getIsSyncableAsUser(account, authority, userId);
|
|||
|
boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
|
|||
|
userId);
|
|||
|
|
|||
|
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.
|
|||
|
}
|
|||
|
// We explicitly don't close 'dataInput' because we must not close the backing fd.
|
|||
|
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);
|
|||
|
|
|||
|
// We explicitly don't close 'dataOutput' because we must not close the backing fd.
|
|||
|
// The FileOutputStream will not close it implicitly.
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
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.getMasterSyncAutomaticallyAsUser(
|
|||
|
mUserId);
|
|||
|
if (currentMasterSyncEnabled) {
|
|||
|
// Disable global sync to prevent any syncs from running.
|
|||
|
ContentResolver.setMasterSyncAutomaticallyAsUser(false, mUserId);
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
restoreFromJsonArray(accountJSONArray, mUserId);
|
|||
|
} finally {
|
|||
|
// Set the global sync preference to the value from the backup set.
|
|||
|
ContentResolver.setMasterSyncAutomaticallyAsUser(masterSyncEnabled, mUserId);
|
|||
|
}
|
|||
|
Log.i(TAG, "Restore successful.");
|
|||
|
} catch (IOException | JSONException e) {
|
|||
|
Log.e(TAG, "Couldn't restore account sync settings\n" + e);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void restoreFromJsonArray(JSONArray accountJSONArray, int userId)
|
|||
|
throws JSONException {
|
|||
|
Set<Account> currentAccounts = getAccounts(userId);
|
|||
|
JSONArray unaddedAccountsJSONArray = new JSONArray();
|
|||
|
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 = null;
|
|||
|
try {
|
|||
|
account = new Account(accountName, accountType);
|
|||
|
} catch (IllegalArgumentException iae) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
// Check if the account already exists. Accounts that don't exist on the device
|
|||
|
// yet won't be restored.
|
|||
|
if (currentAccounts.contains(account)) {
|
|||
|
if (DEBUG) Log.i(TAG, "Restoring Sync Settings for" + accountName);
|
|||
|
restoreExistingAccountSyncSettingsFromJSON(accountJSON, userId);
|
|||
|
} else {
|
|||
|
unaddedAccountsJSONArray.put(accountJSON);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (unaddedAccountsJSONArray.length() > 0) {
|
|||
|
try (FileOutputStream fOutput = new FileOutputStream(getStashFile(userId))) {
|
|||
|
String jsonString = unaddedAccountsJSONArray.toString();
|
|||
|
DataOutputStream out = new DataOutputStream(fOutput);
|
|||
|
out.writeUTF(jsonString);
|
|||
|
} catch (IOException ioe) {
|
|||
|
// Error in writing to stash file
|
|||
|
Log.e(TAG, "unable to write the sync settings to the stash file", ioe);
|
|||
|
}
|
|||
|
} else {
|
|||
|
File stashFile = getStashFile(userId);
|
|||
|
if (stashFile.exists()) {
|
|||
|
stashFile.delete();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Restore SyncSettings for all existing accounts from a stashed backup-set
|
|||
|
*/
|
|||
|
private void accountAddedInternal(int userId) {
|
|||
|
String jsonString;
|
|||
|
|
|||
|
try (FileInputStream fIn = new FileInputStream(getStashFile(userId))) {
|
|||
|
DataInputStream in = new DataInputStream(fIn);
|
|||
|
jsonString = in.readUTF();
|
|||
|
} catch (FileNotFoundException fnfe) {
|
|||
|
// This is expected to happen when there is no accounts info stashed
|
|||
|
if (DEBUG) Log.d(TAG, "unable to find the stash file", fnfe);
|
|||
|
return;
|
|||
|
} catch (IOException ioe) {
|
|||
|
if (DEBUG) Log.d(TAG, "could not read sync settings from stash file", ioe);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
JSONArray unaddedAccountsJSONArray = new JSONArray(jsonString);
|
|||
|
restoreFromJsonArray(unaddedAccountsJSONArray, userId);
|
|||
|
} catch (JSONException jse) {
|
|||
|
// Malformed jsonString
|
|||
|
Log.e(TAG, "there was an error with the stashed sync settings", jse);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Restore SyncSettings for all existing accounts from a stashed backup-set
|
|||
|
*/
|
|||
|
public static void accountAdded(Context context, int userId) {
|
|||
|
AccountSyncSettingsBackupHelper helper = new AccountSyncSettingsBackupHelper(context,
|
|||
|
userId);
|
|||
|
helper.accountAddedInternal(userId);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Helper method - fetch accounts and return them as a HashSet.
|
|||
|
*
|
|||
|
* @return Accounts in a HashSet.
|
|||
|
*/
|
|||
|
private Set<Account> getAccounts(int userId) {
|
|||
|
Account[] accounts = mAccountManager.getAccountsAsUser(userId);
|
|||
|
Set<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.
|
|||
|
* This function will only be called during Setup Wizard, where we are guaranteed that there
|
|||
|
* are no active syncs.
|
|||
|
* There are 2 pieces of data to restore -
|
|||
|
* isSyncable (corresponds to {@link ContentResolver#getIsSyncable(Account, String)}
|
|||
|
* syncEnabled (corresponds to {@link ContentResolver#getSyncAutomatically(Account, String)}
|
|||
|
* <strong>The restore favours adapters that were enabled on the old device, and doesn't care
|
|||
|
* about adapters that were disabled.</strong>
|
|||
|
*
|
|||
|
* syncEnabled=true in restore data.
|
|||
|
* syncEnabled will be true on this device. isSyncable will be left as the default in order to
|
|||
|
* give the enabled adapter the chance to run an initialization sync.
|
|||
|
*
|
|||
|
* syncEnabled=false in restore data.
|
|||
|
* syncEnabled will be false on this device. isSyncable will be set to 2, unless it was 0 on the
|
|||
|
* old device in which case it will be set to 0 on this device. This is because isSyncable=0 is
|
|||
|
* a rare state and was probably set to 0 for good reason (historically isSyncable is a way by
|
|||
|
* which adapters control their own sync state independently of sync settings which is
|
|||
|
* toggleable by the user).
|
|||
|
* isSyncable=2 is a new isSyncable state we introduced specifically to allow adapters that are
|
|||
|
* disabled after a restore to run initialization logic when the adapter is later enabled.
|
|||
|
* See com.android.server.content.SyncStorageEngine#setSyncAutomatically
|
|||
|
*
|
|||
|
* The end result is that an adapter that the user had on will be turned on and get an
|
|||
|
* initialization sync, while an adapter that the user had off will be off until the user
|
|||
|
* enables it on this device at which point it will get an initialization sync.
|
|||
|
*/
|
|||
|
private void restoreExistingAccountSyncSettingsFromJSON(JSONObject accountJSON, int userId)
|
|||
|
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 wasSyncEnabled = authority.getBoolean(KEY_AUTHORITY_SYNC_ENABLED);
|
|||
|
int wasSyncable = authority.getInt(KEY_AUTHORITY_SYNC_STATE);
|
|||
|
|
|||
|
ContentResolver.setSyncAutomaticallyAsUser(
|
|||
|
account, authorityName, wasSyncEnabled, userId);
|
|||
|
|
|||
|
if (!wasSyncEnabled) {
|
|||
|
ContentResolver.setIsSyncableAsUser(
|
|||
|
account,
|
|||
|
authorityName,
|
|||
|
wasSyncable == 0 ?
|
|||
|
0 /* not syncable */ : 2 /* syncable but needs initialization */,
|
|||
|
userId);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
@Override
|
|||
|
public void writeNewStateDescription(ParcelFileDescriptor newState) {
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
private static File getStashFile(int userId) {
|
|||
|
File baseDir = userId == UserHandle.USER_SYSTEM ? Environment.getDataDirectory()
|
|||
|
: Environment.getDataSystemCeDirectory(userId);
|
|||
|
return new File(baseDir, STASH_FILE);
|
|||
|
}
|
|||
|
}
|