1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/BrowserLocaleManager.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,285 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import android.content.BroadcastReceiver; 1.12 +import android.content.Context; 1.13 +import android.content.Intent; 1.14 +import android.content.IntentFilter; 1.15 +import android.content.SharedPreferences; 1.16 +import android.content.res.Configuration; 1.17 +import android.content.res.Resources; 1.18 +import android.util.Log; 1.19 + 1.20 +import java.util.Locale; 1.21 +import java.util.concurrent.atomic.AtomicBoolean; 1.22 +import java.util.concurrent.atomic.AtomicReference; 1.23 + 1.24 +/** 1.25 + * This class manages persistence, application, and otherwise handling of 1.26 + * user-specified locales. 1.27 + * 1.28 + * Of note: 1.29 + * 1.30 + * * It's a singleton, because its scope extends to that of the application, 1.31 + * and definitionally all changes to the locale of the app must go through 1.32 + * this. 1.33 + * * It's lazy. 1.34 + * * It has ties into the Gecko event system, because it has to tell Gecko when 1.35 + * to switch locale. 1.36 + * * It relies on using the SharedPreferences file owned by the browser (in 1.37 + * Fennec's case, "GeckoApp") for performance. 1.38 + */ 1.39 +public class BrowserLocaleManager implements LocaleManager { 1.40 + private static final String LOG_TAG = "GeckoLocales"; 1.41 + 1.42 + private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; 1.43 + private static final String PREF_LOCALE = "locale"; 1.44 + 1.45 + // This is volatile because we don't impose restrictions 1.46 + // over which thread calls our methods. 1.47 + private volatile Locale currentLocale = null; 1.48 + 1.49 + private AtomicBoolean inited = new AtomicBoolean(false); 1.50 + private boolean systemLocaleDidChange = false; 1.51 + private BroadcastReceiver receiver; 1.52 + 1.53 + private static AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>(); 1.54 + 1.55 + public static LocaleManager getInstance() { 1.56 + LocaleManager localeManager = instance.get(); 1.57 + if (localeManager != null) { 1.58 + return localeManager; 1.59 + } 1.60 + 1.61 + localeManager = new BrowserLocaleManager(); 1.62 + if (instance.compareAndSet(null, localeManager)) { 1.63 + return localeManager; 1.64 + } else { 1.65 + return instance.get(); 1.66 + } 1.67 + } 1.68 + 1.69 + /** 1.70 + * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} 1.71 + * stringifies as "es_ES". 1.72 + * 1.73 + * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>. 1.74 + * 1.75 + * @return a locale string suitable for passing to Gecko. 1.76 + */ 1.77 + public static String getLanguageTag(final Locale locale) { 1.78 + // If this were Java 7: 1.79 + // return locale.toLanguageTag(); 1.80 + 1.81 + String language = locale.getLanguage(); // Can, but should never be, an empty string. 1.82 + // Modernize certain language codes. 1.83 + if (language.equals("iw")) { 1.84 + language = "he"; 1.85 + } else if (language.equals("in")) { 1.86 + language = "id"; 1.87 + } else if (language.equals("ji")) { 1.88 + language = "yi"; 1.89 + } 1.90 + 1.91 + String country = locale.getCountry(); // Can be an empty string. 1.92 + if (country.equals("")) { 1.93 + return language; 1.94 + } 1.95 + return language + "-" + country; 1.96 + } 1.97 + 1.98 + private static Locale parseLocaleCode(final String localeCode) { 1.99 + int index; 1.100 + if ((index = localeCode.indexOf('-')) != -1 || 1.101 + (index = localeCode.indexOf('_')) != -1) { 1.102 + final String langCode = localeCode.substring(0, index); 1.103 + final String countryCode = localeCode.substring(index + 1); 1.104 + return new Locale(langCode, countryCode); 1.105 + } else { 1.106 + return new Locale(localeCode); 1.107 + } 1.108 + } 1.109 + 1.110 + /** 1.111 + * Ensure that you call this early in your application startup, 1.112 + * and with a context that's sufficiently long-lived (typically 1.113 + * the application context). 1.114 + * 1.115 + * Calling multiple times is harmless. 1.116 + */ 1.117 + @Override 1.118 + public void initialize(final Context context) { 1.119 + if (!inited.compareAndSet(false, true)) { 1.120 + return; 1.121 + } 1.122 + 1.123 + receiver = new BroadcastReceiver() { 1.124 + @Override 1.125 + public void onReceive(Context context, Intent intent) { 1.126 + systemLocaleDidChange = true; 1.127 + } 1.128 + }; 1.129 + context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); 1.130 + } 1.131 + 1.132 + @Override 1.133 + public boolean systemLocaleDidChange() { 1.134 + return systemLocaleDidChange; 1.135 + } 1.136 + 1.137 + /** 1.138 + * Every time the system gives us a new configuration, it 1.139 + * carries the external locale. Fix it. 1.140 + */ 1.141 + @Override 1.142 + public void correctLocale(Context context, Resources res, Configuration config) { 1.143 + final Locale current = getCurrentLocale(context); 1.144 + if (current == null) { 1.145 + return; 1.146 + } 1.147 + 1.148 + // I know it's tempting to short-circuit here if the config seems to be 1.149 + // up-to-date, but the rest is necessary. 1.150 + 1.151 + config.locale = current; 1.152 + 1.153 + // The following two lines are heavily commented in case someone 1.154 + // decides to chase down performance improvements and decides to 1.155 + // question what's going on here. 1.156 + // Both lines should be cheap, *but*... 1.157 + 1.158 + // This is unnecessary for basic string choice, but it almost 1.159 + // certainly comes into play when rendering numbers, deciding on RTL, 1.160 + // etc. Take it out if you can prove that's not the case. 1.161 + Locale.setDefault(current); 1.162 + 1.163 + // This seems to be a no-op, but every piece of documentation under the 1.164 + // sun suggests that it's necessary, and it certainly makes sense. 1.165 + res.updateConfiguration(config, res.getDisplayMetrics()); 1.166 + } 1.167 + 1.168 + @Override 1.169 + public String getAndApplyPersistedLocale(Context context) { 1.170 + initialize(context); 1.171 + 1.172 + final long t1 = android.os.SystemClock.uptimeMillis(); 1.173 + final String localeCode = getPersistedLocale(context); 1.174 + if (localeCode == null) { 1.175 + return null; 1.176 + } 1.177 + 1.178 + // Note that we don't tell Gecko about this. We notify Gecko when the 1.179 + // locale is set, not when we update Java. 1.180 + final String resultant = updateLocale(context, localeCode); 1.181 + 1.182 + if (resultant == null) { 1.183 + // Update the configuration anyway. 1.184 + updateConfiguration(context, currentLocale); 1.185 + } 1.186 + 1.187 + final long t2 = android.os.SystemClock.uptimeMillis(); 1.188 + Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms."); 1.189 + return resultant; 1.190 + } 1.191 + 1.192 + /** 1.193 + * Returns the set locale if it changed. 1.194 + * 1.195 + * Always persists and notifies Gecko. 1.196 + */ 1.197 + @Override 1.198 + public String setSelectedLocale(Context context, String localeCode) { 1.199 + final String resultant = updateLocale(context, localeCode); 1.200 + 1.201 + // We always persist and notify Gecko, even if nothing seemed to 1.202 + // change. This might happen if you're picking a locale that's the same 1.203 + // as the current OS locale. The OS locale might change next time we 1.204 + // launch, and we need the Gecko pref and persisted locale to have been 1.205 + // set by the time that happens. 1.206 + persistLocale(context, localeCode); 1.207 + 1.208 + // Tell Gecko. 1.209 + GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context))); 1.210 + GeckoAppShell.sendEventToGecko(ev); 1.211 + 1.212 + return resultant; 1.213 + } 1.214 + 1.215 + /** 1.216 + * This is public to allow for an activity to force the 1.217 + * current locale to be applied if necessary (e.g., when 1.218 + * a new activity launches). 1.219 + */ 1.220 + @Override 1.221 + public void updateConfiguration(Context context, Locale locale) { 1.222 + Resources res = context.getResources(); 1.223 + Configuration config = res.getConfiguration(); 1.224 + config.locale = locale; 1.225 + res.updateConfiguration(config, res.getDisplayMetrics()); 1.226 + } 1.227 + 1.228 + private SharedPreferences getSharedPreferences(Context context) { 1.229 + return GeckoSharedPrefs.forApp(context); 1.230 + } 1.231 + 1.232 + private String getPersistedLocale(Context context) { 1.233 + final SharedPreferences settings = getSharedPreferences(context); 1.234 + final String locale = settings.getString(PREF_LOCALE, ""); 1.235 + 1.236 + if ("".equals(locale)) { 1.237 + return null; 1.238 + } 1.239 + return locale; 1.240 + } 1.241 + 1.242 + private void persistLocale(Context context, String localeCode) { 1.243 + final SharedPreferences settings = getSharedPreferences(context); 1.244 + settings.edit().putString(PREF_LOCALE, localeCode).commit(); 1.245 + } 1.246 + 1.247 + private Locale getCurrentLocale(Context context) { 1.248 + if (currentLocale != null) { 1.249 + return currentLocale; 1.250 + } 1.251 + 1.252 + final String current = getPersistedLocale(context); 1.253 + if (current == null) { 1.254 + return null; 1.255 + } 1.256 + return currentLocale = parseLocaleCode(current); 1.257 + } 1.258 + 1.259 + /** 1.260 + * Updates the Java locale and the Android configuration. 1.261 + * 1.262 + * Returns the persisted locale if it differed. 1.263 + * 1.264 + * Does not notify Gecko. 1.265 + */ 1.266 + private String updateLocale(Context context, String localeCode) { 1.267 + // Fast path. 1.268 + final Locale defaultLocale = Locale.getDefault(); 1.269 + if (defaultLocale.toString().equals(localeCode)) { 1.270 + return null; 1.271 + } 1.272 + 1.273 + final Locale locale = parseLocaleCode(localeCode); 1.274 + 1.275 + // Fast path. 1.276 + if (defaultLocale.equals(locale)) { 1.277 + return null; 1.278 + } 1.279 + 1.280 + Locale.setDefault(locale); 1.281 + currentLocale = locale; 1.282 + 1.283 + // Update resources. 1.284 + updateConfiguration(context, locale); 1.285 + 1.286 + return locale.toString(); 1.287 + } 1.288 +}