Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | } |