// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StreamUtil;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.firstrun.FirstRunSignInProcessor;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.preferences.privacy.PrivacyPreferencesManager;
import org.chromium.components.signin.ChromeSigninController;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Backup agent for Chrome, filters the restored backup to remove preferences that should not have
* been restored. Note: Nothing in this class can depend on the ChromeApplication instance having
* been created. During restore Android creates a special instance of the Chrome application with
* its own Android defined application class, which is not derived from ChromeApplication.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ChromeBackupAgent extends BackupAgent {
private static final String TAG = "ChromeBackupAgent";
// Lists of preferences that should be restored unchanged.
private static final String[] RESTORED_ANDROID_PREFS = {
FirstRunStatus.FIRST_RUN_FLOW_COMPLETE,
FirstRunStatus.LIGHTWEIGHT_FIRST_RUN_FLOW_COMPLETE,
FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_SETUP,
PrivacyPreferencesManager.PREF_METRICS_REPORTING,
};
// Sync preferences, all in C++ syncer::prefs namespace.
//
// TODO(aberent): These should ideally use the constants that are used to access the preferences
// elsewhere, but those are currently only exist in C++, so doing so would require some
// reorganization.
private static final String[][] RESTORED_CHROME_PREFS = {
// kSyncFirstSetupComplete
{"sync", "has_setup_completed"},
// kSyncKeepEverythingSynced
{"sync", "keep_everything_synced"},
// kSyncAutofillProfile
{"sync", "autofill_profile"},
// kSyncAutofillWallet
{"sync", "autofill_wallet"},
// kSyncAutofillWalletMetadata
{"sync", "autofill_wallet_metadata"},
// kSyncAutofill
{"sync", "autofill"},
// kSyncBookmarks
{"sync", "bookmarks"},
// kSyncDeviceInfo
{"sync", "device_info"},
// kSyncFaviconImages
{"sync", "favicon_images"},
// kSyncFaviconTracking
{"sync", "favicon_tracking"},
// kSyncHistoryDeleteDirectives
{"sync", "history_delete_directives"},
// kSyncPasswords
{"sync", "passwords"},
// kSyncPreferences
{"sync", "preferences"},
// kSyncPriorityPreferences
{"sync", "priority_preferences"},
// kSyncSessions
{"sync", "sessions"},
// kSyncSupervisedUserSettings
{"sync", "managed_user_settings"},
// kSyncSupervisedUserSharedSettings
{"sync", "managed_user_shared_settings"},
// kSyncSupervisedUserWhitelists
{"sync", "managed_user_whitelists"},
// kSyncTabs
{"sync", "tabs"},
// kSyncTypedUrls
{"sync", "typed_urls"},
// kSyncSuppressStart
{"sync", "suppress_start"},
};
private static final String[] DEFAULT_JSON_PREFS_FILE = {
// chrome::kInitialProfile
"Default",
// chrome::kPreferencesFilename
"Preferences",
};
private static boolean sAllowChromeApplication = false;
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// No implementation needed for Android 6.0 Auto Backup. Used only on older versions of
// Android Backup
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
throws IOException {
// No implementation needed for Android 6.0 Auto Backup. Used only on older versions of
// Android Backup
}
// May be overriden by downstream products that access account information in a different way.
protected Account[] getAccounts() {
Log.d(TAG, "Getting accounts from AccountManager");
AccountManager manager = (AccountManager) getSystemService(ACCOUNT_SERVICE);
return manager.getAccounts();
}
private boolean accountExistsOnDevice(String userName) {
// This cannot use AccountManagerHelper, since that depends on ChromeApplication.
for (Account account : getAccounts()) {
if (account.name.equals(userName)) return true;
}
return false;
}
@Override
public void onRestoreFinished() {
if (getApplicationContext() instanceof ChromeApplication && !sAllowChromeApplication) {
// This should never happen in real use, but will happen during testing if Chrome is
// already running (even in background, started to provide a service, for example).
Log.w(TAG, "Running with wrong type of Application class");
return;
}
// This is running without a ChromeApplication instance, so this has to be done here.
ContextUtils.initApplicationContext(getApplicationContext());
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
// Save the user name for later restoration.
String userName = sharedPrefs.getString(ChromeSigninController.SIGNED_IN_ACCOUNT_KEY, null);
Log.d(TAG, "Previous signed in user name = " + userName);
File prefsFile = this.getDir(ChromeBrowserInitializer.PRIVATE_DATA_DIRECTORY_SUFFIX,
Context.MODE_PRIVATE);
for (String name : DEFAULT_JSON_PREFS_FILE) {
prefsFile = new File(prefsFile, name);
}
// If the user hasn't signed in, or can't sign in, then don't restore anything.
if (userName == null || !accountExistsOnDevice(userName)) {
clearAllPrefs(sharedPrefs, prefsFile);
Log.d(TAG, "onRestoreFinished complete, nothing restored");
return;
}
// Check that the file has been restored.
if (!filterChromePrefs(prefsFile)) {
// The preferences are corrupt, for safety delete all of them
clearAllPrefs(sharedPrefs, prefsFile);
Log.d(TAG, "onRestoreFinished failed");
return;
}
restoreAndroidPrefs(sharedPrefs, userName);
Log.d(TAG, "onRestoreFinished complete");
}
@SuppressLint("CommitPrefEdits")
private void clearAllPrefs(SharedPreferences sharedPrefs, File prefsFile) {
deleteFileIfPossible(prefsFile);
// Android restore closes down the process immediately, so we want to make sure that the
// prefs changes are committed to disk before exiting.
sharedPrefs.edit().clear().commit();
}
@SuppressLint("CommitPrefEdits")
private void restoreAndroidPrefs(SharedPreferences sharedPrefs, String userName) {
Set<String> prefNames = sharedPrefs.getAll().keySet();
SharedPreferences.Editor editor = sharedPrefs.edit();
// Throw away prefs we don't want to restore.
Set<String> restoredPrefs = new HashSet<>(Arrays.asList(RESTORED_ANDROID_PREFS));
for (String pref : prefNames) {
if (!restoredPrefs.contains(pref)) editor.remove(pref);
}
// Because FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_COMPLETE is not restored Chrome
// will sign in the user on first run to the account in FIRST_RUN_FLOW_SIGNIN_ACCOUNT_NAME
// if any. If the rest of FRE has been completed this will happen silently.
editor.putString(FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_ACCOUNT_NAME, userName);
// Android restore closes down the process immediately, so we want to make sure that the
// prefs changes are committed to disk before exiting.
editor.commit();
}
private boolean filterChromePrefs(File prefsFile) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = openInputStream(prefsFile);
int fileLength = (int) getFileLength(prefsFile);
byte[] buffer = new byte[fileLength];
if (inputStream.read(buffer) != fileLength) return false;
JSONObject jsonInput = new JSONObject(new String(buffer, "UTF-8"));
JSONObject jsonOutput = new JSONObject();
for (String[] pref : RESTORED_CHROME_PREFS) {
Object prefValue = readChromePref(jsonInput, pref);
if (prefValue != null) writeChromePref(jsonOutput, pref, prefValue);
}
byte[] outputBytes = jsonOutput.toString().getBytes("UTF-8");
outputStream = openOutputStream(prefsFile);
outputStream.write(outputBytes);
return true;
} catch (IOException | JSONException e) {
Log.d(TAG, "Filtering preferences failed with %s", e.getMessage());
return false;
} finally {
StreamUtil.closeQuietly(inputStream);
StreamUtil.closeQuietly(outputStream);
}
}
@VisibleForTesting
protected long getFileLength(File prefsFile) {
return prefsFile.length();
}
@VisibleForTesting
protected InputStream openInputStream(File prefsFile) throws FileNotFoundException {
return new FileInputStream(prefsFile);
}
@VisibleForTesting
protected OutputStream openOutputStream(File prefsFile) throws FileNotFoundException {
return new FileOutputStream(prefsFile);
}
private Object readChromePref(JSONObject json, String pref[]) {
JSONObject finalParent = json;
for (int i = 0; i < pref.length - 1; i++) {
finalParent = finalParent.optJSONObject(pref[i]);
if (finalParent == null) return null;
}
return finalParent.opt(pref[pref.length - 1]);
}
private void writeChromePref(JSONObject json, String[] prefPath, Object value)
throws JSONException {
JSONObject finalParent = json;
for (int i = 0; i < prefPath.length - 1; i++) {
JSONObject prevParent = finalParent;
finalParent = prevParent.optJSONObject(prefPath[i]);
if (finalParent == null) {
finalParent = new JSONObject();
prevParent.put(prefPath[i], finalParent);
}
}
finalParent.put(prefPath[prefPath.length - 1], value);
}
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
private void deleteFileIfPossible(File file) {
// Ignore result. There is nothing else we can do if the delete fails.
file.delete();
}
@VisibleForTesting
static void allowChromeApplicationForTesting() {
sAllowChromeApplication = true;
}
}