Chromium Code Reviews| 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); |
| } |