michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import android.content.BroadcastReceiver; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.IntentFilter; michael@0: import android.content.SharedPreferences; michael@0: import android.content.res.Configuration; michael@0: import android.content.res.Resources; michael@0: import android.util.Log; michael@0: michael@0: import java.util.Locale; michael@0: import java.util.concurrent.atomic.AtomicBoolean; michael@0: import java.util.concurrent.atomic.AtomicReference; michael@0: michael@0: /** michael@0: * This class manages persistence, application, and otherwise handling of michael@0: * user-specified locales. michael@0: * michael@0: * Of note: michael@0: * michael@0: * * It's a singleton, because its scope extends to that of the application, michael@0: * and definitionally all changes to the locale of the app must go through michael@0: * this. michael@0: * * It's lazy. michael@0: * * It has ties into the Gecko event system, because it has to tell Gecko when michael@0: * to switch locale. michael@0: * * It relies on using the SharedPreferences file owned by the browser (in michael@0: * Fennec's case, "GeckoApp") for performance. michael@0: */ michael@0: public class BrowserLocaleManager implements LocaleManager { michael@0: private static final String LOG_TAG = "GeckoLocales"; michael@0: michael@0: private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; michael@0: private static final String PREF_LOCALE = "locale"; michael@0: michael@0: // This is volatile because we don't impose restrictions michael@0: // over which thread calls our methods. michael@0: private volatile Locale currentLocale = null; michael@0: michael@0: private AtomicBoolean inited = new AtomicBoolean(false); michael@0: private boolean systemLocaleDidChange = false; michael@0: private BroadcastReceiver receiver; michael@0: michael@0: private static AtomicReference instance = new AtomicReference(); michael@0: michael@0: public static LocaleManager getInstance() { michael@0: LocaleManager localeManager = instance.get(); michael@0: if (localeManager != null) { michael@0: return localeManager; michael@0: } michael@0: michael@0: localeManager = new BrowserLocaleManager(); michael@0: if (instance.compareAndSet(null, localeManager)) { michael@0: return localeManager; michael@0: } else { michael@0: return instance.get(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} michael@0: * stringifies as "es_ES". michael@0: * michael@0: * This method approximates the Java 7 method Locale#toLanguageTag(). michael@0: * michael@0: * @return a locale string suitable for passing to Gecko. michael@0: */ michael@0: public static String getLanguageTag(final Locale locale) { michael@0: // If this were Java 7: michael@0: // return locale.toLanguageTag(); michael@0: michael@0: String language = locale.getLanguage(); // Can, but should never be, an empty string. michael@0: // Modernize certain language codes. michael@0: if (language.equals("iw")) { michael@0: language = "he"; michael@0: } else if (language.equals("in")) { michael@0: language = "id"; michael@0: } else if (language.equals("ji")) { michael@0: language = "yi"; michael@0: } michael@0: michael@0: String country = locale.getCountry(); // Can be an empty string. michael@0: if (country.equals("")) { michael@0: return language; michael@0: } michael@0: return language + "-" + country; michael@0: } michael@0: michael@0: private static Locale parseLocaleCode(final String localeCode) { michael@0: int index; michael@0: if ((index = localeCode.indexOf('-')) != -1 || michael@0: (index = localeCode.indexOf('_')) != -1) { michael@0: final String langCode = localeCode.substring(0, index); michael@0: final String countryCode = localeCode.substring(index + 1); michael@0: return new Locale(langCode, countryCode); michael@0: } else { michael@0: return new Locale(localeCode); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Ensure that you call this early in your application startup, michael@0: * and with a context that's sufficiently long-lived (typically michael@0: * the application context). michael@0: * michael@0: * Calling multiple times is harmless. michael@0: */ michael@0: @Override michael@0: public void initialize(final Context context) { michael@0: if (!inited.compareAndSet(false, true)) { michael@0: return; michael@0: } michael@0: michael@0: receiver = new BroadcastReceiver() { michael@0: @Override michael@0: public void onReceive(Context context, Intent intent) { michael@0: systemLocaleDidChange = true; michael@0: } michael@0: }; michael@0: context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); michael@0: } michael@0: michael@0: @Override michael@0: public boolean systemLocaleDidChange() { michael@0: return systemLocaleDidChange; michael@0: } michael@0: michael@0: /** michael@0: * Every time the system gives us a new configuration, it michael@0: * carries the external locale. Fix it. michael@0: */ michael@0: @Override michael@0: public void correctLocale(Context context, Resources res, Configuration config) { michael@0: final Locale current = getCurrentLocale(context); michael@0: if (current == null) { michael@0: return; michael@0: } michael@0: michael@0: // I know it's tempting to short-circuit here if the config seems to be michael@0: // up-to-date, but the rest is necessary. michael@0: michael@0: config.locale = current; michael@0: michael@0: // The following two lines are heavily commented in case someone michael@0: // decides to chase down performance improvements and decides to michael@0: // question what's going on here. michael@0: // Both lines should be cheap, *but*... michael@0: michael@0: // This is unnecessary for basic string choice, but it almost michael@0: // certainly comes into play when rendering numbers, deciding on RTL, michael@0: // etc. Take it out if you can prove that's not the case. michael@0: Locale.setDefault(current); michael@0: michael@0: // This seems to be a no-op, but every piece of documentation under the michael@0: // sun suggests that it's necessary, and it certainly makes sense. michael@0: res.updateConfiguration(config, res.getDisplayMetrics()); michael@0: } michael@0: michael@0: @Override michael@0: public String getAndApplyPersistedLocale(Context context) { michael@0: initialize(context); michael@0: michael@0: final long t1 = android.os.SystemClock.uptimeMillis(); michael@0: final String localeCode = getPersistedLocale(context); michael@0: if (localeCode == null) { michael@0: return null; michael@0: } michael@0: michael@0: // Note that we don't tell Gecko about this. We notify Gecko when the michael@0: // locale is set, not when we update Java. michael@0: final String resultant = updateLocale(context, localeCode); michael@0: michael@0: if (resultant == null) { michael@0: // Update the configuration anyway. michael@0: updateConfiguration(context, currentLocale); michael@0: } michael@0: michael@0: final long t2 = android.os.SystemClock.uptimeMillis(); michael@0: Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms."); michael@0: return resultant; michael@0: } michael@0: michael@0: /** michael@0: * Returns the set locale if it changed. michael@0: * michael@0: * Always persists and notifies Gecko. michael@0: */ michael@0: @Override michael@0: public String setSelectedLocale(Context context, String localeCode) { michael@0: final String resultant = updateLocale(context, localeCode); michael@0: michael@0: // We always persist and notify Gecko, even if nothing seemed to michael@0: // change. This might happen if you're picking a locale that's the same michael@0: // as the current OS locale. The OS locale might change next time we michael@0: // launch, and we need the Gecko pref and persisted locale to have been michael@0: // set by the time that happens. michael@0: persistLocale(context, localeCode); michael@0: michael@0: // Tell Gecko. michael@0: GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context))); michael@0: GeckoAppShell.sendEventToGecko(ev); michael@0: michael@0: return resultant; michael@0: } michael@0: michael@0: /** michael@0: * This is public to allow for an activity to force the michael@0: * current locale to be applied if necessary (e.g., when michael@0: * a new activity launches). michael@0: */ michael@0: @Override michael@0: public void updateConfiguration(Context context, Locale locale) { michael@0: Resources res = context.getResources(); michael@0: Configuration config = res.getConfiguration(); michael@0: config.locale = locale; michael@0: res.updateConfiguration(config, res.getDisplayMetrics()); michael@0: } michael@0: michael@0: private SharedPreferences getSharedPreferences(Context context) { michael@0: return GeckoSharedPrefs.forApp(context); michael@0: } michael@0: michael@0: private String getPersistedLocale(Context context) { michael@0: final SharedPreferences settings = getSharedPreferences(context); michael@0: final String locale = settings.getString(PREF_LOCALE, ""); michael@0: michael@0: if ("".equals(locale)) { michael@0: return null; michael@0: } michael@0: return locale; michael@0: } michael@0: michael@0: private void persistLocale(Context context, String localeCode) { michael@0: final SharedPreferences settings = getSharedPreferences(context); michael@0: settings.edit().putString(PREF_LOCALE, localeCode).commit(); michael@0: } michael@0: michael@0: private Locale getCurrentLocale(Context context) { michael@0: if (currentLocale != null) { michael@0: return currentLocale; michael@0: } michael@0: michael@0: final String current = getPersistedLocale(context); michael@0: if (current == null) { michael@0: return null; michael@0: } michael@0: return currentLocale = parseLocaleCode(current); michael@0: } michael@0: michael@0: /** michael@0: * Updates the Java locale and the Android configuration. michael@0: * michael@0: * Returns the persisted locale if it differed. michael@0: * michael@0: * Does not notify Gecko. michael@0: */ michael@0: private String updateLocale(Context context, String localeCode) { michael@0: // Fast path. michael@0: final Locale defaultLocale = Locale.getDefault(); michael@0: if (defaultLocale.toString().equals(localeCode)) { michael@0: return null; michael@0: } michael@0: michael@0: final Locale locale = parseLocaleCode(localeCode); michael@0: michael@0: // Fast path. michael@0: if (defaultLocale.equals(locale)) { michael@0: return null; michael@0: } michael@0: michael@0: Locale.setDefault(locale); michael@0: currentLocale = locale; michael@0: michael@0: // Update resources. michael@0: updateConfiguration(context, locale); michael@0: michael@0: return locale.toString(); michael@0: } michael@0: }