// Copyright 2016 The Chromium Authors
// 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.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupManager;
import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.base.SplitCompatApplication;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.init.AsyncInitTaskRunner;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.AccountUtils;
import org.chromium.components.signin.SigninFeatureMap;
import org.chromium.components.signin.SigninFeatures;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.sync.internal.SyncPrefNames;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.common.ContentProcessInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
/** Backup agent for Chrome, using Android key/value backup. */
@SuppressWarnings("UseSharedPreferencesManagerFromChromeCheck")
public class ChromeBackupAgentImpl extends ChromeBackupAgent.Impl {
private static final String ANDROID_DEFAULT_PREFIX = "AndroidDefault.";
private static final String NATIVE_BOOL_PREF_PREFIX = "native.";
private static final String NATIVE_DICT_PREF_PREFIX = "NativeJsonDict.";
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.
@IntDef({
RestoreStatus.NO_RESTORE,
RestoreStatus.RESTORE_COMPLETED,
RestoreStatus.RESTORE_AFTER_FIRST_RUN,
RestoreStatus.BROWSER_STARTUP_FAILED,
RestoreStatus.NOT_SIGNED_IN,
RestoreStatus.DEPRECATED_SIGNIN_TIMED_OUT,
RestoreStatus.DEPRECATED_RESTORE_STATUS_RECORDED,
RestoreStatus.SIGNIN_TIMED_OUT,
RestoreStatus.RESTORE_STARTED_NOT_FINISHED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface RestoreStatus {
// Values must match those in histogram.xml AndroidRestoreResult.
int NO_RESTORE = 0;
int RESTORE_COMPLETED = 1;
int RESTORE_AFTER_FIRST_RUN = 2;
int BROWSER_STARTUP_FAILED = 3;
int NOT_SIGNED_IN = 4;
// This enum value has taken the previous value indicating that the histogram has been
// recorded, when it was introduced. Deprecating since the metric is polluted consequently.
int DEPRECATED_SIGNIN_TIMED_OUT = 5;
// Previously, DEPRECATED_RESTORE_STATUS_RECORDED was set when the histogram has been
// recorded, to prevent additional histogram record. This magic value is being replaced by
// the boolean pref RESTORE_STATUS_RECORDED.
// This value is kept for legacy pref support.
int DEPRECATED_RESTORE_STATUS_RECORDED = 6;
int SIGNIN_TIMED_OUT = 7;
// Recorded if `onRestore` was called but the restore flow died or timed out before it could
// record a more specific result.
int RESTORE_STARTED_NOT_FINISHED = 8;
int NUM_ENTRIES = RESTORE_STARTED_NOT_FINISHED;
}
@VisibleForTesting static final String RESTORE_STATUS = "android_restore_status";
private static final String RESTORE_STATUS_RECORDED = "android_restore_status_recorded";
// Keep track of backup failures, so that we give up in the end on persistent problems.
@VisibleForTesting static final String BACKUP_FAILURE_COUNT = "android_backup_failure_count";
@VisibleForTesting static final int MAX_BACKUP_FAILURES = 5;
// Bool entries from SharedPreferences that should be backed up / restored.
static final String[] BACKUP_ANDROID_BOOL_PREFS = {
ChromePreferenceKeys.FIRST_RUN_CACHED_TOS_ACCEPTED,
ChromePreferenceKeys.FIRST_RUN_FLOW_COMPLETE,
ChromePreferenceKeys.FIRST_RUN_LIGHTWEIGHT_FLOW_COMPLETE,
ChromePreferenceKeys.PRIVACY_METRICS_REPORTING_PERMITTED_BY_POLICY,
ChromePreferenceKeys.PRIVACY_METRICS_REPORTING_PERMITTED_BY_USER,
};
// Bool entries from PrefService that should be backed up / restored.
static final String[] BACKUP_NATIVE_SYNC_TYPE_BOOL_PREFS = {
SyncPrefNames.SYNC_KEEP_EVERYTHING_SYNCED,
SyncPrefNames.SYNC_APPS,
SyncPrefNames.SYNC_AUTOFILL,
SyncPrefNames.SYNC_BOOKMARKS,
SyncPrefNames.SYNC_HISTORY,
SyncPrefNames.SYNC_PASSWORDS,
SyncPrefNames.SYNC_PAYMENTS,
SyncPrefNames.SYNC_PREFERENCES,
SyncPrefNames.SYNC_PRODUCT_COMPARISON,
SyncPrefNames.SYNC_READING_LIST,
SyncPrefNames.SYNC_SAVED_TAB_GROUPS,
SyncPrefNames.SYNC_SHARED_TAB_GROUP_DATA,
SyncPrefNames.SYNC_TABS,
};
// Key used to store the email of the syncing account. This email is obtained from
// IdentityManager during the backup.
static final String SYNCING_ACCOUNT_KEY = "google.services.username";
// Key used to store the email of the signed-in account. This email is obtained from
// IdentityManager during the backup.
static final String SIGNED_IN_ACCOUNT_ID_KEY = "Chrome.SignIn.SignedInAccountGaiaIdBackup";
// 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;
// Timeout for the sign-in flow and related preferences commit.
private static final long SIGNIN_TIMEOUT_SECS = 10;
/**
* 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.
*/
private static final class BackupState {
private ArrayList<String> mNames;
private ArrayList<byte[]> mValues;
@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());
}
public void save(ParcelFileDescriptor parceledState) throws IOException {
FileOutputStream outstream = new FileOutputStream(parceledState.getFileDescriptor());
ObjectOutputStream out = new ObjectOutputStream(outstream);
out.writeObject(mNames);
out.writeObject(mValues);
}
}
// TODO (aberent) Refactor the tests to use a mocked ChromeBrowserInitializer, and make this
// private again.
@VisibleForTesting
boolean initializeBrowser() {
// Workaround for https://crbug.com/718166. The backup agent is sometimes being started in a
// child process, before the child process loads its native library. If backup then loads
// the native library the child process is left in a very confused state and crashes.
if (ContentProcessInfo.inChildProcess()) {
Log.e(TAG, "Backup agent started from child process");
return false;
}
ChromeBrowserInitializer.getInstance().handleSynchronousStartup();
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 ArrayList<String> backupNames = new ArrayList<>();
final ArrayList<byte[]> backupValues = new ArrayList<>();
// TODO(crbug.com/40066949): Remove syncAccount once UNO is launched, given the sync feature
// and consent will disappear.
final AtomicReference<CoreAccountInfo> syncAccount = new AtomicReference<>();
final AtomicReference<CoreAccountInfo> signedInAccount = new AtomicReference<>();
// The native preferences can only be read on the UI thread.
Boolean nativePrefsRead =
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
// 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()) return false;
Profile profile = ProfileManager.getLastUsedRegularProfile();
IdentityManager identityManager =
IdentityServicesProvider.get().getIdentityManager(profile);
syncAccount.set(
identityManager.getPrimaryAccountInfo(ConsentLevel.SYNC));
signedInAccount.set(
identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN));
if (syncAccount.get() != null
&& !syncAccount.get().equals(signedInAccount.get())) {
throw new IllegalStateException(
"Recorded signed in account differs from syncing account");
}
// When new data type is added to the UserSelectableType enum, also add
// it to BACKUP_NATIVE_SYNC_TYPE_BOOL_PREFS (if the type is supported on
// Android).
assert UserSelectableType.LAST_TYPE == 14;
PrefService prefService = UserPrefs.get(profile);
for (String name : BACKUP_NATIVE_SYNC_TYPE_BOOL_PREFS) {
backupNames.add(NATIVE_BOOL_PREF_PREFIX + name);
backupValues.add(booleanToBytes(prefService.getBoolean(name)));
}
backupNames.add(
NATIVE_DICT_PREF_PREFIX
+ SyncPrefNames.SELECTED_TYPES_PER_ACCOUNT);
backupValues.add(
ChromeBackupAgentImplJni.get()
.getSerializedDict(
prefService,
SyncPrefNames.SELECTED_TYPES_PER_ACCOUNT)
.getBytes());
return true;
});
SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
if (!nativePrefsRead) {
// Something went wrong reading the native preferences, skip the backup, but try again
// later.
int backupFailureCount = sharedPrefs.getInt(BACKUP_FAILURE_COUNT, 0) + 1;
if (backupFailureCount >= MAX_BACKUP_FAILURES) {
// Too many re-tries, give up and force an unconditional backup next time one is
// requested.
return;
}
sharedPrefs.edit().putInt(BACKUP_FAILURE_COUNT, backupFailureCount).apply();
if (oldState != null) {
try {
// Copy the old state to the new state, so that next time Chrome only does a
// backup if necessary.
BackupState state = new BackupState(oldState);
state.save(newState);
} catch (Exception e) {
// There was no old state, or it was corrupt; leave the newState unwritten,
// hence forcing an unconditional backup on the next attempt.
}
}
// Ask Android to schedule a retry.
new BackupManager(getBackupAgent()).dataChanged();
return;
}
// The backup is going to work, clear the failure count.
sharedPrefs.edit().remove(BACKUP_FAILURE_COUNT).apply();
// Add the Android boolean prefs.
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 signed-in/syncing user ids.
backupNames.add(ANDROID_DEFAULT_PREFIX + SYNCING_ACCOUNT_KEY);
backupValues.add(
ApiCompatibilityUtils.getBytesUtf8(
syncAccount.get() == null ? "" : syncAccount.get().getEmail()));
backupNames.add(ANDROID_DEFAULT_PREFIX + SIGNED_IN_ACCOUNT_ID_KEY);
backupValues.add(
ApiCompatibilityUtils.getBytesUtf8(
signedInAccount.get() == null ? "" : signedInAccount.get().getGaiaId()));
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.
// TODO(https://crbug.com/353661640): Use return value to ensure `RestoreStatus` is provided
// by return statements.
//
// Non-timeout return statements in this method call `setRestoreStatus` before returning -
// this is a fallback value that will be used if the restore flow times out or crashes.
setRestoreStatus(RestoreStatus.RESTORE_STARTED_NOT_FINISHED);
// 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(RestoreStatus.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<>();
@Nullable String restoredSyncUserEmail = null;
@Nullable String restoredSignedInUserID = 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 + SYNCING_ACCOUNT_KEY)) {
restoredSyncUserEmail = new String(buffer);
} else if (key.equals(ANDROID_DEFAULT_PREFIX + SIGNED_IN_ACCOUNT_ID_KEY)) {
restoredSignedInUserID = new String(buffer);
} else {
backupNames.add(key);
backupValues.add(buffer);
}
}
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
// Chrome library loading and metrics-related code below depend on PathUtils.
PathUtils.setPrivateDataDirectorySuffix(
SplitCompatApplication.PRIVATE_DATA_DIRECTORY_SUFFIX);
});
if (isMetricsReportingEnabled(backupNames, backupValues)) {
try {
enableRestoreFlowMetrics();
} catch (IOException e) {
// Couldn't enable metrics - log the error and try to proceed with the restore flow.
Log.w(TAG, "Couldn't enable restore flow metrics", e);
}
}
// 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);
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
// TODO(crbug.com/40283943): Wait for AccountManagerFacade to load accounts.
createAsyncInitTaskRunner(latch)
.startBackgroundTasks(
/* allocateChildConnection= */ false,
/* fetchVariationSeed= */ true);
});
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.
boolean browserStarted =
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
// Start the browser if necessary.
return initializeBrowser();
});
if (!browserStarted) {
// Something went wrong starting Chrome, skip the restore.
setRestoreStatus(RestoreStatus.BROWSER_STARTUP_FAILED);
return;
}
if (SigninFeatureMap.isEnabled(
SigninFeatures.RESTORE_SIGNED_IN_ACCOUNT_AND_SETTINGS_FROM_BACKUP)
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
final CountDownLatch accountsLatch = new CountDownLatch(1);
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
AccountManagerFacadeProvider.getInstance()
.getCoreAccountInfos()
.then(
(ignored) -> {
accountsLatch.countDown();
});
});
try {
// Explicit timeout is not needed here. In the scenario where accounts are not
// available - the restore flow will be stopped several lines below. So, having an
// explicit timeout would still result in the state not getting restored. Thus, it
// is cleaner to just wait without an explicit timeout and rely on the BackupManager
// killing the process if accounts never become available.
accountsLatch.await();
} catch (InterruptedException e) {
// Normally, this shouldn't happen (Chrome process will just get killed). Use
// `RESTORE_STARTED_NOT_FINISHED` as fallback in the unlikely scenario it happens.
setRestoreStatus(RestoreStatus.RESTORE_STARTED_NOT_FINISHED);
return;
}
}
@Nullable
CoreAccountInfo signedInAccountInfo = getDeviceAccountWithGaiaId(restoredSignedInUserID);
@Nullable
CoreAccountInfo syncAccountInfo = getDeviceAccountWithEmail(restoredSyncUserEmail);
// If the user hasn't signed in, or can't sign in, then don't restore anything.
if (syncAccountInfo == null
&& (signedInAccountInfo == null
|| !SigninFeatureMap.isEnabled(
SigninFeatures
.RESTORE_SIGNED_IN_ACCOUNT_AND_SETTINGS_FROM_BACKUP))) {
setRestoreStatus(RestoreStatus.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
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
PrefService prefService =
UserPrefs.get(ProfileManager.getLastUsedRegularProfile());
boolean areAccountSettingsRestored = false;
for (int i = 0; i < backupNames.size(); i++) {
String name = backupNames.get(i);
if (name.startsWith(NATIVE_BOOL_PREF_PREFIX)) {
name = name.substring(NATIVE_BOOL_PREF_PREFIX.length());
if (!Arrays.asList(BACKUP_NATIVE_SYNC_TYPE_BOOL_PREFS).contains(name)) {
// Not among the known prefs, do not restore. In the worst case,
// this could attempt to write a pref which is no longer exists,
// causing a crash.
continue;
}
prefService.setBoolean(name, bytesToBoolean(backupValues.get(i)));
continue;
}
// Restore the account settings if possible.
// It should be done before the potential migration of global boolean
// preferences to account settings:
// - If the user was syncing, the global prefs are more up-to-date so the
// converted global prefs should take precedence;
// - If the user was signed-in only, the global preferences will not be
// migrated to account settings if the latter is restored, so no risk of
// override here.
if (name.startsWith(NATIVE_DICT_PREF_PREFIX)) {
name = name.substring(NATIVE_DICT_PREF_PREFIX.length());
if (!name.equals(SyncPrefNames.SELECTED_TYPES_PER_ACCOUNT)
|| !SigninFeatureMap.isEnabled(
SigninFeatures
.RESTORE_SIGNED_IN_ACCOUNT_AND_SETTINGS_FROM_BACKUP)) {
// Same as above, do not restore prefs if the name is unknown
// or if the restore flag is not enabled.
continue;
}
areAccountSettingsRestored = true;
ChromeBackupAgentImplJni.get()
.setDict(
prefService,
SyncPrefNames.SELECTED_TYPES_PER_ACCOUNT,
new String(backupValues.get(i)));
continue;
}
}
// Migrate global sync settings to account settings when necessary.
// It should be done after the restoration of the existing per-account settings
// from the backup to avoid override, as mentioned above.
final boolean shouldRestoreSelectedTypesAsAccountSettings =
(syncAccountInfo != null || !areAccountSettingsRestored)
&& SigninFeatureMap.isEnabled(
SigninFeatures
.RESTORE_SIGNED_IN_ACCOUNT_AND_SETTINGS_FROM_BACKUP)
&& ChromeFeatureList.isEnabled(
ChromeFeatureList
.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS);
if (shouldRestoreSelectedTypesAsAccountSettings) {
final String gaiaID =
syncAccountInfo != null
? syncAccountInfo.getGaiaId()
: signedInAccountInfo.getGaiaId();
ChromeBackupAgentImplJni.get()
.migrateGlobalDataTypePrefsToAccount(prefService, gaiaID);
}
// TODO(crbug.com/332710541): Another commit is done for signed-in users in
// SigninManager.SignInCallback.onPrefsCommitted(). Do a single one instead.
ChromeBackupAgentImplJni.get().commitPendingPrefWrites(prefService);
});
// 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)));
}
}
if (syncAccountInfo != null) {
// Both accounts are recorded at the same time. Since only one account is in signed-in
// state at a given time, they should be identical if both are valid.
if (signedInAccountInfo != null && !signedInAccountInfo.equals(syncAccountInfo)) {
throw new IllegalStateException(
"Recorded signed in account differs from syncing account");
}
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
editor.apply();
signInAndWaitForResult(syncAccountInfo);
} else {
// This will sign in the user on first run to the account in
// BACKUP_FLOW_SIGNIN_ACCOUNT_NAME if any.
editor.putString(
ChromePreferenceKeys.BACKUP_FLOW_SIGNIN_ACCOUNT_NAME,
restoredSyncUserEmail);
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(RestoreStatus.RESTORE_COMPLETED);
}
} else {
editor.apply();
// signedInAccountInfo and syncAccountInfo should not be null at the same at this point.
// If there's no valid syncing account and the signed-in account restore is disabled,
// the restore should already be stopped and the restore state set to `NOT_SIGNED_IN`.
if (signedInAccountInfo == null
|| !SigninFeatureMap.isEnabled(
SigninFeatures.RESTORE_SIGNED_IN_ACCOUNT_AND_SETTINGS_FROM_BACKUP)) {
throw new IllegalStateException("No valid account can be signed-in");
}
signInAndWaitForResult(signedInAccountInfo);
}
Log.i(TAG, "Restore complete");
}
private boolean isMetricsReportingEnabled(
ArrayList<String> backupNames, ArrayList<byte[]> backupValues) {
Predicate<String> prefGetter =
(String prefName) -> {
int index = backupNames.indexOf(ANDROID_DEFAULT_PREFIX + prefName);
return index != -1 && bytesToBoolean(backupValues.get(index));
};
return prefGetter.test(ChromePreferenceKeys.PRIVACY_METRICS_REPORTING_PERMITTED_BY_POLICY)
&& prefGetter.test(
ChromePreferenceKeys.PRIVACY_METRICS_REPORTING_PERMITTED_BY_USER);
}
// TODO(crbug.com/338972271): Find a less hacky fix for restore flow metrics.
private void enableRestoreFlowMetrics() throws IOException {
File dataDirectory = new File(PathUtils.getDataDirectory());
if (!dataDirectory.exists()) {
dataDirectory.mkdir();
}
// Native code checks the whether metrics are enabled early during start-up - before
// the restore flow can set the correct value in the Local State. To work around this,
// create an empty file - the existence of this file will be used as the default value
// for UMA reporting. It is safe to do here, because we know that metrics state will be
// restored to enabled (due to the check above).
final String consentFileName = "Consent To Send Stats";
File consentFile = new File(dataDirectory, consentFileName);
if (!consentFile.exists()) {
consentFile.createNewFile();
}
// Chrome's process will be terminated after the restore flow is finished. To ensure
// metrics from the restore process survive until the post-restore run and actually get
// uploaded - persistent histograms should be backed by a memory-mapped file. This
// memory-mapped file mechanic requires a spare file on Android (otherwise, histograms
// are stored in memory, without the backing file and will be lost after the restore
// process is finished). Normally, a spare file is created by the previous run of
// Chrome. However, since the restore flow is the very first run for this particular
// install - there won't be a spare file to be used, breaking persistent histograms and
// thus restore flow metrics. To work around this issue and still get metrics from the
// restore flow - create a spare file manually.
// LINT.IfChange
final String spareFileName = "BrowserMetrics-spare.pma";
final int spareFileSize = 4 * 1024 * 1024;
// LINT.ThenChange(/components/metrics/persistent_histograms.cc)
File spareFile = new File(dataDirectory, spareFileName);
try (OutputStream outputStream = new FileOutputStream(spareFile)) {
// Zero-initialize the whole file to make sure the space is actually allocated and it
// can be used for persisting histograms.
byte[] buffer = new byte[8192];
for (int writtenBytes = 0; writtenBytes < spareFileSize; ) {
int writeSize = Math.min(buffer.length, spareFileSize - writtenBytes);
outputStream.write(buffer, 0, writeSize);
writtenBytes += writeSize;
}
} catch (IOException e) {
// The writing failed in the middle - delete the file.
spareFile.delete();
throw e;
}
}
@VisibleForTesting
AsyncInitTaskRunner createAsyncInitTaskRunner(final CountDownLatch latch) {
return new AsyncInitTaskRunner() {
@Override
protected void onSuccess() {
latch.countDown();
}
@Override
protected void onFailure(Exception failureCause) {
// 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();
}
};
}
private @Nullable CoreAccountInfo getDeviceAccountWithEmail(@Nullable String accountEmail) {
if (accountEmail == null) {
return null;
}
return PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
return AccountUtils.findCoreAccountInfoByEmail(getAccountInfos(), accountEmail);
});
}
private @Nullable CoreAccountInfo getDeviceAccountWithGaiaId(@Nullable String accountGaiaId) {
if (accountGaiaId == null) {
return null;
}
return PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
return AccountUtils.findCoreAccountInfoByGaiaId(
getAccountInfos(), accountGaiaId);
});
}
private static List<CoreAccountInfo> getAccountInfos() {
return AccountManagerFacadeProvider.getInstance().getCoreAccountInfos().getResult();
}
private static void signInAndWaitForResult(CoreAccountInfo accountInfo) {
final CountDownLatch latch = new CountDownLatch(1);
SigninManager.SignInCallback signInCallback =
new SigninManager.SignInCallback() {
@Override
public void onSignInComplete() {
// Sign-in preferences need to be committed for the sign-in to be effective.
// Therefore the count down is done in `onPrefsCommitted` instead.
}
@Override
public void onPrefsCommitted() {
latch.countDown();
}
@Override
public void onSignInAborted() {
// Ignore failure as Chrome will simply remain signed-out otherwise, and the
// user is still able to sign-in manually after opening Chrome.
latch.countDown();
}
};
signIn(accountInfo, signInCallback);
try {
// Wait the sign-in to finish the restore. Otherwise, the account info request will be
// cancelled one the restore ends. Timeout can be ignored as Chrome will simply remain
// signed-out otherwise, and the user is still able to sign-in manually after opening
// Chrome.
boolean success = latch.await(SIGNIN_TIMEOUT_SECS, TimeUnit.SECONDS);
int status = success ? RestoreStatus.RESTORE_COMPLETED : RestoreStatus.SIGNIN_TIMED_OUT;
setRestoreStatus(status);
} catch (InterruptedException e) {
// Exception can be ignored as explained above.
setRestoreStatus(RestoreStatus.SIGNIN_TIMED_OUT);
}
}
private static void signIn(CoreAccountInfo accountInfo, SigninManager.SignInCallback callback) {
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
SigninManager signinManager =
IdentityServicesProvider.get()
.getSigninManager(ProfileManager.getLastUsedRegularProfile());
final AccountManagerFacade accountManagerFacade =
AccountManagerFacadeProvider.getInstance();
Callback<Boolean> accountManagedCallback =
(isManaged) -> {
// If restoring a managed account, the user most likely already
// accepted account management previously and we don't have the
// ability to re-show the confirmation dialog here anyways.
if (isManaged) signinManager.setUserAcceptedAccountManagement(true);
signinManager.runAfterOperationInProgress(
() -> {
signinManager.signin(
accountInfo,
SigninAccessPoint
.POST_DEVICE_RESTORE_BACKGROUND_SIGNIN,
callback);
});
};
AccountManagerFacade.ChildAccountStatusListener listener =
(isChild, unused) -> {
if (isChild) {
// TODO(crbug.com/40835324):
// Pre-AllowSyncOffForChildAccounts, the backup sign-in for
// child accounts would happen in SigninChecker anyways.
// Maybe it should be handled by this class once the
// feature launches.
callback.onSignInAborted();
return;
}
signinManager.isAccountManaged(accountInfo, accountManagedCallback);
};
AccountUtils.checkChildAccountStatus(
accountManagerFacade, getAccountInfos(), listener);
});
}
/**
* Get the saved result of any restore that may have happened.
*
* @return the restore status, a RestoreStatus value.
*/
@VisibleForTesting
static @RestoreStatus int getRestoreStatus() {
return ContextUtils.getAppSharedPreferences()
.getInt(RESTORE_STATUS, RestoreStatus.NO_RESTORE);
}
/**
* Save the restore status for later transfer to a histogram, and reset histogram recorded
* status if needed.
*
* @param status the status.
*/
@VisibleForTesting
static void setRestoreStatus(@RestoreStatus int status) {
assert status != RestoreStatus.DEPRECATED_RESTORE_STATUS_RECORDED
&& status != RestoreStatus.DEPRECATED_SIGNIN_TIMED_OUT;
ContextUtils.getAppSharedPreferences().edit().putInt(RESTORE_STATUS, status).apply();
if (isRestoreStatusRecorded()) {
setRestoreStatusRecorded(false);
}
}
/**
* Get from the saved values whether the restore status histogram has been recorded.
*
* @return Whether the restore status has been recorded.
*/
@VisibleForTesting
static boolean isRestoreStatusRecorded() {
return ContextUtils.getAppSharedPreferences().getBoolean(RESTORE_STATUS_RECORDED, false);
}
/**
* Save the value indicating whether the restore status histogram has been recorded.
*
* @param isRecorded Whether the restore status is recorded.
*/
@VisibleForTesting
static void setRestoreStatusRecorded(boolean isRecorded) {
ContextUtils.getAppSharedPreferences()
.edit()
.putBoolean(RESTORE_STATUS_RECORDED, isRecorded)
.apply();
}
/** Record the restore histogram. To be called from Chrome itself once it is running. */
public static void recordRestoreHistogram() {
boolean isStatusRecorded = isRestoreStatusRecorded();
// Ensure restore status is only recorded once.
if (isStatusRecorded) {
return;
}
@RestoreStatus int restoreStatus = getRestoreStatus();
if (restoreStatus != RestoreStatus.DEPRECATED_RESTORE_STATUS_RECORDED
&& restoreStatus != RestoreStatus.DEPRECATED_SIGNIN_TIMED_OUT) {
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_ANDROID_RESTORE_RESULT, restoreStatus, RestoreStatus.NUM_ENTRIES);
}
setRestoreStatusRecorded(true);
}
@NativeMethods
interface Natives {
// See PrefService::CommitPendingWrite().
void commitPendingPrefWrites(PrefService prefService);
// Returns a serialized version of PrefService::GetDict(), which can be stored in backups.
@JniType("std::string")
String getSerializedDict(PrefService prefService, @JniType("std::string") String prefName);
// If `serializedDict` was obtained from `getSerializedDict(prefService, prefName)`,
// deserializes and passes the result to PrefService::SetDict(). If deserialization fails,
// does nothing.
void setDict(
PrefService prefService,
@JniType("std::string") String prefName,
@JniType("std::string") String serializedDict);
// Calls syncer::MigrateGlobalDataTypePrefsToAccount() to migrate global boolean sync prefs
// to account settings.
void migrateGlobalDataTypePrefsToAccount(
PrefService prefService, @JniType("std::string") String gaiaId);
}
}