blob: bc9dd5e66d7b0fc79e3b55cc79ba2ed059de02db [file] [log] [blame]
// 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.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import android.support.annotation.IntDef;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.firstrun.FirstRunGlueImpl;
import org.chromium.chrome.browser.firstrun.FirstRunSignInProcessor;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.init.AsyncInitTaskRunner;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.preferences.privacy.PrivacyPreferencesManager;
import org.chromium.components.signin.AccountManagerHelper;
import org.chromium.components.signin.ChromeSigninController;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Backup agent for Chrome, using Android key/value backup.
*/
public class ChromeBackupAgent extends BackupAgent {
private static final String ANDROID_DEFAULT_PREFIX = "AndroidDefault.";
private static final String NATIVE_PREF_PREFIX = "native.";
private static final String TAG = "ChromeBackupAgent";
@VisibleForTesting
static final String HISTOGRAM_ANDROID_RESTORE_RESULT = "Android.RestoreResult";
// Restore status is used to pass the result of any restore to Chrome's first run, so that
// it can be recorded as a histogram.
@Retention(RetentionPolicy.SOURCE)
@IntDef({NO_RESTORE, RESTORE_COMPLETED, RESTORE_AFTER_FIRST_RUN, BROWSER_STARTUP_FAILED,
NOT_SIGNED_IN, RESTORE_STATUS_RECORDED})
public @interface RestoreStatus {}
// Values must match those in histogram.xml AndroidRestoreResult.
static final int NO_RESTORE = 0;
static final int RESTORE_COMPLETED = 1;
static final int RESTORE_AFTER_FIRST_RUN = 2;
static final int BROWSER_STARTUP_FAILED = 3;
static final int NOT_SIGNED_IN = 4;
static final int RESTORE_HISTOGRAM_BOUNDARY = 5;
// Set RESTORE_STATUS_RECORDED when the histogram has been recorded; so that it is only recorded
// once.
public static final int RESTORE_STATUS_RECORDED = 5;
private static final String RESTORE_STATUS = "android_restore_status";
// List of preferences that should be restored unchanged.
static final String[] BACKUP_ANDROID_BOOL_PREFS = {
DataReductionProxySettings.DATA_REDUCTION_ENABLED_PREF,
FirstRunGlueImpl.CACHED_TOS_ACCEPTED_PREF, FirstRunStatus.FIRST_RUN_FLOW_COMPLETE,
FirstRunStatus.LIGHTWEIGHT_FIRST_RUN_FLOW_COMPLETE,
FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_SETUP,
PrivacyPreferencesManager.PREF_METRICS_REPORTING,
};
// Timeout for running the background tasks, needs to be quite long since they may be doing
// network access, but must be less than the 1 minute restore timeout to be useful.
private static final long BACKGROUND_TASK_TIMEOUT_SECS = 20;
/**
* Class to save and restore the backup state, used to decide if backups are needed. Since the
* backup data is small, and stored as private data by the backup service, this can simply store
* and compare a copy of the data.
*/
@SuppressFBWarnings(value = {"HE_EQUALS_USE_HASHCODE"},
justification = "Only local use, hashcode never used")
private static final class BackupState {
private ArrayList<String> mNames;
private ArrayList<byte[]> mValues;
@SuppressFBWarnings(value = {"OS_OPEN_STREAM"}, justification = "Closed by backup system")
@SuppressWarnings("unchecked")
public BackupState(ParcelFileDescriptor parceledState) throws IOException {
if (parceledState == null) return;
try {
FileInputStream instream = new FileInputStream(parceledState.getFileDescriptor());
ObjectInputStream in = new ObjectInputStream(instream);
mNames = (ArrayList<String>) in.readObject();
mValues = (ArrayList<byte[]>) in.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public BackupState(ArrayList<String> names, ArrayList<byte[]> values) {
mNames = names;
mValues = values;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof BackupState)) return false;
BackupState otherBackupState = (BackupState) other;
return mNames.equals(otherBackupState.mNames)
&& Arrays.deepEquals(mValues.toArray(), otherBackupState.mValues.toArray());
}
@SuppressFBWarnings(value = {"OS_OPEN_STREAM"}, justification = "Closed by backup system")
public void save(ParcelFileDescriptor parceledState) throws IOException {
FileOutputStream outstream = new FileOutputStream(parceledState.getFileDescriptor());
ObjectOutputStream out = new ObjectOutputStream(outstream);
out.writeObject(mNames);
out.writeObject(mValues);
}
}
@VisibleForTesting
protected boolean accountExistsOnDevice(String userName) {
return AccountManagerHelper.get().getAccountFromName(userName) != null;
}
@VisibleForTesting
protected boolean initializeBrowser(Context context) {
try {
ChromeBrowserInitializer.getInstance(context).handleSynchronousStartup();
} catch (ProcessInitException e) {
Log.w(TAG, "Browser launch failed on backup or restore: " + e);
return false;
}
return true;
}
private static byte[] booleanToBytes(boolean value) {
return value ? new byte[] {1} : new byte[] {0};
}
private static boolean bytesToBoolean(byte[] bytes) {
return bytes[0] != 0;
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
final ChromeBackupAgent backupAgent = this;
final ArrayList<String> backupNames = new ArrayList<>();
final ArrayList<byte[]> backupValues = new ArrayList<>();
// The native preferences can only be read on the UI thread.
Boolean nativePrefsRead =
ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
@Override
public Boolean call() {
// Start the browser if necessary, so that Chrome can access the native
// preferences. Although Chrome requests the backup, it doesn't happen
// immediately, so by the time it does Chrome may not be running.
if (!initializeBrowser(backupAgent)) return false;
String[] nativeBackupNames = nativeGetBoolBackupNames();
boolean[] nativeBackupValues = nativeGetBoolBackupValues();
assert nativeBackupNames.length == nativeBackupValues.length;
for (String name : nativeBackupNames) {
backupNames.add(NATIVE_PREF_PREFIX + name);
}
for (boolean val : nativeBackupValues) {
backupValues.add(booleanToBytes(val));
}
return true;
}
});
if (!nativePrefsRead) {
// Something went wrong reading the native preferences, skip the backup.
return;
}
// Add the Android boolean prefs.
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
for (String prefName : BACKUP_ANDROID_BOOL_PREFS) {
if (sharedPrefs.contains(prefName)) {
backupNames.add(ANDROID_DEFAULT_PREFIX + prefName);
backupValues.add(booleanToBytes(sharedPrefs.getBoolean(prefName, false)));
}
}
// Finally add the user id.
backupNames.add(ANDROID_DEFAULT_PREFIX + ChromeSigninController.SIGNED_IN_ACCOUNT_KEY);
backupValues.add(
sharedPrefs.getString(ChromeSigninController.SIGNED_IN_ACCOUNT_KEY, "").getBytes());
BackupState newBackupState = new BackupState(backupNames, backupValues);
// Check if a backup is actually needed.
try {
BackupState oldBackupState = new BackupState(oldState);
if (newBackupState.equals(oldBackupState)) {
Log.i(TAG, "Nothing has changed since the last backup. Backup skipped.");
newBackupState.save(newState);
return;
}
} catch (IOException e) {
// This will happen if Chrome has never written backup data, or if the backup status is
// corrupt. Create a new backup in either case.
Log.i(TAG, "Can't read backup status file");
}
// Write the backup data
for (int i = 0; i < backupNames.size(); i++) {
data.writeEntityHeader(backupNames.get(i), backupValues.get(i).length);
data.writeEntityData(backupValues.get(i), backupValues.get(i).length);
}
// Remember the backup state.
newBackupState.save(newState);
Log.i(TAG, "Backup complete");
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
throws IOException {
// TODO(aberent) Check that this is not running on the UI thread. Doing so, however, makes
// testing difficult since the test code runs on the UI thread.
// Check that the user hasn't already seen FRE (not sure if this can ever happen, but if it
// does then restoring the backup will overwrite the user's choices).
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
if (FirstRunStatus.getFirstRunFlowComplete()
|| FirstRunStatus.getLightweightFirstRunFlowComplete()) {
setRestoreStatus(RESTORE_AFTER_FIRST_RUN);
Log.w(TAG, "Restore attempted after first run");
return;
}
final ArrayList<String> backupNames = new ArrayList<>();
final ArrayList<byte[]> backupValues = new ArrayList<>();
String restoredUserName = null;
while (data.readNextHeader()) {
String key = data.getKey();
int dataSize = data.getDataSize();
byte[] buffer = new byte[dataSize];
data.readEntityData(buffer, 0, dataSize);
if (key.equals(ANDROID_DEFAULT_PREFIX + ChromeSigninController.SIGNED_IN_ACCOUNT_KEY)) {
restoredUserName = new String(buffer);
} else {
backupNames.add(key);
backupValues.add(buffer);
}
}
// Start and wait for the Async init tasks. This loads the library, and attempts to load the
// first run variations seed. Since these are both slow it makes sense to run them in
// parallel as Android AsyncTasks, reusing some of Chrome's async startup logic.
//
// Note that this depends on onRestore being run from a background thread, since
// if it were called from the UI thread the broadcast would not be received until after it
// exited.
final CountDownLatch latch = new CountDownLatch(1);
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
// Chrome library loading depends on PathUtils.
PathUtils.setPrivateDataDirectorySuffix(
ChromeBrowserInitializer.PRIVATE_DATA_DIRECTORY_SUFFIX);
createAsyncInitTaskRunner(latch).startBackgroundTasks(
false /* allocateChildConnection */, true /* initVariationSeed */);
}
});
try {
// Ignore result. It will only be false if it times out. Problems with fetching the
// variation seed can be ignored, and other problems will either recover or be repeated
// when Chrome is started synchronously.
latch.await(BACKGROUND_TASK_TIMEOUT_SECS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// Should never happen, but can be ignored (as explained above) anyway.
}
// Chrome has to be running before it can check if the account exists. Because the native
// library is already loaded Chrome startup should be fast.
final ChromeBackupAgent backupAgent = this;
boolean browserStarted =
ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
@Override
public Boolean call() {
// Start the browser if necessary.
return initializeBrowser(backupAgent);
}
});
if (!browserStarted) {
// Something went wrong starting Chrome, skip the restore.
setRestoreStatus(BROWSER_STARTUP_FAILED);
return;
}
// If the user hasn't signed in, or can't sign in, then don't restore anything.
if (restoredUserName == null || !accountExistsOnDevice(restoredUserName)) {
setRestoreStatus(NOT_SIGNED_IN);
Log.i(TAG, "Chrome was not signed in with a known account name, not restoring");
return;
}
// Restore the native preferences on the UI thread
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
ArrayList<String> nativeBackupNames = new ArrayList<>();
boolean[] nativeBackupValues = new boolean[backupNames.size()];
int count = 0;
int prefixLength = NATIVE_PREF_PREFIX.length();
for (int i = 0; i < backupNames.size(); i++) {
String name = backupNames.get(i);
if (name.startsWith(NATIVE_PREF_PREFIX)) {
nativeBackupNames.add(name.substring(prefixLength));
nativeBackupValues[count] = bytesToBoolean(backupValues.get(i));
count++;
}
}
nativeSetBoolBackupPrefs(nativeBackupNames.toArray(new String[count]),
Arrays.copyOf(nativeBackupValues, count));
}
});
// Now that everything looks good so restore the Android preferences.
SharedPreferences.Editor editor = sharedPrefs.edit();
// Only restore preferences that we know about.
int prefixLength = ANDROID_DEFAULT_PREFIX.length();
for (int i = 0; i < backupNames.size(); i++) {
String name = backupNames.get(i);
if (name.startsWith(ANDROID_DEFAULT_PREFIX)
&& Arrays.asList(BACKUP_ANDROID_BOOL_PREFS)
.contains(name.substring(prefixLength))) {
editor.putBoolean(
name.substring(prefixLength), bytesToBoolean(backupValues.get(i)));
}
}
// 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, restoredUserName);
editor.apply();
// The silent first run will change things, so there is no point in trying to prevent
// additional backups at this stage. Don't write anything to |newState|.
setRestoreStatus(RESTORE_COMPLETED);
Log.i(TAG, "Restore complete");
}
@VisibleForTesting
AsyncInitTaskRunner createAsyncInitTaskRunner(final CountDownLatch latch) {
return new AsyncInitTaskRunner() {
@Override
protected void onSuccess() {
latch.countDown();
}
@Override
protected void onFailure() {
// Ignore failure. Problems with the variation seed can be ignored, and other
// problems will either recover or be repeated when Chrome is started synchronously.
latch.countDown();
}
};
}
/**
* Get the saved result of any restore that may have happened.
*
* @return the restore status, a RestoreStatus value.
*/
@VisibleForTesting
@RestoreStatus
static int getRestoreStatus() {
return ContextUtils.getAppSharedPreferences().getInt(RESTORE_STATUS, NO_RESTORE);
}
/**
* Save the restore status for later transfer to a histogram.
*
* @param status the status.
*/
@VisibleForTesting
static void setRestoreStatus(@RestoreStatus int status) {
ContextUtils.getAppSharedPreferences().edit().putInt(RESTORE_STATUS, status).apply();
}
/**
* Record the restore histogram. To be called from Chrome itself once it is running.
*/
static void recordRestoreHistogram() {
int restoreStatus = getRestoreStatus();
// Ensure restore status is only recorded once
if (restoreStatus != RESTORE_STATUS_RECORDED) {
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_ANDROID_RESTORE_RESULT, restoreStatus, RESTORE_HISTOGRAM_BOUNDARY);
setRestoreStatus(RESTORE_STATUS_RECORDED);
}
}
@VisibleForTesting
protected native String[] nativeGetBoolBackupNames();
@VisibleForTesting
protected native boolean[] nativeGetBoolBackupValues();
@VisibleForTesting
protected native void nativeSetBoolBackupPrefs(String[] name, boolean[] value);
}