|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
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; |
|
16 |
|
17 import java.util.Locale; |
|
18 import java.util.concurrent.atomic.AtomicBoolean; |
|
19 import java.util.concurrent.atomic.AtomicReference; |
|
20 |
|
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"; |
|
38 |
|
39 private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; |
|
40 private static final String PREF_LOCALE = "locale"; |
|
41 |
|
42 // This is volatile because we don't impose restrictions |
|
43 // over which thread calls our methods. |
|
44 private volatile Locale currentLocale = null; |
|
45 |
|
46 private AtomicBoolean inited = new AtomicBoolean(false); |
|
47 private boolean systemLocaleDidChange = false; |
|
48 private BroadcastReceiver receiver; |
|
49 |
|
50 private static AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>(); |
|
51 |
|
52 public static LocaleManager getInstance() { |
|
53 LocaleManager localeManager = instance.get(); |
|
54 if (localeManager != null) { |
|
55 return localeManager; |
|
56 } |
|
57 |
|
58 localeManager = new BrowserLocaleManager(); |
|
59 if (instance.compareAndSet(null, localeManager)) { |
|
60 return localeManager; |
|
61 } else { |
|
62 return instance.get(); |
|
63 } |
|
64 } |
|
65 |
|
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(); |
|
77 |
|
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 } |
|
87 |
|
88 String country = locale.getCountry(); // Can be an empty string. |
|
89 if (country.equals("")) { |
|
90 return language; |
|
91 } |
|
92 return language + "-" + country; |
|
93 } |
|
94 |
|
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 } |
|
106 |
|
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 } |
|
119 |
|
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 } |
|
128 |
|
129 @Override |
|
130 public boolean systemLocaleDidChange() { |
|
131 return systemLocaleDidChange; |
|
132 } |
|
133 |
|
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 } |
|
144 |
|
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. |
|
147 |
|
148 config.locale = current; |
|
149 |
|
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*... |
|
154 |
|
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); |
|
159 |
|
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 } |
|
164 |
|
165 @Override |
|
166 public String getAndApplyPersistedLocale(Context context) { |
|
167 initialize(context); |
|
168 |
|
169 final long t1 = android.os.SystemClock.uptimeMillis(); |
|
170 final String localeCode = getPersistedLocale(context); |
|
171 if (localeCode == null) { |
|
172 return null; |
|
173 } |
|
174 |
|
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); |
|
178 |
|
179 if (resultant == null) { |
|
180 // Update the configuration anyway. |
|
181 updateConfiguration(context, currentLocale); |
|
182 } |
|
183 |
|
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 } |
|
188 |
|
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); |
|
197 |
|
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); |
|
204 |
|
205 // Tell Gecko. |
|
206 GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context))); |
|
207 GeckoAppShell.sendEventToGecko(ev); |
|
208 |
|
209 return resultant; |
|
210 } |
|
211 |
|
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 } |
|
224 |
|
225 private SharedPreferences getSharedPreferences(Context context) { |
|
226 return GeckoSharedPrefs.forApp(context); |
|
227 } |
|
228 |
|
229 private String getPersistedLocale(Context context) { |
|
230 final SharedPreferences settings = getSharedPreferences(context); |
|
231 final String locale = settings.getString(PREF_LOCALE, ""); |
|
232 |
|
233 if ("".equals(locale)) { |
|
234 return null; |
|
235 } |
|
236 return locale; |
|
237 } |
|
238 |
|
239 private void persistLocale(Context context, String localeCode) { |
|
240 final SharedPreferences settings = getSharedPreferences(context); |
|
241 settings.edit().putString(PREF_LOCALE, localeCode).commit(); |
|
242 } |
|
243 |
|
244 private Locale getCurrentLocale(Context context) { |
|
245 if (currentLocale != null) { |
|
246 return currentLocale; |
|
247 } |
|
248 |
|
249 final String current = getPersistedLocale(context); |
|
250 if (current == null) { |
|
251 return null; |
|
252 } |
|
253 return currentLocale = parseLocaleCode(current); |
|
254 } |
|
255 |
|
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 } |
|
269 |
|
270 final Locale locale = parseLocaleCode(localeCode); |
|
271 |
|
272 // Fast path. |
|
273 if (defaultLocale.equals(locale)) { |
|
274 return null; |
|
275 } |
|
276 |
|
277 Locale.setDefault(locale); |
|
278 currentLocale = locale; |
|
279 |
|
280 // Update resources. |
|
281 updateConfiguration(context, locale); |
|
282 |
|
283 return locale.toString(); |
|
284 } |
|
285 } |