| 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 360b0dbcbe99e70439b78089a227c5d89672ba31..865f12d4ca2f0301427938cc14072f52e209c5e0 100644
|
| --- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeBackupAgent.java
|
| @@ -10,30 +10,40 @@ 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.PrivacyPreferences;
|
| import org.chromium.sync.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.
|
| + * 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 {
|
| @@ -42,17 +52,69 @@ public class ChromeBackupAgent extends BackupAgent {
|
|
|
| // Lists of preferences that should be restored unchanged.
|
|
|
| - // TODO(aberent): At present this only restores the signed in user, and the FRE settings
|
| - // (whether is has been completed, and whether the user disabled crash dump reporting). It
|
| - // should also restore all the user's sync choices. This will involve restoring a number of
|
| - // Chrome preferences (in Chrome's JSON preference file) in addition to the Android preferences
|
| - // currently restored.
|
| private static final String[] RESTORED_ANDROID_PREFS = {
|
| PrivacyPreferences.PREF_CRASH_DUMP_UPLOAD,
|
| FirstRunStatus.FIRST_RUN_FLOW_COMPLETE,
|
| FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_SETUP,
|
| };
|
|
|
| + // Sync preferences, all in C++ sync_driver::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
|
| @@ -78,7 +140,7 @@ public class ChromeBackupAgent extends BackupAgent {
|
|
|
| private boolean accountExistsOnDevice(String userName) {
|
| // This cannot use AccountManagerHelper, since that depends on ChromeApplication.
|
| - for (Account account: getAccounts()) {
|
| + for (Account account : getAccounts()) {
|
| if (account.name.equals(userName)) return true;
|
| }
|
| return false;
|
| @@ -95,18 +157,42 @@ public class ChromeBackupAgent extends BackupAgent {
|
| // This is running without a ChromeApplication instance, so this has to be done here.
|
| ContextUtils.initApplicationContext(getApplicationContext());
|
| SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
|
| - Set<String> prefNames = sharedPrefs.getAll().keySet();
|
| // 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)) {
|
| - boolean commitResult = sharedPrefs.edit().clear().commit();
|
| - Log.d(TAG, "onRestoreFinished complete, nothing restored; commit result = %s",
|
| - commitResult);
|
| + 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");
|
| + }
|
| +
|
| + private void clearAllPrefs(SharedPreferences sharedPrefs, File prefsFile) {
|
| + deleteFileIfPossible(prefsFile);
|
| + sharedPrefs.edit().clear().commit();
|
| + }
|
| +
|
| + private boolean 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));
|
| @@ -118,12 +204,82 @@ public class ChromeBackupAgent extends BackupAgent {
|
| // if any. If the rest of FRE has been completed this will happen silently.
|
| editor.putString(FirstRunSignInProcessor.FIRST_RUN_FLOW_SIGNIN_ACCOUNT_NAME, userName);
|
| boolean commitResult = editor.commit();
|
| + return commitResult;
|
| + }
|
| +
|
| + 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);
|
| + }
|
|
|
| - Log.d(TAG, "onRestoreFinished complete; commit result = %s", commitResult);
|
| + @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;
|
| + sAllowChromeApplication = true;
|
| }
|
| }
|
|
|