1 /*
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.preferences;
19 
20 import com.google.common.annotations.VisibleForTesting;
21 import com.google.common.collect.Lists;
22 
23 import android.app.backup.BackupManager;
24 import android.content.Context;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.Editor;
27 
28 import com.android.mail.MailIntentService;
29 import com.android.mail.utils.LogTag;
30 import com.android.mail.utils.LogUtils;
31 
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 
36 /**
37  * A high-level API to store and retrieve preferences, that can be versioned in a similar manner as
38  * SQLite databases. You must not use the preference key
39  * {@value VersionedPrefs#PREFS_VERSION_NUMBER}
40  */
41 public abstract class VersionedPrefs {
42     private final Context mContext;
43     private final String mSharedPreferencesName;
44     private final SharedPreferences mSharedPreferences;
45     private final Editor mEditor;
46 
47     /** The key for the version number of the {@link SharedPreferences} file. */
48     private static final String PREFS_VERSION_NUMBER = "prefs-version-number";
49 
50     /**
51      * The current version number for {@link SharedPreferences}. This is a constant for all
52      * applications based on UnifiedEmail.
53      */
54     protected static final int CURRENT_VERSION_NUMBER = 4;
55 
56     protected static final String LOG_TAG = LogTag.getLogTag();
57 
58     /**
59      * @param sharedPrefsName The name of the {@link SharedPreferences} file to use
60      */
VersionedPrefs(final Context context, final String sharedPrefsName)61     protected VersionedPrefs(final Context context, final String sharedPrefsName) {
62         mContext = context.getApplicationContext();
63         mSharedPreferencesName = sharedPrefsName;
64         mSharedPreferences = context.getSharedPreferences(sharedPrefsName, Context.MODE_PRIVATE);
65         mEditor = mSharedPreferences.edit();
66 
67         final int oldVersion = getCurrentVersion();
68 
69         performUpgrade(oldVersion, CURRENT_VERSION_NUMBER);
70         setCurrentVersion(CURRENT_VERSION_NUMBER);
71 
72         if (!hasMigrationCompleted()) {
73             final BasePreferenceMigrator preferenceMigrator =
74                     PreferenceMigratorHolder.createPreferenceMigrator();
75             final boolean migrationComplete;
76             if (preferenceMigrator != null) {
77                 migrationComplete = preferenceMigrator
78                         .performMigration(context, oldVersion, CURRENT_VERSION_NUMBER);
79             } else {
80                 LogUtils.w(LogUtils.TAG, "No preference migrator found, not migrating preferences");
81                 migrationComplete = false;
82             }
83 
84             if (migrationComplete) {
85                 setMigrationComplete();
86             }
87         }
88     }
89 
getContext()90     protected Context getContext() {
91         return mContext;
92     }
93 
getSharedPreferencesName()94     public String getSharedPreferencesName() {
95         return mSharedPreferencesName;
96     }
97 
getSharedPreferences()98     protected SharedPreferences getSharedPreferences() {
99         return mSharedPreferences;
100     }
101 
getEditor()102     protected Editor getEditor() {
103         return mEditor;
104     }
105 
registerOnSharedPreferenceChangeListener( SharedPreferences.OnSharedPreferenceChangeListener listener)106     public void registerOnSharedPreferenceChangeListener(
107             SharedPreferences.OnSharedPreferenceChangeListener listener) {
108         mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
109     }
110 
unregisterOnSharedPreferenceChangeListener( SharedPreferences.OnSharedPreferenceChangeListener listener)111     public void unregisterOnSharedPreferenceChangeListener(
112             SharedPreferences.OnSharedPreferenceChangeListener listener) {
113         mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
114     }
115 
116     /**
117      * Returns the current version of the {@link SharedPreferences} file.
118      */
getCurrentVersion()119     private int getCurrentVersion() {
120         return mSharedPreferences.getInt(PREFS_VERSION_NUMBER, 0);
121     }
122 
setCurrentVersion(final int versionNumber)123     private void setCurrentVersion(final int versionNumber) {
124         getEditor().putInt(PREFS_VERSION_NUMBER, versionNumber);
125 
126         /*
127          * If the only preference we have is the version number, we do not want to commit it.
128          * Instead, we will wait for some other preference to be written. This prevents us from
129          * creating a file with only the version number.
130          */
131         if (shouldBackUp()) {
132             getEditor().apply();
133         }
134     }
135 
hasMigrationCompleted()136     protected boolean hasMigrationCompleted() {
137         return MailPrefs.get(mContext).hasMigrationCompleted();
138     }
139 
setMigrationComplete()140     protected void setMigrationComplete() {
141         MailPrefs.get(mContext).setMigrationComplete();
142     }
143 
144     /**
145      * Commits all pending changes to the preferences.
146      */
commit()147     public void commit() {
148         getEditor().commit();
149     }
150 
151     /**
152      * Upgrades the {@link SharedPreferences} file.
153      *
154      * @param oldVersion The current version
155      * @param newVersion The new version
156      */
performUpgrade(int oldVersion, int newVersion)157     protected abstract void performUpgrade(int oldVersion, int newVersion);
158 
159     @VisibleForTesting
clearAllPreferences()160     public void clearAllPreferences() {
161         getEditor().clear().commit();
162     }
163 
canBackup(String key)164     protected abstract boolean canBackup(String key);
165 
166     /**
167      * Gets the value to backup for a given key-value pair. By default, returns the passed in value.
168      *
169      * @param key The key to backup
170      * @param value The locally stored value for the given key
171      * @return The value to backup
172      */
getBackupValue(final String key, final Object value)173     protected Object getBackupValue(final String key, final Object value) {
174         return value;
175     }
176 
177     /**
178      * Gets the value to restore for a given key-value pair. By default, returns the passed in
179      * value.
180      *
181      * @param key The key to restore
182      * @param value The backed up value for the given key
183      * @return The value to restore
184      */
getRestoreValue(final String key, final Object value)185     protected Object getRestoreValue(final String key, final Object value) {
186         return value;
187     }
188 
189     /**
190      * Return a list of shared preferences that should be backed up.
191      */
getBackupPreferences()192     public List<BackupSharedPreference> getBackupPreferences() {
193         final List<BackupSharedPreference> backupPreferences = Lists.newArrayList();
194         final SharedPreferences sharedPreferences = getSharedPreferences();
195         final Map<String, ?> preferences = sharedPreferences.getAll();
196 
197         for (final Map.Entry<String, ?> entry : preferences.entrySet()) {
198             final String key = entry.getKey();
199 
200             if (!canBackup(key)) {
201                 continue;
202             }
203 
204             final Object value = entry.getValue();
205             final Object backupValue = getBackupValue(key, value);
206 
207             if (backupValue != null) {
208                 backupPreferences.add(new SimpleBackupSharedPreference(key, backupValue));
209             }
210         }
211 
212         return backupPreferences;
213     }
214 
215     /**
216      * Restores preferences from a backup.
217      */
restorePreferences(final List<BackupSharedPreference> preferences)218     public void restorePreferences(final List<BackupSharedPreference> preferences) {
219         for (final BackupSharedPreference preference : preferences) {
220             final String key = preference.getKey();
221             final Object value = preference.getValue();
222 
223             if (!canBackup(key) || value == null) {
224                 continue;
225             }
226 
227             final Object restoreValue = getRestoreValue(key, value);
228 
229             if (restoreValue instanceof Boolean) {
230                 getEditor().putBoolean(key, (Boolean) restoreValue);
231                 LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
232             } else if (restoreValue instanceof Float) {
233                 getEditor().putFloat(key, (Float) restoreValue);
234                 LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
235             } else if (restoreValue instanceof Integer) {
236                 getEditor().putInt(key, (Integer) restoreValue);
237                 LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
238             } else if (restoreValue instanceof Long) {
239                 getEditor().putLong(key, (Long) restoreValue);
240                 LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
241             } else if (restoreValue instanceof String) {
242                 getEditor().putString(key, (String) restoreValue);
243                 LogUtils.v(LOG_TAG, "MailPrefs Restore: %s", preference);
244             } else if (restoreValue instanceof Set) {
245                 getEditor().putStringSet(key, (Set<String>) restoreValue);
246             } else {
247                 LogUtils.e(LOG_TAG, "Unknown MailPrefs preference data type: %s", value.getClass());
248             }
249         }
250 
251         getEditor().apply();
252     }
253 
254     /**
255      * <p>
256      * Checks if any of the preferences eligible for backup have been modified from their default
257      * values, and therefore should be backed up.
258      * </p>
259      *
260      * @return <code>true</code> if anything has been modified, <code>false</code> otherwise
261      */
shouldBackUp()262     public boolean shouldBackUp() {
263         final Map<String, ?> allPrefs = getSharedPreferences().getAll();
264 
265         for (final String key : allPrefs.keySet()) {
266             if (canBackup(key)) {
267                 return true;
268             }
269         }
270 
271         return false;
272     }
273 
274     /**
275      * Notifies {@link BackupManager} that we have new data to back up.
276      */
notifyBackupPreferenceChanged()277     protected void notifyBackupPreferenceChanged() {
278         MailIntentService.broadcastBackupDataChanged(getContext());
279     }
280 }
281