Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1110)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java

Issue 2496693002: Implement Android key/value backup (Closed)
Patch Set: Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
index e3c7fe55ae353c07798a921bfa0ca4efe8f62e2a..33622f6e56906201433db3d0199f95435d5356e4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
@@ -4,289 +4,255 @@
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.ThreadUtils;
import org.chromium.base.VisibleForTesting;
-import org.chromium.base.annotations.SuppressFBWarnings;
+import org.chromium.base.library_loader.ProcessInitException;
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.AccountManagerHelper;
import org.chromium.components.signin.ChromeSigninController;
-import org.json.JSONException;
-import org.json.JSONObject;
-import java.io.File;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
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.ArrayList;
import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.concurrent.Callable;
/**
- * 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.
+ * Backup agent for Chrome, using Android key/value backup.
*/
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+
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";
// Lists of preferences that should be restored unchanged.
- private static final String[] RESTORED_ANDROID_PREFS = {
+ static final String[] BACKUP_ANDROID_BOOL_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 boolean accountExistsOnDevice(String userName) {
+ return AccountManagerHelper.get(this).getAccountFromName(userName) != null;
+ }
- private static final String[] DEFAULT_JSON_PREFS_FILE = {
- // chrome::kInitialProfile
- "Default",
- // chrome::kPreferencesFilename
- "Preferences",
- };
+ private void recordHashCode(ParcelFileDescriptor newState, long hashCode) throws IOException {
+ // And record the hash in the output data
+ FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
+ DataOutputStream out = new DataOutputStream(outstream);
+ out.writeLong(hashCode);
+ out.close();
+ }
- private static boolean sAllowChromeApplication = false;
+ private long getHashCode(ArrayList<String> backupNames, ArrayList<byte[]> backupValues) {
+ long hashCode = backupNames.hashCode() ^ Arrays.deepHashCode(backupValues.toArray());
Bernhard Bauer 2016/11/14 10:05:38 hashCode() isn't guaranteed to avoid collisions, a
aberent 2016/11/14 17:23:39 Actually we can use the whole data unmodified (jus
+ return hashCode;
+ }
@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
- }
+ final ChromeBackupAgent backupAgent = this;
- @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
- }
+ final ArrayList<String> backupNames = new ArrayList<String>();
+ final ArrayList<byte[]> backupValues = new ArrayList<byte[]>();
- // 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();
- }
+ // The native preferences can only be read on the UI thread.
+ if (!ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ // Start the browser if necessary, so that we can access the native preferences
+ try {
+ ChromeBrowserInitializer.getInstance(backupAgent)
+ .handleSynchronousStartup();
+ } catch (ProcessInitException e) {
+ Log.w(TAG, "Browser launch failed on backup");
Bernhard Bauer 2016/11/14 10:05:38 Maybe log the exception as well?
aberent 2016/11/14 17:23:39 Done.
+ return false;
+ }
+ String[] nativeBackupNames = nativeGetBoolBackupNames();
+ boolean[] nativeBackupValues = nativeGetBoolBackupValues();
+ assert nativeBackupNames.length == nativeBackupValues.length;
- 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;
+ for (String name : nativeBackupNames) {
+ backupNames.add(NATIVE_PREF_PREFIX + name);
+ }
+ for (Boolean val : nativeBackupValues) {
Bernhard Bauer 2016/11/14 10:05:38 Unboxed boolean?
aberent 2016/11/14 17:23:39 Done.
+ backupValues.add(val ? new byte[] {1} : new byte[] {0});
Bernhard Bauer 2016/11/14 10:05:38 Extract this part into a helper method?
aberent 2016/11/14 17:23:39 Done, assuming you meant the bool to byte conversi
+ }
+ return true;
+ }
+ })) {
+ // Something went wrong reading the native preferences, skip the backup.
+ return;
}
- return false;
+ // 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(
+ sharedPrefs.getBoolean(prefName, false) ? new byte[] {1} : new byte[] {0});
+ }
+ }
+
+ // Finally we need to add the user id
+ backupNames.add(ANDROID_DEFAULT_PREFIX + ChromeSigninController.SIGNED_IN_ACCOUNT_KEY);
+ backupValues.add(
+ sharedPrefs.getString(ChromeSigninController.SIGNED_IN_ACCOUNT_KEY, "").getBytes());
+ long hashCode = getHashCode(backupNames, backupValues);
+
+ // Check if we actually need to do a backup
+ try {
+ FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
+ DataInputStream in = new DataInputStream(instream);
+ long oldHash = in.readLong();
+ in.close();
+ if (oldHash == hashCode) {
+ // Nothing has changed.
+ Log.i(TAG, "Nothing changed - backup skipped");
+ return;
+ }
+ } catch (IOException e) {
+ // This will happen if this is the first time we have 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);
+ }
+ recordHashCode(newState, hashCode);
+
+ Log.i(TAG, "Backup complete");
}
@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");
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ // Check that the user hasn't already seen FRE (not sure if this can ever happen, but we
+ // certainly don't want to overwrite anything if the user has).
+ SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
+ if (sharedPrefs.getBoolean(FirstRunStatus.FIRST_RUN_FLOW_COMPLETE, false)
+ || sharedPrefs.getBoolean(
+ FirstRunStatus.LIGHTWEIGHT_FIRST_RUN_FLOW_COMPLETE, false)) {
+ Log.w(TAG, "Restore attempted after first run");
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);
+ // Read the keys into a map first, since we need to do some checks before we restore
+ // anything. This is also needed for the hash code.
+ 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);
+ }
}
- // 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");
+ // Chrome has to be running before we can check if the account exists
+ final ChromeBackupAgent backupAgent = this;
+ if (!ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ // Start the browser if necessary, so that we can access the native preferences
+ try {
+ ChromeBrowserInitializer.getInstance(backupAgent)
+ .handleSynchronousStartup();
+ } catch (ProcessInitException e) {
+ Log.w(TAG, "Browser launch failed on restore");
+ return false;
+ }
+ return true;
+ }
+ })) {
+ // Something went wrong starting Chrome, skip the restore.
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");
+ // If the user hasn't signed in, or can't sign in, then don't restore anything.
+ if (restoredUserName == null || !accountExistsOnDevice(restoredUserName)) {
+ Log.i(TAG, "Chrome was not signed in with a known account name, not restoring");
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();
- }
+ // Restore the native preferences on the UI thread
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+ @Override
+ public void run() {
+ ArrayList<String> nativeBackupNames = new ArrayList<String>();
+ 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] = (backupValues.get(i)[0] != 0);
+ count++;
+ }
+ }
+ nativeSetBoolBackupPrefs(
+ nativeBackupNames.toArray(new String[count]), nativeBackupValues);
+ }
+ });
- @SuppressLint("CommitPrefEdits")
- private void restoreAndroidPrefs(SharedPreferences sharedPrefs, String userName) {
- Set<String> prefNames = sharedPrefs.getAll().keySet();
+ // Now that everything looks good we can restore the Android preferences.
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);
+ // 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), backupValues.get(i)[0] != 0);
+ }
}
+
// 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);
- }
- }
+ editor.putString(
+ FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_ACCOUNT_NAME, restoredUserName);
+ editor.apply();
- @VisibleForTesting
- protected long getFileLength(File prefsFile) {
- return prefsFile.length();
+ // Finally record the hash code to avoid unnecessary future backups.
+ recordHashCode(newState, getHashCode(backupNames, backupValues));
+ Log.i(TAG, "Restore complete");
}
@VisibleForTesting
- protected InputStream openInputStream(File prefsFile) throws FileNotFoundException {
- return new FileInputStream(prefsFile);
- }
+ protected static native String[] nativeGetBoolBackupNames();
@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();
- }
+ protected static native boolean[] nativeGetBoolBackupValues();
@VisibleForTesting
- static void allowChromeApplicationForTesting() {
- sAllowChromeApplication = true;
- }
+ protected static native void nativeSetBoolBackupPrefs(String[] name, boolean[] value);
}

Powered by Google App Engine
This is Rietveld 408576698