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
18package com.android.mail.preferences;
19
20import com.google.common.annotations.VisibleForTesting;
21import com.google.common.collect.Lists;
22
23import android.app.backup.BackupManager;
24import android.content.Context;
25import android.content.SharedPreferences;
26import android.content.SharedPreferences.Editor;
27
28import com.android.mail.MailIntentService;
29import com.android.mail.utils.LogTag;
30import com.android.mail.utils.LogUtils;
31
32import java.util.List;
33import java.util.Map;
34import 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 */
41public 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     */
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
90    protected Context getContext() {
91        return mContext;
92    }
93
94    public String getSharedPreferencesName() {
95        return mSharedPreferencesName;
96    }
97
98    protected SharedPreferences getSharedPreferences() {
99        return mSharedPreferences;
100    }
101
102    protected Editor getEditor() {
103        return mEditor;
104    }
105
106    public void registerOnSharedPreferenceChangeListener(
107            SharedPreferences.OnSharedPreferenceChangeListener listener) {
108        mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
109    }
110
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     */
119    private int getCurrentVersion() {
120        return mSharedPreferences.getInt(PREFS_VERSION_NUMBER, 0);
121    }
122
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
136    protected boolean hasMigrationCompleted() {
137        return MailPrefs.get(mContext).hasMigrationCompleted();
138    }
139
140    protected void setMigrationComplete() {
141        MailPrefs.get(mContext).setMigrationComplete();
142    }
143
144    /**
145     * Commits all pending changes to the preferences.
146     */
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     */
157    protected abstract void performUpgrade(int oldVersion, int newVersion);
158
159    @VisibleForTesting
160    public void clearAllPreferences() {
161        getEditor().clear().commit();
162    }
163
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     */
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     */
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     */
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     */
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     */
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     */
277    protected void notifyBackupPreferenceChanged() {
278        MailIntentService.broadcastBackupDataChanged(getContext());
279    }
280}
281