Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import android.content.BroadcastReceiver;
9 import android.content.Context;
10 import android.content.Intent;
11 import android.content.IntentFilter;
12 import android.content.SharedPreferences;
13 import android.content.res.Configuration;
14 import android.content.res.Resources;
15 import android.util.Log;
17 import java.util.Locale;
18 import java.util.concurrent.atomic.AtomicBoolean;
19 import java.util.concurrent.atomic.AtomicReference;
21 /**
22 * This class manages persistence, application, and otherwise handling of
23 * user-specified locales.
24 *
25 * Of note:
26 *
27 * * It's a singleton, because its scope extends to that of the application,
28 * and definitionally all changes to the locale of the app must go through
29 * this.
30 * * It's lazy.
31 * * It has ties into the Gecko event system, because it has to tell Gecko when
32 * to switch locale.
33 * * It relies on using the SharedPreferences file owned by the browser (in
34 * Fennec's case, "GeckoApp") for performance.
35 */
36 public class BrowserLocaleManager implements LocaleManager {
37 private static final String LOG_TAG = "GeckoLocales";
39 private static final String EVENT_LOCALE_CHANGED = "Locale:Changed";
40 private static final String PREF_LOCALE = "locale";
42 // This is volatile because we don't impose restrictions
43 // over which thread calls our methods.
44 private volatile Locale currentLocale = null;
46 private AtomicBoolean inited = new AtomicBoolean(false);
47 private boolean systemLocaleDidChange = false;
48 private BroadcastReceiver receiver;
50 private static AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>();
52 public static LocaleManager getInstance() {
53 LocaleManager localeManager = instance.get();
54 if (localeManager != null) {
55 return localeManager;
56 }
58 localeManager = new BrowserLocaleManager();
59 if (instance.compareAndSet(null, localeManager)) {
60 return localeManager;
61 } else {
62 return instance.get();
63 }
64 }
66 /**
67 * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale}
68 * stringifies as "es_ES".
69 *
70 * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>.
71 *
72 * @return a locale string suitable for passing to Gecko.
73 */
74 public static String getLanguageTag(final Locale locale) {
75 // If this were Java 7:
76 // return locale.toLanguageTag();
78 String language = locale.getLanguage(); // Can, but should never be, an empty string.
79 // Modernize certain language codes.
80 if (language.equals("iw")) {
81 language = "he";
82 } else if (language.equals("in")) {
83 language = "id";
84 } else if (language.equals("ji")) {
85 language = "yi";
86 }
88 String country = locale.getCountry(); // Can be an empty string.
89 if (country.equals("")) {
90 return language;
91 }
92 return language + "-" + country;
93 }
95 private static Locale parseLocaleCode(final String localeCode) {
96 int index;
97 if ((index = localeCode.indexOf('-')) != -1 ||
98 (index = localeCode.indexOf('_')) != -1) {
99 final String langCode = localeCode.substring(0, index);
100 final String countryCode = localeCode.substring(index + 1);
101 return new Locale(langCode, countryCode);
102 } else {
103 return new Locale(localeCode);
104 }
105 }
107 /**
108 * Ensure that you call this early in your application startup,
109 * and with a context that's sufficiently long-lived (typically
110 * the application context).
111 *
112 * Calling multiple times is harmless.
113 */
114 @Override
115 public void initialize(final Context context) {
116 if (!inited.compareAndSet(false, true)) {
117 return;
118 }
120 receiver = new BroadcastReceiver() {
121 @Override
122 public void onReceive(Context context, Intent intent) {
123 systemLocaleDidChange = true;
124 }
125 };
126 context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
127 }
129 @Override
130 public boolean systemLocaleDidChange() {
131 return systemLocaleDidChange;
132 }
134 /**
135 * Every time the system gives us a new configuration, it
136 * carries the external locale. Fix it.
137 */
138 @Override
139 public void correctLocale(Context context, Resources res, Configuration config) {
140 final Locale current = getCurrentLocale(context);
141 if (current == null) {
142 return;
143 }
145 // I know it's tempting to short-circuit here if the config seems to be
146 // up-to-date, but the rest is necessary.
148 config.locale = current;
150 // The following two lines are heavily commented in case someone
151 // decides to chase down performance improvements and decides to
152 // question what's going on here.
153 // Both lines should be cheap, *but*...
155 // This is unnecessary for basic string choice, but it almost
156 // certainly comes into play when rendering numbers, deciding on RTL,
157 // etc. Take it out if you can prove that's not the case.
158 Locale.setDefault(current);
160 // This seems to be a no-op, but every piece of documentation under the
161 // sun suggests that it's necessary, and it certainly makes sense.
162 res.updateConfiguration(config, res.getDisplayMetrics());
163 }
165 @Override
166 public String getAndApplyPersistedLocale(Context context) {
167 initialize(context);
169 final long t1 = android.os.SystemClock.uptimeMillis();
170 final String localeCode = getPersistedLocale(context);
171 if (localeCode == null) {
172 return null;
173 }
175 // Note that we don't tell Gecko about this. We notify Gecko when the
176 // locale is set, not when we update Java.
177 final String resultant = updateLocale(context, localeCode);
179 if (resultant == null) {
180 // Update the configuration anyway.
181 updateConfiguration(context, currentLocale);
182 }
184 final long t2 = android.os.SystemClock.uptimeMillis();
185 Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms.");
186 return resultant;
187 }
189 /**
190 * Returns the set locale if it changed.
191 *
192 * Always persists and notifies Gecko.
193 */
194 @Override
195 public String setSelectedLocale(Context context, String localeCode) {
196 final String resultant = updateLocale(context, localeCode);
198 // We always persist and notify Gecko, even if nothing seemed to
199 // change. This might happen if you're picking a locale that's the same
200 // as the current OS locale. The OS locale might change next time we
201 // launch, and we need the Gecko pref and persisted locale to have been
202 // set by the time that happens.
203 persistLocale(context, localeCode);
205 // Tell Gecko.
206 GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context)));
207 GeckoAppShell.sendEventToGecko(ev);
209 return resultant;
210 }
212 /**
213 * This is public to allow for an activity to force the
214 * current locale to be applied if necessary (e.g., when
215 * a new activity launches).
216 */
217 @Override
218 public void updateConfiguration(Context context, Locale locale) {
219 Resources res = context.getResources();
220 Configuration config = res.getConfiguration();
221 config.locale = locale;
222 res.updateConfiguration(config, res.getDisplayMetrics());
223 }
225 private SharedPreferences getSharedPreferences(Context context) {
226 return GeckoSharedPrefs.forApp(context);
227 }
229 private String getPersistedLocale(Context context) {
230 final SharedPreferences settings = getSharedPreferences(context);
231 final String locale = settings.getString(PREF_LOCALE, "");
233 if ("".equals(locale)) {
234 return null;
235 }
236 return locale;
237 }
239 private void persistLocale(Context context, String localeCode) {
240 final SharedPreferences settings = getSharedPreferences(context);
241 settings.edit().putString(PREF_LOCALE, localeCode).commit();
242 }
244 private Locale getCurrentLocale(Context context) {
245 if (currentLocale != null) {
246 return currentLocale;
247 }
249 final String current = getPersistedLocale(context);
250 if (current == null) {
251 return null;
252 }
253 return currentLocale = parseLocaleCode(current);
254 }
256 /**
257 * Updates the Java locale and the Android configuration.
258 *
259 * Returns the persisted locale if it differed.
260 *
261 * Does not notify Gecko.
262 */
263 private String updateLocale(Context context, String localeCode) {
264 // Fast path.
265 final Locale defaultLocale = Locale.getDefault();
266 if (defaultLocale.toString().equals(localeCode)) {
267 return null;
268 }
270 final Locale locale = parseLocaleCode(localeCode);
272 // Fast path.
273 if (defaultLocale.equals(locale)) {
274 return null;
275 }
277 Locale.setDefault(locale);
278 currentLocale = locale;
280 // Update resources.
281 updateConfiguration(context, locale);
283 return locale.toString();
284 }
285 }