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