diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java new file mode 100644 index 000000000..ec928dd86 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java @@ -0,0 +1,318 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import org.mozilla.gecko.annotation.RobocopTarget; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.StrictMode; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances. + * You should use this API instead of using Context.getSharedPreferences() + * directly. There are four methods to get scoped SharedPreferences instances: + * + * forApp() + * Use it for app-wide, cross-profile pref keys. + * forCrashReporter() + * For the crash reporter, which runs in its own process. + * forProfile() + * Use it to fetch and store keys for the current profile. + * forProfileName() + * Use it to fetch and store keys from/for a specific profile. + * + * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to + * migrate keys from one scope to another. You can trigger a new migration by + * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly. + * + * Migration history: + * 1: Move all PreferenceManager keys to app/profile scopes + * 2: Move the crash reporter's private preferences into their own scope + */ +@RobocopTarget +public final class GeckoSharedPrefs { + private static final String LOGTAG = "GeckoSharedPrefs"; + + // Increment it to trigger a new migration + public static final int PREFS_VERSION = 2; + + // Name for app-scoped prefs + public static final String APP_PREFS_NAME = "GeckoApp"; + + // Name for crash reporter prefs + public static final String CRASH_PREFS_NAME = "CrashReporter"; + + // Used when fetching profile-scoped prefs. + public static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-"; + + // The prefs key that holds the current migration + private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration"; + + // For disabling migration when getting a SharedPreferences instance + private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS); + + // The keys that have to be moved from ProfileManager's default + // shared prefs to the profile from version 0 to 1. + private static final String[] PROFILE_MIGRATIONS_0_TO_1 = { + "home_panels", + "home_locale" + }; + + // The keys that have to be moved from the app prefs + // into the crash reporter's own prefs. + private static final String[] PROFILE_MIGRATIONS_1_TO_2 = { + "sendReport", + "includeUrl", + "allowContact", + "contactEmail" + }; + + // For optimizing the migration check in subsequent get() calls + private static volatile boolean migrationDone; + + public enum Flags { + DISABLE_MIGRATIONS + } + + public static SharedPreferences forApp(Context context) { + return forApp(context, EnumSet.noneOf(Flags.class)); + } + + /** + * Returns an app-scoped SharedPreferences instance. You can disable + * migrations by using the DISABLE_MIGRATIONS flag. + */ + public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) { + if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) { + migrateIfNecessary(context); + } + + return context.getSharedPreferences(APP_PREFS_NAME, 0); + } + + public static SharedPreferences forCrashReporter(Context context) { + return forCrashReporter(context, EnumSet.noneOf(Flags.class)); + } + + /** + * Returns a crash-reporter-scoped SharedPreferences instance. You can disable + * migrations by using the DISABLE_MIGRATIONS flag. + */ + public static SharedPreferences forCrashReporter(Context context, EnumSet<Flags> flags) { + if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) { + migrateIfNecessary(context); + } + + return context.getSharedPreferences(CRASH_PREFS_NAME, 0); + } + + public static SharedPreferences forProfile(Context context) { + return forProfile(context, EnumSet.noneOf(Flags.class)); + } + + /** + * Returns a SharedPreferences instance scoped to the current profile + * in the app. You can disable migrations by using the DISABLE_MIGRATIONS + * flag. + */ + public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) { + String profileName = GeckoProfile.get(context).getName(); + if (profileName == null) { + throw new IllegalStateException("Could not get current profile name"); + } + + return forProfileName(context, profileName, flags); + } + + public static SharedPreferences forProfileName(Context context, String profileName) { + return forProfileName(context, profileName, EnumSet.noneOf(Flags.class)); + } + + /** + * Returns an SharedPreferences instance scoped to the given profile name. + * You can disable migrations by using the DISABLE_MIGRATION flag. + */ + public static SharedPreferences forProfileName(Context context, String profileName, + EnumSet<Flags> flags) { + if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) { + migrateIfNecessary(context); + } + + final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName; + return context.getSharedPreferences(prefsName, 0); + } + + /** + * Returns the current version of the prefs. + */ + public static int getVersion(Context context) { + return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0); + } + + /** + * Resets migration flag. Should only be used in tests. + */ + public static synchronized void reset() { + migrationDone = false; + } + + /** + * Performs all prefs migrations in the background thread to avoid StrictMode + * exceptions from reading/writing in the UI thread. This method will block + * the current thread until the migration is finished. + */ + private static synchronized void migrateIfNecessary(final Context context) { + if (migrationDone) { + return; + } + + // We deliberately perform the migration in the current thread (which + // is likely the UI thread) as this is actually cheaper than enforcing a + // context switch to another thread (see bug 940575). + // Avoid strict mode warnings when doing so. + final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); + StrictMode.allowThreadDiskWrites(); + try { + performMigration(context); + } finally { + StrictMode.setThreadPolicy(savedPolicy); + } + + migrationDone = true; + } + + private static void performMigration(Context context) { + final SharedPreferences appPrefs = forApp(context, disableMigrations); + + final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0); + Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION); + + if (currentVersion == PREFS_VERSION) { + return; + } + + Log.d(LOGTAG, "Performing migration"); + + final Editor appEditor = appPrefs.edit(); + + // The migration always moves prefs to the default profile, not + // the current one. We might have to revisit this if we ever support + // multiple profiles. + final String defaultProfileName; + try { + defaultProfileName = GeckoProfile.getDefaultProfileName(context); + } catch (Exception e) { + throw new IllegalStateException("Failed to get default profile name for migration"); + } + + final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit(); + final Editor crashEditor = forCrashReporter(context, disableMigrations).edit(); + + List<String> profileKeys; + Editor pmEditor = null; + + for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) { + Log.d(LOGTAG, "Migrating to version = " + v); + + switch (v) { + case 1: + profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1); + pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys); + break; + case 2: + profileKeys = Arrays.asList(PROFILE_MIGRATIONS_1_TO_2); + migrateCrashReporterSettings(appPrefs, appEditor, crashEditor, profileKeys); + break; + } + } + + // Update prefs version accordingly. + appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION); + + appEditor.apply(); + profileEditor.apply(); + crashEditor.apply(); + if (pmEditor != null) { + pmEditor.apply(); + } + + Log.d(LOGTAG, "All keys have been migrated"); + } + + /** + * Moves all preferences stored in PreferenceManager's default prefs + * to either app or profile scopes. The profile-scoped keys are defined + * in given profileKeys list, all other keys are moved to the app scope. + */ + public static Editor migrateFromPreferenceManager(Context context, Editor appEditor, + Editor profileEditor, List<String> profileKeys) { + Log.d(LOGTAG, "Migrating from PreferenceManager"); + + final SharedPreferences pmPrefs = + PreferenceManager.getDefaultSharedPreferences(context); + + for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) { + final String key = entry.getKey(); + + final Editor to; + if (profileKeys.contains(key)) { + to = profileEditor; + } else { + to = appEditor; + } + + putEntry(to, key, entry.getValue()); + } + + // Clear PreferenceManager's prefs once we're done + // and return the Editor to be committed. + return pmPrefs.edit().clear(); + } + + /** + * Moves the crash reporter's preferences from the app-wide prefs + * into its own shared prefs to avoid cross-process pref accesses. + */ + public static void migrateCrashReporterSettings(SharedPreferences appPrefs, Editor appEditor, + Editor crashEditor, List<String> profileKeys) { + Log.d(LOGTAG, "Migrating crash reporter settings"); + + for (Map.Entry<String, ?> entry : appPrefs.getAll().entrySet()) { + final String key = entry.getKey(); + + if (profileKeys.contains(key)) { + putEntry(crashEditor, key, entry.getValue()); + appEditor.remove(key); + } + } + } + + private static void putEntry(Editor to, String key, Object value) { + Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value); + + if (value instanceof String) { + to.putString(key, (String) value); + } else if (value instanceof Boolean) { + to.putBoolean(key, (Boolean) value); + } else if (value instanceof Long) { + to.putLong(key, (Long) value); + } else if (value instanceof Float) { + to.putFloat(key, (Float) value); + } else if (value instanceof Integer) { + to.putInt(key, (Integer) value); + } else { + throw new IllegalStateException("Unrecognized value type for key: " + key); + } + } +}
\ No newline at end of file |