Merge "Preferences backup & restore."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1cddc54..e6f05e3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -12,7 +12,14 @@
android:name=".DocumentsApplication"
android:label="@string/app_label"
android:icon="@drawable/app_icon"
- android:supportsRtl="true">
+ android:supportsRtl="true"
+ android:allowBackup="true"
+ android:backupAgent=".prefs.BackupAgent"
+ android:fullBackupOnly="false">
+
+ <meta-data
+ android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAInBA8ued0O_ZyYUsVhwinUF-x50NIe9K0GzBW4A" />
<activity
android:name=".picker.PickActivity"
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 59fbc27..2cfb0df 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -54,6 +54,7 @@
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.prefs.LocalPreferences;
+import com.android.documentsui.prefs.PreferencesMonitor;
import com.android.documentsui.queries.DebugCommandProcessor;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
@@ -99,6 +100,8 @@
private long mStartTime;
+ private PreferencesMonitor mPreferencesMonitor;
+
public BaseActivity(@LayoutRes int layoutId, String tag) {
mLayoutId = layoutId;
mTag = tag;
@@ -180,6 +183,8 @@
mSearchManager = new SearchViewManager(searchListener, dbgCommands, icicle);
mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
+ mPreferencesMonitor = new PreferencesMonitor(getApplicationContext());
+
// Base classes must update result in their onCreate.
setResult(Activity.RESULT_CANCELED);
}
@@ -211,6 +216,18 @@
}
@Override
+ protected void onResume() {
+ super.onResume();
+ mPreferencesMonitor.start();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mPreferencesMonitor.stop();
+ }
+
+ @Override
@CallSuper
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
diff --git a/src/com/android/documentsui/prefs/BackupAgent.java b/src/com/android/documentsui/prefs/BackupAgent.java
new file mode 100644
index 0000000..65a94ca
--- /dev/null
+++ b/src/com/android/documentsui/prefs/BackupAgent.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.documentsui.prefs;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.SharedPreferences;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.VisibleForTesting;
+
+import java.io.IOException;
+
+/**
+ * Provides glue between backup infrastructure and PrefsBackupHelper (which contains the core logic
+ * for retrieving and restoring settings).
+ *
+ * When doing backup & restore, we create and add a {@link SharedPreferencesBackupHelper} for our
+ * backup preferences file in {@link #onCreate}, and populate the backup preferences file in
+ * {@link #onBackup} and {@link #onRestore}. Then {@link BackupAgentHelper#onBackup} and
+ * {@link BackupAgentHelper#onRestore} will take care of the rest of the work. See external
+ * documentation below.
+ *
+ * https://developer.android.com/guide/topics/data/keyvaluebackup.html#BackupAgentHelper
+ */
+public class BackupAgent extends BackupAgentHelper {
+
+ /**
+ * Name of the shared preferences file used by BackupAgent. Should only be used in
+ * this class.
+ *
+ * @see #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)
+ * @see #onRestore(BackupDataInput, int, ParcelFileDescriptor)
+ */
+ private static final String BACKUP_PREFS = "documentsui_backup_prefs";
+
+ /**
+ * An arbitrary string used by the BackupHelper.
+ *
+ * BackupAgentHelper works with BackupHelper. When adding a BackupHelper in #onCreate,
+ * it requires a "key". This string is that "key".
+ *
+ * https://developer.android.com/guide/topics/data/keyvaluebackup.html#BackupAgentHelper
+ * See "Backing up SharedPreference" for the purpose of this string.
+ *
+ * @see #onCreate()
+ */
+ private static final String BACKUP_HELPER_KEY = "DOCUMENTSUI_BACKUP_HELPER_KEY";
+
+ private PrefsBackupHelper mPrefsBackupHelper;
+ private SharedPreferences mBackupPreferences;
+
+ @Override
+ public void onCreate() {
+ addHelper(BACKUP_HELPER_KEY, new SharedPreferencesBackupHelper(this, BACKUP_PREFS));
+ mPrefsBackupHelper = new PrefsBackupHelper(this);
+ mBackupPreferences = getSharedPreferences(BACKUP_PREFS, MODE_PRIVATE);
+ }
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ mPrefsBackupHelper.getBackupPreferences(mBackupPreferences);
+ super.onBackup(oldState, data, newState);
+ mBackupPreferences.edit().clear().apply();
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ // TODO: refresh the UI after restore finished. Currently the restore for system apps only
+ // happens during SUW, at which time user haven't open the app. However the restore time may
+ // change in ODR. Once it happens, we may need to refresh the UI after restore finished.
+ super.onRestore(data, appVersionCode, newState);
+ mPrefsBackupHelper.putBackupPreferences(mBackupPreferences);
+ mBackupPreferences.edit().clear().apply();
+ }
+
+}
diff --git a/src/com/android/documentsui/prefs/LocalPreferences.java b/src/com/android/documentsui/prefs/LocalPreferences.java
index 1beaf5d..f5a70fc 100644
--- a/src/com/android/documentsui/prefs/LocalPreferences.java
+++ b/src/com/android/documentsui/prefs/LocalPreferences.java
@@ -123,4 +123,8 @@
? userId + "|" + packageName + "||" + directory
: userId + "|" + packageName + "|" + uuid + "|" + directory;
}
+
+ static boolean shouldBackup(String s) {
+ return (s != null) ? s.startsWith(ROOT_VIEW_MODE_PREFIX) : false;
+ }
}
diff --git a/src/com/android/documentsui/prefs/PreferencesMonitor.java b/src/com/android/documentsui/prefs/PreferencesMonitor.java
new file mode 100644
index 0000000..33a4749
--- /dev/null
+++ b/src/com/android/documentsui/prefs/PreferencesMonitor.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.prefs;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+/**
+ * A class that monitors changes to the default shared preferences file. If a preference which
+ * should be backed up changed, schedule a backup.
+ */
+public final class PreferencesMonitor
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private Context mContext;
+
+ public PreferencesMonitor(Context context) {
+ mContext = context;
+ }
+
+ public void start() {
+ PreferenceManager.getDefaultSharedPreferences(mContext)
+ .registerOnSharedPreferenceChangeListener(this);
+ }
+
+ public void stop() {
+ PreferenceManager.getDefaultSharedPreferences(mContext)
+ .unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (PrefsBackupHelper.shouldBackup(key)) {
+ BackupManager.dataChanged(mContext.getPackageName());
+ }
+ }
+}
diff --git a/src/com/android/documentsui/prefs/PrefsBackupHelper.java b/src/com/android/documentsui/prefs/PrefsBackupHelper.java
new file mode 100644
index 0000000..8e7741d
--- /dev/null
+++ b/src/com/android/documentsui/prefs/PrefsBackupHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.documentsui.prefs;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.Map;
+
+/**
+ * Class providing core logic for backup and restore of DocumentsUI preferences.
+ */
+final class PrefsBackupHelper {
+
+ private SharedPreferences mDefaultPreferences;
+
+ @VisibleForTesting
+ PrefsBackupHelper(SharedPreferences overridePreferences) {
+ mDefaultPreferences = overridePreferences;
+ }
+
+ PrefsBackupHelper(Context context) {
+ mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * Loads all applicable preferences to supplied backup file.
+ */
+ void getBackupPreferences(SharedPreferences prefs) {
+ Editor editor = prefs.edit();
+ editor.clear();
+
+ copyMatchingPreferences(mDefaultPreferences, editor);
+ editor.apply();
+ }
+
+ /**
+ * Restores all applicable preferences from the supplied preferences file.
+ */
+ void putBackupPreferences(SharedPreferences prefs) {
+ Editor editor = mDefaultPreferences.edit();
+
+ copyMatchingPreferences(prefs, editor);
+ editor.apply();
+ }
+
+ private void copyMatchingPreferences(SharedPreferences source, Editor destination) {
+ for (Map.Entry<String, ?> preference : source.getAll().entrySet()) {
+ if (shouldBackup(preference.getKey())) {
+ setPreference(destination, preference);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @VisibleForTesting
+ void setPreference(Editor target, final Map.Entry<String, ?> preference) {
+ final String key = preference.getKey();
+ final Object value = preference.getValue();
+ // Only handle already know types.
+ if (value instanceof Integer) {
+ target.putInt(key, (Integer) value);
+ } else if (value instanceof Boolean) {
+ target.putBoolean(key, (Boolean) value);
+ } else {
+ throw new IllegalArgumentException("DocumentsUI backup: invalid preference "
+ + (value == null ? null : value.getClass()));
+ }
+ }
+
+ static boolean shouldBackup(String s) {
+ return LocalPreferences.shouldBackup(s) || ScopedPreferences.shouldBackup(s);
+ }
+}
diff --git a/src/com/android/documentsui/prefs/ScopedPreferences.java b/src/com/android/documentsui/prefs/ScopedPreferences.java
index c74753a..51dbb1b 100644
--- a/src/com/android/documentsui/prefs/ScopedPreferences.java
+++ b/src/com/android/documentsui/prefs/ScopedPreferences.java
@@ -28,6 +28,8 @@
*/
public interface ScopedPreferences {
+ static final String INCLUDE_DEVICE_ROOT = "includeDeviceRoot-";
+
boolean getShowDeviceRoot();
void setShowDeviceRoot(boolean display);
@@ -42,8 +44,6 @@
static final class RuntimeScopedPreferences implements ScopedPreferences {
- private static final String INCLUDE_DEVICE_ROOT = "includeDeviceRoot-";
-
private SharedPreferences mSharedPrefs;
private String mScope;
@@ -64,4 +64,8 @@
mSharedPrefs.edit().putBoolean(INCLUDE_DEVICE_ROOT + mScope, display).apply();
}
}
+
+ static boolean shouldBackup(String s) {
+ return (s != null) ? s.startsWith(INCLUDE_DEVICE_ROOT) : false;
+ }
}
diff --git a/tests/unit/com/android/documentsui/prefs/PrefsBackupHelperTest.java b/tests/unit/com/android/documentsui/prefs/PrefsBackupHelperTest.java
new file mode 100644
index 0000000..9cb9cd6
--- /dev/null
+++ b/tests/unit/com/android/documentsui/prefs/PrefsBackupHelperTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.prefs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PrefsBackupHelperTest {
+
+ private static final String LOCAL_PREFERENCE_1 = "rootViewMode-validPreference1";
+ private static final String LOCAL_PREFERENCE_2 = "rootViewMode-validPreference2";
+ private static final String SCOPED_PREFERENCE = "includeDeviceRoot-validPreference";
+ private static final String NON_BACKUP_PREFERENCE = "notBackup-invalidPreference";
+
+ private SharedPreferences mDefaultPrefs;
+ private SharedPreferences mBackupPrefs;
+ private PrefsBackupHelper mPrefsBackupHelper;
+
+ @Before
+ public void setUp() {
+ mDefaultPrefs = InstrumentationRegistry.getContext().getSharedPreferences("prefs1", 0);
+ mBackupPrefs = InstrumentationRegistry.getContext().getSharedPreferences("prefs2", 0);
+ mPrefsBackupHelper = new PrefsBackupHelper(mDefaultPrefs);
+ }
+
+ @Test
+ public void testPrepareBackupFile_BackupLocalPreferences() {
+ mDefaultPrefs.edit().putInt(LOCAL_PREFERENCE_1, 1).commit();
+
+ mPrefsBackupHelper.getBackupPreferences(mBackupPrefs);
+
+ assertEquals(mBackupPrefs.getInt(LOCAL_PREFERENCE_1, 0), 1);
+ }
+
+ @Test
+ public void testPrepareBackupFile_BackupScopedPreferences() {
+ mDefaultPrefs.edit().putBoolean(SCOPED_PREFERENCE, true).commit();
+
+ mPrefsBackupHelper.getBackupPreferences(mBackupPrefs);
+
+ assertEquals(mBackupPrefs.getBoolean(SCOPED_PREFERENCE, false), true);
+ }
+
+ @Test
+ public void testPrepareBackupFile_BackupNotInterestedPreferences() {
+ mDefaultPrefs.edit().putBoolean(NON_BACKUP_PREFERENCE, true).commit();
+
+ mPrefsBackupHelper.getBackupPreferences(mBackupPrefs);
+
+ assertFalse(mBackupPrefs.contains(NON_BACKUP_PREFERENCE));
+ }
+
+ @Test
+ public void testPrepareBackupFile_BackupUnexpectedType() throws Exception {
+ // Currently only Integer and Boolean type are supported.
+ mDefaultPrefs.edit().putString(LOCAL_PREFERENCE_1, "String is not accepted").commit();
+
+ try {
+ mPrefsBackupHelper.getBackupPreferences(mBackupPrefs);
+ fail();
+ } catch(IllegalArgumentException e) {
+
+ } finally {
+ assertFalse(mBackupPrefs.contains(LOCAL_PREFERENCE_1));
+ }
+ }
+
+ @Test
+ public void testRestorePreferences_RestoreLocalPreferences() {
+ mBackupPrefs.edit().putInt(LOCAL_PREFERENCE_1, 1).commit();
+
+ mPrefsBackupHelper.putBackupPreferences(mBackupPrefs);
+
+ assertEquals(mDefaultPrefs.getInt(LOCAL_PREFERENCE_1, 0), 1);
+ }
+
+ @Test
+ public void testRestorePreferences_RestoreScopedPreferences() {
+ mBackupPrefs.edit().putBoolean(SCOPED_PREFERENCE, true).commit();
+
+ mPrefsBackupHelper.putBackupPreferences(mBackupPrefs);
+
+ assertEquals(mDefaultPrefs.getBoolean(SCOPED_PREFERENCE, false), true);
+ }
+
+ @Test
+ public void testEndToEnd() {
+ // Simulating an end to end backup & restore process. At the begining, all preferences are
+ // stored in the default shared preferences file, includes preferences that we don't want
+ // to backup.
+ //
+ // On backup, we copy all preferences that we want to backup to the backup shared
+ // preferences file, and then backup that single file.
+ //
+ // On restore, we restore the backup file first, and then copy all preferences in the backup
+ // file to the app's default shared preferences file.
+
+ SharedPreferences.Editor editor = mDefaultPrefs.edit();
+
+ // Set preferences to the default file, includes preferences that are not backed up.
+ editor.putInt(LOCAL_PREFERENCE_1, 1);
+ editor.putInt(LOCAL_PREFERENCE_2, 2);
+ editor.putBoolean(SCOPED_PREFERENCE, true);
+ editor.putBoolean(NON_BACKUP_PREFERENCE, true);
+ editor.commit();
+
+ // Write all backed up preferences to backup shared preferences file.
+ mPrefsBackupHelper.getBackupPreferences(mBackupPrefs);
+
+ // Assume we are doing backup to the backup file.
+
+ // Clear all preferences in default shared preferences file.
+ editor.clear().commit();
+
+ // Assume we are doing restore to the backup file.
+
+ // Copy all backuped preferences to default shared preferences file.
+ mPrefsBackupHelper.putBackupPreferences(mBackupPrefs);
+
+ // Check all preferences are correctly restored.
+ assertEquals(mDefaultPrefs.getInt(LOCAL_PREFERENCE_1, 0), 1);
+ assertEquals(mDefaultPrefs.getInt(LOCAL_PREFERENCE_2, 0), 2);
+ assertEquals(mDefaultPrefs.getBoolean(SCOPED_PREFERENCE, false), true);
+ assertFalse(mDefaultPrefs.contains(NON_BACKUP_PREFERENCE));
+ }
+
+ @Test
+ public void testPreferenceTypesSupport() {
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("int", (Integer) 1);
+ map.put("float", (Float) 0.1f);
+ map.put("long", (Long) 10000000000l);
+ map.put("boolean", true);
+ map.put("String", "String");
+ Set<String> stringSet = new HashSet<String>();
+ stringSet.add("string1");
+ stringSet.add("string2");
+ map.put("StringSet", stringSet);
+
+ // SharedPreferences accept Integer, Float, Long, Boolean, String, Set<String> types.
+ // Currently in DocumentsUI, only Integer and Boolean preferences are backed up.
+ for (Map.Entry<String, ?> entry : map.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ Editor editor = mDefaultPrefs.edit().clear();
+ if (value instanceof Integer) {
+ mPrefsBackupHelper.setPreference(editor, entry);
+ editor.apply();
+ assertEquals(mDefaultPrefs.getInt("int", 0), 1);
+ } else if(value instanceof Boolean) {
+ mPrefsBackupHelper.setPreference(editor, entry);
+ editor.apply();
+ assertEquals(mDefaultPrefs.getBoolean("boolean", false), true);
+ } else {
+ try {
+ mPrefsBackupHelper.setPreference(editor, entry);
+ fail();
+ } catch(IllegalArgumentException e) {
+
+ } finally {
+ editor.apply();
+ assertFalse(mDefaultPrefs.contains(key));
+ }
+ }
+ }
+ }
+}