summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTransformer Team <transformer-eng@google.com>2017-02-07 16:14:05 +0000
committerPetr Cermak <petrcermak@google.com>2017-02-14 11:26:33 +0000
commit1e64246154401f7798c9070f74e45de6891c9d62 (patch)
tree8fac355875d3d9a381a0aab4ded8d7db3dca86f6
parentffb9ec8fb712fd6cf6205508cb508d23104c130c (diff)
downloadplatform_external_libbackup-1e64246154401f7798c9070f74e45de6891c9d62.tar.gz
platform_external_libbackup-1e64246154401f7798c9070f74e45de6891c9d62.tar.bz2
platform_external_libbackup-1e64246154401f7798c9070f74e45de6891c9d62.zip
Import from google3
See google3/third_party/java_src/android_libs/backup/copy.bara.sky. Project import generated by Copybara (go/copybara). Bug: 34489332 Bug: 35093989 Bug: 35129081 PiperOrigin-RevId: 146787977 Change-Id: I904e1ba370a33212aca8b7840ee78d6aee2e0924
-rw-r--r--LICENSE202
-rw-r--r--src/com/google/android/libraries/backup/Backup.java13
-rw-r--r--src/com/google/android/libraries/backup/BackupKeyPredicate.java8
-rw-r--r--src/com/google/android/libraries/backup/BackupKeyPredicates.java169
-rw-r--r--src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java212
-rw-r--r--src/com/google/android/libraries/backup/PreferenceBackupUtil.java99
-rw-r--r--src/com/google/android/libraries/backup/shadow/BackupAgentHelperShadow.java181
-rw-r--r--src/com/google/android/libraries/backup/shadow/BackupHelperSimulator.java28
-rw-r--r--src/com/google/android/libraries/backup/shadow/FileBackupHelperSimulator.java157
-rw-r--r--src/com/google/android/libraries/backup/shadow/SharedPreferencesBackupHelperSimulator.java147
10 files changed, 1216 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/src/com/google/android/libraries/backup/Backup.java b/src/com/google/android/libraries/backup/Backup.java
new file mode 100644
index 0000000..372f5a7
--- /dev/null
+++ b/src/com/google/android/libraries/backup/Backup.java
@@ -0,0 +1,13 @@
+package com.google.android.libraries.backup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a static field should be backed up. This should ONLY be used on static fields.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Backup {} \ No newline at end of file
diff --git a/src/com/google/android/libraries/backup/BackupKeyPredicate.java b/src/com/google/android/libraries/backup/BackupKeyPredicate.java
new file mode 100644
index 0000000..6c85dbd
--- /dev/null
+++ b/src/com/google/android/libraries/backup/BackupKeyPredicate.java
@@ -0,0 +1,8 @@
+package com.google.android.libraries.backup;
+
+/** A predicate that determines whether a given key should be backed up. */
+public interface BackupKeyPredicate {
+
+ /** Returns whether a given key should be backed up. */
+ boolean shouldBeBackedUp(String key);
+}
diff --git a/src/com/google/android/libraries/backup/BackupKeyPredicates.java b/src/com/google/android/libraries/backup/BackupKeyPredicates.java
new file mode 100644
index 0000000..56570ad
--- /dev/null
+++ b/src/com/google/android/libraries/backup/BackupKeyPredicates.java
@@ -0,0 +1,169 @@
+package com.google.android.libraries.backup;
+
+import android.content.Context;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Static utility methods returning {@link BackupKeyPredicate} instances. */
+public class BackupKeyPredicates {
+
+ /**
+ * Returns a predicate that determines whether a key was defined as a field with the given
+ * annotation in one of the given classes. Assumes that the given annotation and classes are
+ * valid. You must ensure that proguard does not remove your annotation or any fields annotated
+ * with it.
+ *
+ * @see Backup
+ */
+ public static BackupKeyPredicate buildPredicateFromAnnotatedFieldsIn(
+ Class<? extends Annotation> annotation, Class<?>... klasses) {
+ return in(getAnnotatedFieldValues(annotation, klasses));
+ }
+
+ /**
+ * Returns a predicate that determines whether a key matches a regex that was defined as a field
+ * with the given annotation in one of the given classes. The test used is equivalent to
+ * {@link #containsPattern(String)} for each annotated field value. Assumes that the given
+ * annotation and classes are valid. You must ensure that proguard does not remove your annotation
+ * or any fields annotated with it.
+ *
+ * @see Backup
+ */
+ public static BackupKeyPredicate buildPredicateFromAnnotatedRegexFieldsIn(
+ Class<? extends Annotation> annotation, Class<?>... klasses) {
+ Set<String> patterns = getAnnotatedFieldValues(annotation, klasses);
+ Set<BackupKeyPredicate> patternPredicates = new HashSet<>();
+ for (String pattern : patterns) {
+ patternPredicates.add(containsPattern(pattern));
+ }
+ return or(patternPredicates);
+ }
+
+ private static Set<String> getAnnotatedFieldValues(
+ Class<? extends Annotation> annotation, Class<?>... klasses) {
+ Set<String> values = new HashSet<>();
+ for (Class<?> klass : klasses) {
+ addAnnotatedFieldValues(annotation, klass, values);
+ }
+ return values;
+ }
+
+ private static void addAnnotatedFieldValues(
+ Class<? extends Annotation> annotation, Class<?> klass, Set<String> values) {
+ for (Field field : klass.getDeclaredFields()) {
+ addFieldValueIfAnnotated(annotation, field, values);
+ }
+ }
+
+ private static void addFieldValueIfAnnotated(
+ Class<? extends Annotation> annotation, Field field, Set<String> values) {
+ if (field.isAnnotationPresent(annotation) && field.getType().equals(String.class)) {
+ try {
+ values.add((String) field.get(null));
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+
+ /**
+ * Returns a predicate that determines whether a key is a member of the given collection. Changes
+ * to the given collection will change the returned predicate.
+ */
+ public static BackupKeyPredicate in(final Collection<? extends String> collection) {
+ if (collection == null) {
+ throw new NullPointerException("Null collection given.");
+ }
+ return new BackupKeyPredicate() {
+ @Override
+ public boolean shouldBeBackedUp(String key) {
+ return collection.contains(key);
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that determines whether a key contains any match for the given regular
+ * expression pattern. The test used is equivalent to {@link Matcher#find()}.
+ */
+ public static BackupKeyPredicate containsPattern(String pattern) {
+ final Pattern compiledPattern = Pattern.compile(pattern);
+ return new BackupKeyPredicate() {
+ @Override
+ public boolean shouldBeBackedUp(String key) {
+ return compiledPattern.matcher(key).find();
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that determines whether a key passes any of the given predicates. Each
+ * predicate is evaluated in the order given, and the evaluation process stops as soon as an
+ * accepting predicate is found. Changes to the given iterable will not change the returned
+ * predicate. The returned predicate returns {@code false} for any key if the given iterable is
+ * empty.
+ */
+ public static BackupKeyPredicate or(Iterable<BackupKeyPredicate> predicates) {
+ final List<BackupKeyPredicate> copiedPredicates = new ArrayList<>();
+ for (BackupKeyPredicate predicate : predicates) {
+ copiedPredicates.add(predicate);
+ }
+ return orDefensivelyCopied(new ArrayList<>(copiedPredicates));
+ }
+
+ /**
+ * Returns a predicate that determines whether a key passes any of the given predicates. Each
+ * predicate is evaluated in the order given, and the evaluation process stops as soon as an
+ * accepting predicate is found. The returned predicate returns {@code false} for any key if no
+ * there are no given predicates.
+ */
+ public static BackupKeyPredicate or(BackupKeyPredicate... predicates) {
+ return orDefensivelyCopied(Arrays.asList(predicates));
+ }
+
+ private static BackupKeyPredicate orDefensivelyCopied(
+ final Iterable<BackupKeyPredicate> predicates) {
+ return new BackupKeyPredicate() {
+ @Override
+ public boolean shouldBeBackedUp(String key) {
+ for (BackupKeyPredicate predicate : predicates) {
+ if (predicate.shouldBeBackedUp(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that determines whether a key is one of the resources from the provided
+ * resource IDs. Assumes that all of the given resource IDs are valid.
+ */
+ public static BackupKeyPredicate buildPredicateFromResourceIds(
+ Context context, Collection<Integer> ids) {
+ Set<String> keys = new HashSet<>();
+ for (Integer id : ids) {
+ keys.add(context.getString(id));
+ }
+ return in(keys);
+ }
+
+ /** Returns a predicate that returns true for any key. */
+ public static BackupKeyPredicate alwaysTrue() {
+ return new BackupKeyPredicate() {
+ @Override
+ public boolean shouldBeBackedUp(String key) {
+ return true;
+ }
+ };
+ }
+}
diff --git a/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java b/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
new file mode 100644
index 0000000..a107281
--- /dev/null
+++ b/src/com/google/android/libraries/backup/PersistentBackupAgentHelper.java
@@ -0,0 +1,212 @@
+package com.google.android.libraries.backup;
+
+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.content.SharedPreferences.Editor;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link BackupAgentHelper} that contains the following improvements:
+ *
+ * <p>1) All backed-up shared preference files will automatically be restored; the app does not need
+ * to know the list of files in advance at restore time. This is important for apps that generate
+ * files dynamically, and it's also important for all apps that use restoreAnyVersion because
+ * additional files could have been added.
+ *
+ * <p>2) Only the requested keys will be backed up from each shared preference file. All keys that
+ * were backed up will be restored.
+ *
+ * <p>These benefits apply only to shared preference files. Other file helpers can be added in the
+ * normal way for a {@link BackupAgentHelper}.
+ *
+ * <p>This class works by creating a separate shared preference file named
+ * {@link #RESERVED_SHARED_PREFERENCES} that it backs up and restores. Before backing up, this file
+ * is populated based on the requested shared preference files and keys. After restoring, the data
+ * is copied back into the original files.
+ */
+public abstract class PersistentBackupAgentHelper extends BackupAgentHelper {
+
+ /**
+ * The name of the shared preferences file reserved for use by the
+ * {@link PersistentBackupAgentHelper}. Files with this name cannot be backed up by this helper.
+ */
+ protected static final String RESERVED_SHARED_PREFERENCES = "persistent_backup_agent_helper";
+
+ private static final String TAG = "PersistentBackupAgentHe"; // The max tag length is 23.
+ private static final String BACKUP_KEY = RESERVED_SHARED_PREFERENCES + "_prefs";
+ private static final String BACKUP_DELIMITER = "/";
+
+ @Override
+ public void onCreate() {
+ addHelper(BACKUP_KEY, new SharedPreferencesBackupHelper(this, RESERVED_SHARED_PREFERENCES));
+ }
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ writeFromPreferenceFilesToBackupFile();
+ super.onBackup(oldState, data, newState);
+ clearBackupFile();
+ }
+
+ @VisibleForTesting
+ void writeFromPreferenceFilesToBackupFile() {
+ Map<String, BackupKeyPredicate> fileBackupKeyPredicates = getBackupSpecification();
+ Editor backupEditor = getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit();
+ backupEditor.clear();
+ for (Map.Entry<String, BackupKeyPredicate> entry : fileBackupKeyPredicates.entrySet()) {
+ writeToBackupFile(entry.getKey(), backupEditor, entry.getValue());
+ }
+ backupEditor.apply();
+ }
+
+ /**
+ * Returns the predicate that decides which keys should be backed up for each shared preference
+ * file name. There must be no files with the same name as {@link #RESERVED_SHARED_PREFERENCES}.
+ * Assumes all shared preference file names are valid.
+ *
+ * <p>This method will only be called at backup time. At restore time, everything that was backed
+ * up is restored.
+ *
+ * @see BackupKeyPredicates
+ */
+ protected abstract Map<String, BackupKeyPredicate> getBackupSpecification();
+
+ /**
+ * Adds data from the given file name for keys that pass the given predicate.
+ * {@link Editor#apply()} is not called.
+ */
+ private void writeToBackupFile(
+ String srcFileName, Editor editor, BackupKeyPredicate backupKeyPredicate) {
+ if (srcFileName.equals(RESERVED_SHARED_PREFERENCES)) {
+ throw new IllegalStateException("Backup file name \"" + RESERVED_SHARED_PREFERENCES + "\" is "
+ + "reserved by PersistentBackupAgentHelper and cannot be used.");
+ }
+ if (srcFileName.contains(BACKUP_DELIMITER)) {
+ throw new IllegalStateException("Backup file name \"" + srcFileName + "\" cannot contain "
+ + "delimiter \"" + BACKUP_DELIMITER + "\".");
+ }
+ SharedPreferences srcSharedPreferences = getSharedPreferences(srcFileName, MODE_PRIVATE);
+ Map<String, ?> srcMap = srcSharedPreferences.getAll();
+ for (Map.Entry<String, ?> entry : srcMap.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ if (backupKeyPredicate.shouldBeBackedUp(key)) {
+ putSharedPreference(editor, buildBackupKey(srcFileName, key), value);
+ }
+ }
+ }
+
+ private static String buildBackupKey(String fileName, String key) {
+ return fileName + BACKUP_DELIMITER + key;
+ }
+
+ /**
+ * Puts the given value into the given editor for the given key. {@link Editor#apply()} is not
+ * called.
+ */
+ @SuppressWarnings("unchecked") // There are no unchecked casts - the Set<String> cast IS checked.
+ public static void putSharedPreference(Editor editor, String key, Object value) {
+ if (value instanceof Boolean) {
+ editor.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Float) {
+ editor.putFloat(key, (Float) value);
+ } else if (value instanceof Integer) {
+ editor.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ editor.putLong(key, (Long) value);
+ } else if (value instanceof String) {
+ editor.putString(key, (String) value);
+ } else if (value instanceof Set) {
+ for (Object object : (Set) value) {
+ if (!(object instanceof String)) {
+ // If a new type of shared preference set is added in the future, it can't be correctly
+ // restored on this version.
+ Log.w(TAG, "Skipping restore of key " + key + " because its value is a set containing"
+ + " an object of type " + (value == null ? null : value.getClass()) + ".");
+ return;
+ }
+ }
+ editor.putStringSet(key, (Set<String>) value);
+ } else {
+ // If a new type of shared preference is added in the future, it can't be correctly restored
+ // on this version.
+ Log.w(TAG, "Skipping restore of key " + key + " because its value is the unrecognized type "
+ + (value == null ? null : value.getClass()) + ".");
+ return;
+ }
+ }
+
+ private void clearBackupFile() {
+ // We don't currently delete the file because of a lack of a supported way to do it and because
+ // of the concerns of synchronously doing so.
+ getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE).edit().clear().apply();
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor stateFile)
+ throws IOException {
+ super.onRestore(data, appVersionCode, stateFile);
+ writeFromBackupFileToPreferenceFiles(appVersionCode);
+ clearBackupFile();
+ }
+
+ @VisibleForTesting
+ void writeFromBackupFileToPreferenceFiles(int appVersionCode) {
+ SharedPreferences backupSharedPreferences =
+ getSharedPreferences(RESERVED_SHARED_PREFERENCES, MODE_PRIVATE);
+ Map<String, Editor> editors = new HashMap<>();
+ for (Map.Entry<String, ?> entry : backupSharedPreferences.getAll().entrySet()) {
+ // We restore all files and keys, including those that this version doesn't know about or
+ // wouldn't have backed up. This ensures forward-compatibility.
+ String backupKey = entry.getKey();
+ Object value = entry.getValue();
+ int backupDelimiterIndex = backupKey.indexOf(BACKUP_DELIMITER);
+ if (backupDelimiterIndex < 1 || backupDelimiterIndex >= backupKey.length() - 1) {
+ Log.w(TAG, "Format of key \"" + backupKey + "\" not understood, so skipping its restore.");
+ continue;
+ }
+ String fileName = backupKey.substring(0, backupDelimiterIndex);
+ String preferenceKey = backupKey.substring(backupDelimiterIndex + 1);
+ Editor editor = editors.get(fileName);
+ if (editor == null) {
+ // #apply is called once for each editor later.
+ editor = getSharedPreferences(fileName, MODE_PRIVATE).edit();
+ editors.put(fileName, editor);
+ }
+ putSharedPreference(editor, preferenceKey, value);
+ }
+ for (Editor editor : editors.values()) {
+ editor.apply();
+ }
+ onPreferencesRestored(editors.keySet(), appVersionCode);
+ }
+
+ /**
+ * This method is called when the preferences have been restored. It can be overridden to apply
+ * processing to the restored preferences. However, this is not recommended to be used in
+ * conjunction with restoreAnyVersion unless the following problems are considered:
+ *
+ * <p>1) Once the processing is live, it could be applied to any data that ever gets backed up by
+ * the app, not just the types of data that were available when the processing was originally
+ * added.
+ *
+ * <p>2) Older versions of the app (that use restoreAnyVersion) will restore data without applying
+ * the processing. For first-party apps pre-installed on the device, this could be the case for
+ * every new user.
+ *
+ * @param names The list of files restored.
+ * @param appVersionCode The app version code from {@link #onRestore}.
+ */
+ @SuppressWarnings({"unused"})
+ protected void onPreferencesRestored(Set<String> names, int appVersionCode) {}
+}
diff --git a/src/com/google/android/libraries/backup/PreferenceBackupUtil.java b/src/com/google/android/libraries/backup/PreferenceBackupUtil.java
new file mode 100644
index 0000000..6cc4012
--- /dev/null
+++ b/src/com/google/android/libraries/backup/PreferenceBackupUtil.java
@@ -0,0 +1,99 @@
+package com.google.android.libraries.backup;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * Utility class of static methods to help process shared preferences for backup and restore.
+ */
+public class PreferenceBackupUtil {
+
+ @VisibleForTesting
+ @Nullable
+ static String getRingtoneTitleFromUri(Context context, @Nullable String uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ Ringtone sound = RingtoneManager.getRingtone(context, Uri.parse(uri));
+ if (sound == null) {
+ return null;
+ }
+ return sound.getTitle(context);
+ }
+
+ /**
+ * Get ringtone uri from a preference key in a shared preferences file, retrieve the associated
+ * ringtone's title and, if possible, save the title to the target preference key.
+ *
+ * @param srcRingtoneUriPrefKey preference key of the ringtone uri.
+ * @param dstRingtoneTitlePrefKey preference key where the ringtone title should be put.
+ * @return whether the ringtoneTitleKey was set.
+ */
+ public static boolean encodeRingtonePreference(Context context, String prefsName,
+ String srcRingtoneUriPrefKey, String dstRingtoneTitlePrefKey) {
+ SharedPreferences preferences = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
+
+ String uri = preferences.getString(srcRingtoneUriPrefKey, null);
+ String title = getRingtoneTitleFromUri(context, uri);
+ if (title == null) {
+ return false;
+ }
+
+ preferences.edit().putString(dstRingtoneTitlePrefKey, title).apply();
+ return true;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static String getRingtoneUriFromTitle(Context context, @Nullable String title, int ringtoneType) {
+ // Check whether the ringtoneType is a valid combination of the 3 ringtone types.
+ if ((ringtoneType == 0)
+ || ((RingtoneManager.TYPE_ALL & ringtoneType) != ringtoneType)) {
+ throw new IllegalStateException();
+ }
+ if (title == null) {
+ return null;
+ }
+
+ RingtoneManager manager = new RingtoneManager(context);
+ manager.setType(ringtoneType);
+ Cursor cur = manager.getCursor();
+ for (int i = 0; i < cur.getCount(); i++) {
+ Ringtone ringtone = manager.getRingtone(i);
+ if (ringtone.getTitle(context).equals(title)) {
+ return manager.getRingtoneUri(i).toString();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get ringtone title from a preference key of a shared preferences file, find a ringtone with the
+ * same title and, if possible, save its uri to the target preference key.
+ *
+ * @param dstRingtoneUriPrefKey preference key where the ringtone uri should be put.
+ * @param srcRingtoneTitlePrefKey preference key of the ringtone title.
+ * @return whether the ringtoneUriKey was set.
+ */
+ public static boolean decodeRingtonePreference(Context context, String prefsName,
+ String dstRingtoneUriPrefKey, String srcRingtoneTitlePrefKey, int ringtoneType) {
+ SharedPreferences preferences = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
+
+ String title = preferences.getString(srcRingtoneTitlePrefKey, null);
+ String uri = getRingtoneUriFromTitle(context, title, ringtoneType);
+ if (uri == null) {
+ return false;
+ }
+
+ preferences.edit().putString(dstRingtoneUriPrefKey, uri).apply();
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/com/google/android/libraries/backup/shadow/BackupAgentHelperShadow.java b/src/com/google/android/libraries/backup/shadow/BackupAgentHelperShadow.java
new file mode 100644
index 0000000..18deabf
--- /dev/null
+++ b/src/com/google/android/libraries/backup/shadow/BackupAgentHelperShadow.java
@@ -0,0 +1,181 @@
+package com.google.android.libraries.backup.shadow;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.BackupHelper;
+import android.app.backup.FileBackupHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.fakes.RoboSharedPreferences;
+
+/**
+ * Shadow class for end-to-end testing of {@link BackupAgentHelper} subclasses in unit tests.
+ *
+ * <p>This class currently supports <b>key-value backups only</b>. In other words, it does
+ * <b>not</b> support Dolly. In addition, the testing framework has the following two limitations
+ * with regards to backup/restore of {@link SharedPreferences}:
+ *
+ * <ol>
+ * <li>Preferences are normally backed by xml files in the app's shared_prefs directory, but
+ * Robolectric replaces them with {@link RoboSharedPreferences}, which are backed by an in-memory
+ * {@link Map}. Therefore, modifying the relevant xml files will have no effect on the preferences
+ * (and vice versa).
+ * <li>For the same reason, the testing framework cannot easily determine whether the underlying
+ * xml file for given shared preferences would have been empty or missing upon backup. The latter
+ * is assumed to ensure that apps don't rely on restore to implicitly clear data (potentially
+ * PII).
+ * </ol>
+ */
+@Implements(BackupAgentHelper.class)
+public class BackupAgentHelperShadow {
+ private static final String TAG = "BackupAgentHelperShadow";
+
+ /**
+ * Temporarily stores the backup data generated in {@link #onBackup} so that it could be returned
+ * by {@link #simulateBackup}.
+ */
+ private static final AtomicReference<Map<String, Object>> backupDataMapToBackup =
+ new AtomicReference<>();
+
+ /**
+ * Temporarily stores the backed up data passed to {@link #simulateRestore} so that it could be
+ * used in {@link #onRestore}.
+ */
+ private static final AtomicReference<Map<String, Object>> backupDataMapToRestore =
+ new AtomicReference<>();
+
+ /**
+ * Simulates key-value backup for the provided agent all the way from {@link
+ * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive).
+ */
+ public static Map<String, Object> simulateBackup(BackupAgentHelper agent) {
+ Map<String, Object> backupDataMap;
+ attachBaseContextToAgentIfNecessary(agent);
+ agent.onCreate();
+ try {
+ agent.onBackup(null, null, null);
+ backupDataMap = backupDataMapToBackup.getAndSet(null);
+ } catch (IOException e) {
+ backupDataMapToBackup.set(null);
+ throw new IllegalStateException(e);
+ }
+ agent.onDestroy();
+ return backupDataMap;
+ }
+
+ /**
+ * Simulates key-value restore for the provided agent all the way from {@link
+ * BackupAgentHelper#onCreate} to {@link BackupAgentHelper#onDestroy} (both inclusive).
+ *
+ * <p>Note: To make end-to-end tests more realistic, <b>different {@link BackupAgentHelper}
+ * instances</b> should be used in {@link #simulateBackup} and {@link #simulateRestore}.
+ */
+ public static void simulateRestore(
+ BackupAgentHelper agent, Map<String, Object> backupDataMap, int appVersionCode) {
+ attachBaseContextToAgentIfNecessary(agent);
+ agent.onCreate();
+ assertTrue(backupDataMapToRestore.compareAndSet(null, backupDataMap));
+ try {
+ agent.onRestore(null, appVersionCode, null);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ } finally {
+ backupDataMapToRestore.set(null);
+ }
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ agent.onRestoreFinished();
+ }
+ agent.onDestroy();
+ }
+
+ private static void attachBaseContextToAgentIfNecessary(BackupAgentHelper agent) {
+ if (agent.getBaseContext() != null) {
+ return;
+ }
+ try {
+ // {@link BackupAgent#attach} is a hidden method, so we need to call it via reflection.
+ Method method = BackupAgent.class.getMethod("attach", Context.class);
+ method.invoke(agent, RuntimeEnvironment.application);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private final Map<String, BackupHelperSimulator> helperSimulators;
+
+ public BackupAgentHelperShadow() {
+ // Use a {@link TreeMap} to mirror the internal implementation of {@link BackupHelperDispatcher}
+ // as closely as possible.
+ helperSimulators = new TreeMap<>();
+ }
+
+ @RealObject private BackupAgentHelper realHelper;
+
+ @Implementation
+ public void addHelper(String keyPrefix, BackupHelper helper) {
+ Class<? extends BackupHelper> helperClass = helper.getClass();
+ final BackupHelperSimulator simulator;
+ if (helperClass == SharedPreferencesBackupHelper.class) {
+ simulator = SharedPreferencesBackupHelperSimulator.fromHelper(
+ keyPrefix, (SharedPreferencesBackupHelper) helper);
+ } else if (helperClass == FileBackupHelper.class) {
+ simulator = FileBackupHelperSimulator.fromHelper(keyPrefix, (FileBackupHelper) helper);
+ } else {
+ throw new UnsupportedOperationException(
+ "Unknown backup helper class for key prefix \"" + keyPrefix + "\": " + helperClass);
+ }
+ helperSimulators.put(keyPrefix, simulator);
+ }
+
+ @Implementation
+ public void onBackup(
+ ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)
+ throws IOException {
+ ImmutableMap.Builder<String, Object> backupDataMapBuilder = ImmutableMap.builder();
+ for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) {
+ String keyPrefix = simulatorEntry.getKey();
+ BackupHelperSimulator simulator = simulatorEntry.getValue();
+ backupDataMapBuilder.put(keyPrefix, simulator.backup(realHelper));
+ }
+
+ assertTrue(backupDataMapToBackup.compareAndSet(null, backupDataMapBuilder.build()));
+ }
+
+ @Implementation
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ Map<String, Object> backupDataMap = backupDataMapToRestore.getAndSet(null);
+ assertNotNull(backupDataMap);
+
+ for (Map.Entry<String, BackupHelperSimulator> simulatorEntry : helperSimulators.entrySet()) {
+ String keyPrefix = simulatorEntry.getKey();
+ Object dataToRestore = backupDataMap.get(keyPrefix);
+ if (dataToRestore == null) {
+ Log.w(TAG, "No data to restore for key prefix: \"" + keyPrefix + "\".");
+ continue;
+ }
+ BackupHelperSimulator simulator = simulatorEntry.getValue();
+ simulator.restore(realHelper, dataToRestore);
+ }
+ }
+}
diff --git a/src/com/google/android/libraries/backup/shadow/BackupHelperSimulator.java b/src/com/google/android/libraries/backup/shadow/BackupHelperSimulator.java
new file mode 100644
index 0000000..01236f6
--- /dev/null
+++ b/src/com/google/android/libraries/backup/shadow/BackupHelperSimulator.java
@@ -0,0 +1,28 @@
+package com.google.android.libraries.backup.shadow;
+
+import android.app.backup.BackupHelper;
+import android.app.backup.FileBackupHelper;
+import android.content.Context;
+import com.google.common.base.Preconditions;
+
+/**
+ * Class which simulates backup & restore functionality of a {@link BackupHelper}.
+ */
+public abstract class BackupHelperSimulator {
+
+ /** Prefix key of the corresponding {@link FileBackupHelper}. */
+ protected final String keyPrefix;
+
+ public BackupHelperSimulator(String keyPrefix) {
+ this.keyPrefix = Preconditions.checkNotNull(keyPrefix);
+ }
+
+ /** Perform backup into an {@link Object}, which is then returned by the method. */
+ public abstract Object backup(Context context);
+
+ /**
+ * Perform restore from the provided {@link Object}, which must have the same type as the one
+ * returned by {@link #backup}.
+ */
+ public abstract void restore(Context context, Object data);
+}
diff --git a/src/com/google/android/libraries/backup/shadow/FileBackupHelperSimulator.java b/src/com/google/android/libraries/backup/shadow/FileBackupHelperSimulator.java
new file mode 100644
index 0000000..77ed6a3
--- /dev/null
+++ b/src/com/google/android/libraries/backup/shadow/FileBackupHelperSimulator.java
@@ -0,0 +1,157 @@
+package com.google.android.libraries.backup.shadow;
+
+import android.app.backup.FileBackupHelper;
+import android.content.Context;
+import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Representation of {@link FileBackupHelper} configuration used for testing. This class simulates
+ * backing up and restoring files by storing their contents in memory.
+ *
+ * <p>{@see BackupAgentHelperShadow}
+ */
+public class FileBackupHelperSimulator extends BackupHelperSimulator {
+ private static final String TAG = "FileBackupHelperSimulat";
+
+ /** Filenames which should be backed up/restored. */
+ private final Set<String> fileNames;
+
+ private FileBackupHelperSimulator(String keyPrefix, Set<String> fileNames) {
+ super(keyPrefix);
+ this.fileNames = Preconditions.checkNotNull(fileNames);
+ }
+
+ public static FileBackupHelperSimulator fromFileNames(String keyPrefix, Set<String> fileNames) {
+ return new FileBackupHelperSimulator(keyPrefix, fileNames);
+ }
+
+ public static FileBackupHelperSimulator fromHelper(String keyPrefix, FileBackupHelper helper) {
+ return new FileBackupHelperSimulator(keyPrefix, extractFileNamesFromHelper(helper));
+ }
+
+ @VisibleForTesting
+ static Set<String> extractFileNamesFromHelper(FileBackupHelper helper) {
+ try {
+ Field filesField = FileBackupHelper.class.getDeclaredField("mFiles");
+ filesField.setAccessible(true);
+ return ImmutableSet.copyOf((String[]) filesField.get(helper));
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /** Collection of backed up files. */
+ public static class FilesBackupData {
+ /** Map from file names to their backed up contents. */
+ private final Map<String, FileBackupContents> files;
+
+ public FilesBackupData(Map<String, FileBackupContents> files) {
+ this.files = Preconditions.checkNotNull(files);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof FilesBackupData && files.equals(((FilesBackupData) obj).files);
+ }
+
+ @Override
+ public int hashCode() {
+ return files.hashCode();
+ }
+
+ public Map<String, FileBackupContents> getFiles() {
+ return files;
+ }
+ }
+
+ /** Single backed up file. */
+ public static class FileBackupContents {
+ private final byte[] bytes;
+
+ public FileBackupContents(byte[] bytes) {
+ this.bytes = Preconditions.checkNotNull(bytes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof FileBackupContents
+ && Arrays.equals(bytes, ((FileBackupContents) obj).bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+
+ public static FileBackupContents readFromFile(File file) {
+ return new FileBackupContents(readBytesFromFile(file));
+ }
+
+ public void writeToFile(File file) {
+ writeBytesToFile(file, bytes);
+ }
+
+ public static byte[] readBytesFromFile(File file) {
+ try {
+ return Files.toByteArray(file);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static void writeBytesToFile(File file, byte[] bytes) {
+ try {
+ Files.write(bytes, file);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ @Override
+ public Object backup(Context context) {
+ File base = context.getFilesDir();
+ ImmutableMap.Builder<String, FileBackupContents> dataToBackupBuilder = ImmutableMap.builder();
+ for (String fileName : fileNames) {
+ File file = new File(base, fileName);
+ if (!file.exists()) {
+ Log.w(TAG, "File \"" + fileName + "\" not found by helper \"" + keyPrefix + "\".");
+ continue;
+ }
+ dataToBackupBuilder.put(fileName, FileBackupContents.readFromFile(file));
+ }
+ return new FilesBackupData(dataToBackupBuilder.build());
+ }
+
+ @Override
+ public void restore(Context context, Object data) {
+ if (!(data instanceof FilesBackupData)) {
+ throw new IllegalArgumentException("Invalid type of files to restore in helper \""
+ + keyPrefix + "\": " + data.getClass());
+ }
+
+ File base = context.getFilesDir();
+ Map<String, FileBackupContents> dataToRestore = ((FilesBackupData) data).getFiles();
+ for (Map.Entry<String, FileBackupContents> restoreEntry : dataToRestore.entrySet()) {
+ String fileName = restoreEntry.getKey();
+ if (!fileNames.contains(fileName)) {
+ Log.w(TAG, "File \"" + fileName + "\" ignored by helper \"" + keyPrefix + "\".");
+ continue;
+ }
+ FileBackupContents contents = restoreEntry.getValue();
+ File file = new File(base, fileName);
+ contents.writeToFile(file);
+ }
+ }
+}
diff --git a/src/com/google/android/libraries/backup/shadow/SharedPreferencesBackupHelperSimulator.java b/src/com/google/android/libraries/backup/shadow/SharedPreferencesBackupHelperSimulator.java
new file mode 100644
index 0000000..407f3f0
--- /dev/null
+++ b/src/com/google/android/libraries/backup/shadow/SharedPreferencesBackupHelperSimulator.java
@@ -0,0 +1,147 @@
+package com.google.android.libraries.backup.shadow;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.Context;
+import android.content.SharedPreferences.Editor;
+import android.util.Log;
+import com.google.android.libraries.backup.PersistentBackupAgentHelper;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Representation of {@link SharedPreferencesBackupHelper} configuration used for testing. This
+ * class simulates backing up and restoring shared preferences by storing them in memory.
+ *
+ * <p>{@see BackupAgentHelperShadow}
+ */
+public class SharedPreferencesBackupHelperSimulator extends BackupHelperSimulator {
+ private static final String TAG = "SharedPreferencesBackup";
+
+ /** Shared preferences file names which should be backed up/restored. */
+ private final Set<String> prefGroups;
+
+ private SharedPreferencesBackupHelperSimulator(String keyPrefix, Set<String> prefGroups) {
+ super(keyPrefix);
+ this.prefGroups = Preconditions.checkNotNull(prefGroups);
+ }
+
+ public static SharedPreferencesBackupHelperSimulator fromPreferenceGroups(
+ String keyPrefix, Set<String> prefGroups) {
+ return new SharedPreferencesBackupHelperSimulator(keyPrefix, prefGroups);
+ }
+
+ public static SharedPreferencesBackupHelperSimulator fromHelper(
+ String keyPrefix, SharedPreferencesBackupHelper helper) {
+ return new SharedPreferencesBackupHelperSimulator(
+ keyPrefix, extractPreferenceGroupsFromHelper(helper));
+ }
+
+ @VisibleForTesting
+ static Set<String> extractPreferenceGroupsFromHelper(SharedPreferencesBackupHelper helper) {
+ try {
+ Field prefGroupsField = SharedPreferencesBackupHelper.class.getDeclaredField("mPrefGroups");
+ prefGroupsField.setAccessible(true);
+ return ImmutableSet.copyOf((String[]) prefGroupsField.get(helper));
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(
+ "Failed to construct SharedPreferencesBackupHelperSimulator", e);
+ }
+ }
+
+ /** Collection of backed up shared preferences. */
+ public static class SharedPreferencesBackupData {
+ /** Map from shared preferences file names to key-value preference maps. */
+ private final Map<String, Map<String, ?>> preferences;
+
+ public SharedPreferencesBackupData(Map<String, Map<String, ?>> data) {
+ this.preferences = Preconditions.checkNotNull(data);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof SharedPreferencesBackupData
+ && preferences.equals(((SharedPreferencesBackupData) obj).preferences);
+ }
+
+ @Override
+ public int hashCode() {
+ return preferences.hashCode();
+ }
+
+ public Map<String, Map<String, ?>> getPreferences() {
+ return preferences;
+ }
+ }
+
+ @Override
+ public Object backup(Context context) {
+ ImmutableMap.Builder<String, Map<String, ?>> dataToBackupBuilder = ImmutableMap.builder();
+ for (String prefGroup : prefGroups) {
+ Map<String, ?> prefs = context.getSharedPreferences(prefGroup, MODE_PRIVATE).getAll();
+ if (prefs.isEmpty()) {
+ Log.w(TAG, "Shared prefs \"" + prefGroup + "\" are empty. The helper \"" + keyPrefix
+ + "\" assumes this is due to a missing (rather than empty) shared preferences file.");
+ continue;
+ }
+ ImmutableMap.Builder<String, Object> prefsData = ImmutableMap.builder();
+ for (Map.Entry<String, ?> prefEntry : prefs.entrySet()) {
+ String key = prefEntry.getKey();
+ Object value = prefEntry.getValue();
+ if (value instanceof Set) {
+ value = ImmutableSet.copyOf((Set<?>) value);
+ }
+ prefsData.put(key, value);
+ }
+ dataToBackupBuilder.put(prefGroup, prefsData.build());
+ }
+ return new SharedPreferencesBackupData(dataToBackupBuilder.build());
+ }
+
+ @Override
+ public void restore(Context context, Object data) {
+ if (!(data instanceof SharedPreferencesBackupData)) {
+ throw new IllegalArgumentException("Invalid type of files to restore in helper \""
+ + keyPrefix + "\": " + data.getClass());
+ }
+
+ Map<String, Map<String, ?>> prefsToRestore =
+ ((SharedPreferencesBackupData) data).getPreferences();
+
+ // Display a warning when missing/empty preferences are restored onto non-empty preferences.
+ for (String prefGroup : prefGroups) {
+ if (context.getSharedPreferences(prefGroup, MODE_PRIVATE).getAll().isEmpty()) {
+ continue;
+ }
+ Map<String, ?> prefsData = prefsToRestore.get(prefGroup);
+ if (prefsData == null) {
+ Log.w(TAG, "Non-empty shared prefs \"" + prefGroup + "\" will NOT be cleared by helper \""
+ + keyPrefix + "\" because the corresponding file is missing in the restored data.");
+ } else if (prefsData.isEmpty()) {
+ Log.w(TAG, "Non-empty shared prefs \"" + prefGroup + "\" will be cleared by helper \""
+ + keyPrefix + "\" because the corresponding file is empty in the restored data.");
+ }
+ }
+
+ for (Map.Entry<String, Map<String, ?>> restoreEntry : prefsToRestore.entrySet()) {
+ String prefGroup = restoreEntry.getKey();
+ if (!prefGroups.contains(prefGroup)) {
+ Log.w(TAG, "Shared prefs \"" + prefGroup + "\" ignored by helper \"" + keyPrefix + "\".");
+ continue;
+ }
+ Map<String, ?> prefsData = restoreEntry.getValue();
+ Editor editor = context.getSharedPreferences(prefGroup, MODE_PRIVATE).edit().clear();
+ for (Map.Entry<String, ?> prefEntry : prefsData.entrySet()) {
+ PersistentBackupAgentHelper.putSharedPreference(
+ editor, prefEntry.getKey(), prefEntry.getValue());
+ }
+ editor.apply();
+ }
+ }
+}