1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/GeckoSharedPrefs.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,275 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko; 1.9 + 1.10 +import org.mozilla.gecko.mozglue.RobocopTarget; 1.11 +import org.mozilla.gecko.util.ThreadUtils; 1.12 + 1.13 +import android.content.Context; 1.14 +import android.content.SharedPreferences; 1.15 +import android.content.SharedPreferences.Editor; 1.16 +import android.os.Build; 1.17 +import android.os.StrictMode; 1.18 +import android.preference.PreferenceManager; 1.19 +import android.util.Log; 1.20 + 1.21 +import java.util.Arrays; 1.22 +import java.util.EnumSet; 1.23 +import java.util.List; 1.24 +import java.util.Map; 1.25 +import java.util.Set; 1.26 + 1.27 +/** 1.28 + * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances. 1.29 + * You should use this API instead of using Context.getSharedPreferences() 1.30 + * directly. There are three methods to get scoped SharedPreferences instances: 1.31 + * 1.32 + * forApp() 1.33 + * Use it for app-wide, cross-profile pref keys. 1.34 + * forProfile() 1.35 + * Use it to fetch and store keys for the current profile. 1.36 + * forProfileName() 1.37 + * Use it to fetch and store keys from/for a specific profile. 1.38 + * 1.39 + * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to 1.40 + * migrate keys from one scope to another. You can trigger a new migration by 1.41 + * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly. 1.42 + * 1.43 + * Migration history: 1.44 + * 1: Move all PreferenceManager keys to app/profile scopes 1.45 + */ 1.46 +@RobocopTarget 1.47 +public final class GeckoSharedPrefs { 1.48 + private static final String LOGTAG = "GeckoSharedPrefs"; 1.49 + 1.50 + // Increment it to trigger a new migration 1.51 + public static final int PREFS_VERSION = 1; 1.52 + 1.53 + // Name for app-scoped prefs 1.54 + public static final String APP_PREFS_NAME = "GeckoApp"; 1.55 + 1.56 + // The prefs key that holds the current migration 1.57 + private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration"; 1.58 + 1.59 + // For disabling migration when getting a SharedPreferences instance 1.60 + private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS); 1.61 + 1.62 + // Timeout for migration commits to be done (10 seconds) 1.63 + private static final int MIGRATION_COMMIT_TIMEOUT_MSEC = 10000; 1.64 + 1.65 + // The keys that have to be moved from ProfileManager's default 1.66 + // shared prefs to the profile from version 0 to 1. 1.67 + private static final String[] PROFILE_MIGRATIONS_0_TO_1 = { 1.68 + "home_panels", 1.69 + "home_locale" 1.70 + }; 1.71 + 1.72 + // For optimizing the migration check in subsequent get() calls 1.73 + private static volatile boolean migrationDone = false; 1.74 + 1.75 + public enum Flags { 1.76 + DISABLE_MIGRATIONS 1.77 + } 1.78 + 1.79 + // Used when fetching profile-scoped prefs. 1.80 + private static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-"; 1.81 + 1.82 + public static SharedPreferences forApp(Context context) { 1.83 + return forApp(context, EnumSet.noneOf(Flags.class)); 1.84 + } 1.85 + 1.86 + /** 1.87 + * Returns an app-scoped SharedPreferences instance. You can disable 1.88 + * migrations by using the DISABLE_MIGRATIONS flag. 1.89 + */ 1.90 + public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) { 1.91 + if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) { 1.92 + migrateIfNecessary(context); 1.93 + } 1.94 + 1.95 + return context.getSharedPreferences(APP_PREFS_NAME, 0); 1.96 + } 1.97 + 1.98 + public static SharedPreferences forProfile(Context context) { 1.99 + return forProfile(context, EnumSet.noneOf(Flags.class)); 1.100 + } 1.101 + 1.102 + /** 1.103 + * Returns a SharedPreferences instance scoped to the current profile 1.104 + * in the app. You can disable migrations by using the DISABLE_MIGRATIONS 1.105 + * flag. 1.106 + */ 1.107 + public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) { 1.108 + String profileName = GeckoProfile.get(context).getName(); 1.109 + if (profileName == null) { 1.110 + throw new IllegalStateException("Could not get current profile name"); 1.111 + } 1.112 + 1.113 + return forProfileName(context, profileName, flags); 1.114 + } 1.115 + 1.116 + public static SharedPreferences forProfileName(Context context, String profileName) { 1.117 + return forProfileName(context, profileName, EnumSet.noneOf(Flags.class)); 1.118 + } 1.119 + 1.120 + /** 1.121 + * Returns an SharedPreferences instance scoped to the given profile name. 1.122 + * You can disable migrations by using the DISABLE_MIGRATION flag. 1.123 + */ 1.124 + public static SharedPreferences forProfileName(Context context, String profileName, 1.125 + EnumSet<Flags> flags) { 1.126 + if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) { 1.127 + migrateIfNecessary(context); 1.128 + } 1.129 + 1.130 + final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName; 1.131 + return context.getSharedPreferences(prefsName, 0); 1.132 + } 1.133 + 1.134 + /** 1.135 + * Returns the current version of the prefs. 1.136 + */ 1.137 + public static int getVersion(Context context) { 1.138 + return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0); 1.139 + } 1.140 + 1.141 + /** 1.142 + * Resets migration flag. Should only be used in tests. 1.143 + */ 1.144 + public static synchronized void reset() { 1.145 + migrationDone = false; 1.146 + } 1.147 + 1.148 + /** 1.149 + * Performs all prefs migrations in the background thread to avoid StrictMode 1.150 + * exceptions from reading/writing in the UI thread. This method will block 1.151 + * the current thread until the migration is finished. 1.152 + */ 1.153 + private static synchronized void migrateIfNecessary(final Context context) { 1.154 + if (migrationDone) { 1.155 + return; 1.156 + } 1.157 + 1.158 + // We deliberatly perform the migration in the current thread (which 1.159 + // is likely the UI thread) as this is actually cheaper than enforcing a 1.160 + // context switch to another thread (see bug 940575). 1.161 + if (Build.VERSION.SDK_INT < 9) { 1.162 + performMigration(context); 1.163 + } else { 1.164 + // Avoid strict mode warnings. 1.165 + final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); 1.166 + StrictMode.allowThreadDiskWrites(); 1.167 + 1.168 + try { 1.169 + performMigration(context); 1.170 + } finally { 1.171 + StrictMode.setThreadPolicy(savedPolicy); 1.172 + } 1.173 + } 1.174 + 1.175 + migrationDone = true; 1.176 + } 1.177 + 1.178 + private static void performMigration(Context context) { 1.179 + final SharedPreferences appPrefs = forApp(context, disableMigrations); 1.180 + 1.181 + final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0); 1.182 + Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION); 1.183 + 1.184 + if (currentVersion == PREFS_VERSION) { 1.185 + return; 1.186 + } 1.187 + 1.188 + Log.d(LOGTAG, "Performing migration"); 1.189 + 1.190 + final Editor appEditor = appPrefs.edit(); 1.191 + 1.192 + // The migration always moves prefs to the default profile, not 1.193 + // the current one. We might have to revisit this if we ever support 1.194 + // multiple profiles. 1.195 + final String defaultProfileName; 1.196 + try { 1.197 + defaultProfileName = GeckoProfile.getDefaultProfileName(context); 1.198 + } catch (Exception e) { 1.199 + throw new IllegalStateException("Failed to get default profile name for migration"); 1.200 + } 1.201 + 1.202 + final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit(); 1.203 + 1.204 + List<String> profileKeys; 1.205 + Editor pmEditor = null; 1.206 + 1.207 + for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) { 1.208 + Log.d(LOGTAG, "Migrating to version = " + v); 1.209 + 1.210 + switch (v) { 1.211 + case 1: 1.212 + profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1); 1.213 + pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys); 1.214 + break; 1.215 + } 1.216 + } 1.217 + 1.218 + // Update prefs version accordingly. 1.219 + appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION); 1.220 + 1.221 + appEditor.commit(); 1.222 + profileEditor.commit(); 1.223 + if (pmEditor != null) { 1.224 + pmEditor.commit(); 1.225 + } 1.226 + 1.227 + Log.d(LOGTAG, "All keys have been migrated"); 1.228 + } 1.229 + 1.230 + /** 1.231 + * Moves all preferences stored in PreferenceManager's default prefs 1.232 + * to either app or profile scopes. The profile-scoped keys are defined 1.233 + * in given profileKeys list, all other keys are moved to the app scope. 1.234 + */ 1.235 + public static Editor migrateFromPreferenceManager(Context context, Editor appEditor, 1.236 + Editor profileEditor, List<String> profileKeys) { 1.237 + Log.d(LOGTAG, "Migrating from PreferenceManager"); 1.238 + 1.239 + final SharedPreferences pmPrefs = 1.240 + PreferenceManager.getDefaultSharedPreferences(context); 1.241 + 1.242 + for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) { 1.243 + final String key = entry.getKey(); 1.244 + 1.245 + final Editor to; 1.246 + if (profileKeys.contains(key)) { 1.247 + to = profileEditor; 1.248 + } else { 1.249 + to = appEditor; 1.250 + } 1.251 + 1.252 + putEntry(to, key, entry.getValue()); 1.253 + } 1.254 + 1.255 + // Clear PreferenceManager's prefs once we're done 1.256 + // and return the Editor to be committed. 1.257 + return pmPrefs.edit().clear(); 1.258 + } 1.259 + 1.260 + @SuppressWarnings("unchecked") 1.261 + private static void putEntry(Editor to, String key, Object value) { 1.262 + Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value); 1.263 + 1.264 + if (value instanceof String) { 1.265 + to.putString(key, (String) value); 1.266 + } else if (value instanceof Boolean) { 1.267 + to.putBoolean(key, (Boolean) value); 1.268 + } else if (value instanceof Long) { 1.269 + to.putLong(key, (Long) value); 1.270 + } else if (value instanceof Float) { 1.271 + to.putFloat(key, (Float) value); 1.272 + } else if (value instanceof Integer) { 1.273 + to.putInt(key, (Integer) value); 1.274 + } else { 1.275 + throw new IllegalStateException("Unrecognized value type for key: " + key); 1.276 + } 1.277 + } 1.278 +} 1.279 \ No newline at end of file