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