mobile/android/base/GeckoSharedPrefs.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko;
michael@0 6
michael@0 7 import org.mozilla.gecko.mozglue.RobocopTarget;
michael@0 8 import org.mozilla.gecko.util.ThreadUtils;
michael@0 9
michael@0 10 import android.content.Context;
michael@0 11 import android.content.SharedPreferences;
michael@0 12 import android.content.SharedPreferences.Editor;
michael@0 13 import android.os.Build;
michael@0 14 import android.os.StrictMode;
michael@0 15 import android.preference.PreferenceManager;
michael@0 16 import android.util.Log;
michael@0 17
michael@0 18 import java.util.Arrays;
michael@0 19 import java.util.EnumSet;
michael@0 20 import java.util.List;
michael@0 21 import java.util.Map;
michael@0 22 import java.util.Set;
michael@0 23
michael@0 24 /**
michael@0 25 * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
michael@0 26 * You should use this API instead of using Context.getSharedPreferences()
michael@0 27 * directly. There are three methods to get scoped SharedPreferences instances:
michael@0 28 *
michael@0 29 * forApp()
michael@0 30 * Use it for app-wide, cross-profile pref keys.
michael@0 31 * forProfile()
michael@0 32 * Use it to fetch and store keys for the current profile.
michael@0 33 * forProfileName()
michael@0 34 * Use it to fetch and store keys from/for a specific profile.
michael@0 35 *
michael@0 36 * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
michael@0 37 * migrate keys from one scope to another. You can trigger a new migration by
michael@0 38 * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
michael@0 39 *
michael@0 40 * Migration history:
michael@0 41 * 1: Move all PreferenceManager keys to app/profile scopes
michael@0 42 */
michael@0 43 @RobocopTarget
michael@0 44 public final class GeckoSharedPrefs {
michael@0 45 private static final String LOGTAG = "GeckoSharedPrefs";
michael@0 46
michael@0 47 // Increment it to trigger a new migration
michael@0 48 public static final int PREFS_VERSION = 1;
michael@0 49
michael@0 50 // Name for app-scoped prefs
michael@0 51 public static final String APP_PREFS_NAME = "GeckoApp";
michael@0 52
michael@0 53 // The prefs key that holds the current migration
michael@0 54 private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
michael@0 55
michael@0 56 // For disabling migration when getting a SharedPreferences instance
michael@0 57 private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
michael@0 58
michael@0 59 // Timeout for migration commits to be done (10 seconds)
michael@0 60 private static final int MIGRATION_COMMIT_TIMEOUT_MSEC = 10000;
michael@0 61
michael@0 62 // The keys that have to be moved from ProfileManager's default
michael@0 63 // shared prefs to the profile from version 0 to 1.
michael@0 64 private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
michael@0 65 "home_panels",
michael@0 66 "home_locale"
michael@0 67 };
michael@0 68
michael@0 69 // For optimizing the migration check in subsequent get() calls
michael@0 70 private static volatile boolean migrationDone = false;
michael@0 71
michael@0 72 public enum Flags {
michael@0 73 DISABLE_MIGRATIONS
michael@0 74 }
michael@0 75
michael@0 76 // Used when fetching profile-scoped prefs.
michael@0 77 private static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
michael@0 78
michael@0 79 public static SharedPreferences forApp(Context context) {
michael@0 80 return forApp(context, EnumSet.noneOf(Flags.class));
michael@0 81 }
michael@0 82
michael@0 83 /**
michael@0 84 * Returns an app-scoped SharedPreferences instance. You can disable
michael@0 85 * migrations by using the DISABLE_MIGRATIONS flag.
michael@0 86 */
michael@0 87 public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
michael@0 88 if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
michael@0 89 migrateIfNecessary(context);
michael@0 90 }
michael@0 91
michael@0 92 return context.getSharedPreferences(APP_PREFS_NAME, 0);
michael@0 93 }
michael@0 94
michael@0 95 public static SharedPreferences forProfile(Context context) {
michael@0 96 return forProfile(context, EnumSet.noneOf(Flags.class));
michael@0 97 }
michael@0 98
michael@0 99 /**
michael@0 100 * Returns a SharedPreferences instance scoped to the current profile
michael@0 101 * in the app. You can disable migrations by using the DISABLE_MIGRATIONS
michael@0 102 * flag.
michael@0 103 */
michael@0 104 public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
michael@0 105 String profileName = GeckoProfile.get(context).getName();
michael@0 106 if (profileName == null) {
michael@0 107 throw new IllegalStateException("Could not get current profile name");
michael@0 108 }
michael@0 109
michael@0 110 return forProfileName(context, profileName, flags);
michael@0 111 }
michael@0 112
michael@0 113 public static SharedPreferences forProfileName(Context context, String profileName) {
michael@0 114 return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
michael@0 115 }
michael@0 116
michael@0 117 /**
michael@0 118 * Returns an SharedPreferences instance scoped to the given profile name.
michael@0 119 * You can disable migrations by using the DISABLE_MIGRATION flag.
michael@0 120 */
michael@0 121 public static SharedPreferences forProfileName(Context context, String profileName,
michael@0 122 EnumSet<Flags> flags) {
michael@0 123 if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
michael@0 124 migrateIfNecessary(context);
michael@0 125 }
michael@0 126
michael@0 127 final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
michael@0 128 return context.getSharedPreferences(prefsName, 0);
michael@0 129 }
michael@0 130
michael@0 131 /**
michael@0 132 * Returns the current version of the prefs.
michael@0 133 */
michael@0 134 public static int getVersion(Context context) {
michael@0 135 return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
michael@0 136 }
michael@0 137
michael@0 138 /**
michael@0 139 * Resets migration flag. Should only be used in tests.
michael@0 140 */
michael@0 141 public static synchronized void reset() {
michael@0 142 migrationDone = false;
michael@0 143 }
michael@0 144
michael@0 145 /**
michael@0 146 * Performs all prefs migrations in the background thread to avoid StrictMode
michael@0 147 * exceptions from reading/writing in the UI thread. This method will block
michael@0 148 * the current thread until the migration is finished.
michael@0 149 */
michael@0 150 private static synchronized void migrateIfNecessary(final Context context) {
michael@0 151 if (migrationDone) {
michael@0 152 return;
michael@0 153 }
michael@0 154
michael@0 155 // We deliberatly perform the migration in the current thread (which
michael@0 156 // is likely the UI thread) as this is actually cheaper than enforcing a
michael@0 157 // context switch to another thread (see bug 940575).
michael@0 158 if (Build.VERSION.SDK_INT < 9) {
michael@0 159 performMigration(context);
michael@0 160 } else {
michael@0 161 // Avoid strict mode warnings.
michael@0 162 final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
michael@0 163 StrictMode.allowThreadDiskWrites();
michael@0 164
michael@0 165 try {
michael@0 166 performMigration(context);
michael@0 167 } finally {
michael@0 168 StrictMode.setThreadPolicy(savedPolicy);
michael@0 169 }
michael@0 170 }
michael@0 171
michael@0 172 migrationDone = true;
michael@0 173 }
michael@0 174
michael@0 175 private static void performMigration(Context context) {
michael@0 176 final SharedPreferences appPrefs = forApp(context, disableMigrations);
michael@0 177
michael@0 178 final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
michael@0 179 Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
michael@0 180
michael@0 181 if (currentVersion == PREFS_VERSION) {
michael@0 182 return;
michael@0 183 }
michael@0 184
michael@0 185 Log.d(LOGTAG, "Performing migration");
michael@0 186
michael@0 187 final Editor appEditor = appPrefs.edit();
michael@0 188
michael@0 189 // The migration always moves prefs to the default profile, not
michael@0 190 // the current one. We might have to revisit this if we ever support
michael@0 191 // multiple profiles.
michael@0 192 final String defaultProfileName;
michael@0 193 try {
michael@0 194 defaultProfileName = GeckoProfile.getDefaultProfileName(context);
michael@0 195 } catch (Exception e) {
michael@0 196 throw new IllegalStateException("Failed to get default profile name for migration");
michael@0 197 }
michael@0 198
michael@0 199 final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
michael@0 200
michael@0 201 List<String> profileKeys;
michael@0 202 Editor pmEditor = null;
michael@0 203
michael@0 204 for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
michael@0 205 Log.d(LOGTAG, "Migrating to version = " + v);
michael@0 206
michael@0 207 switch (v) {
michael@0 208 case 1:
michael@0 209 profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
michael@0 210 pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
michael@0 211 break;
michael@0 212 }
michael@0 213 }
michael@0 214
michael@0 215 // Update prefs version accordingly.
michael@0 216 appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
michael@0 217
michael@0 218 appEditor.commit();
michael@0 219 profileEditor.commit();
michael@0 220 if (pmEditor != null) {
michael@0 221 pmEditor.commit();
michael@0 222 }
michael@0 223
michael@0 224 Log.d(LOGTAG, "All keys have been migrated");
michael@0 225 }
michael@0 226
michael@0 227 /**
michael@0 228 * Moves all preferences stored in PreferenceManager's default prefs
michael@0 229 * to either app or profile scopes. The profile-scoped keys are defined
michael@0 230 * in given profileKeys list, all other keys are moved to the app scope.
michael@0 231 */
michael@0 232 public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
michael@0 233 Editor profileEditor, List<String> profileKeys) {
michael@0 234 Log.d(LOGTAG, "Migrating from PreferenceManager");
michael@0 235
michael@0 236 final SharedPreferences pmPrefs =
michael@0 237 PreferenceManager.getDefaultSharedPreferences(context);
michael@0 238
michael@0 239 for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
michael@0 240 final String key = entry.getKey();
michael@0 241
michael@0 242 final Editor to;
michael@0 243 if (profileKeys.contains(key)) {
michael@0 244 to = profileEditor;
michael@0 245 } else {
michael@0 246 to = appEditor;
michael@0 247 }
michael@0 248
michael@0 249 putEntry(to, key, entry.getValue());
michael@0 250 }
michael@0 251
michael@0 252 // Clear PreferenceManager's prefs once we're done
michael@0 253 // and return the Editor to be committed.
michael@0 254 return pmPrefs.edit().clear();
michael@0 255 }
michael@0 256
michael@0 257 @SuppressWarnings("unchecked")
michael@0 258 private static void putEntry(Editor to, String key, Object value) {
michael@0 259 Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
michael@0 260
michael@0 261 if (value instanceof String) {
michael@0 262 to.putString(key, (String) value);
michael@0 263 } else if (value instanceof Boolean) {
michael@0 264 to.putBoolean(key, (Boolean) value);
michael@0 265 } else if (value instanceof Long) {
michael@0 266 to.putLong(key, (Long) value);
michael@0 267 } else if (value instanceof Float) {
michael@0 268 to.putFloat(key, (Float) value);
michael@0 269 } else if (value instanceof Integer) {
michael@0 270 to.putInt(key, (Integer) value);
michael@0 271 } else {
michael@0 272 throw new IllegalStateException("Unrecognized value type for key: " + key);
michael@0 273 }
michael@0 274 }
michael@0 275 }

mercurial