script-astra/Android/Sdk/sources/android-35/com/android/server/backup/AccountSyncSettingsBackupHelper.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

443 lines
18 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 arent 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);
}
}