diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java new file mode 100644 index 000000000..c5c041c7a --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java @@ -0,0 +1,439 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.io.File; +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.annotation.ReflectionTarget; +import org.mozilla.gecko.util.GeckoJarReader; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.Log; + +/** + * This class manages persistence, application, and otherwise handling of + * user-specified locales. + * + * Of note: + * + * * It's a singleton, because its scope extends to that of the application, + * and definitionally all changes to the locale of the app must go through + * this. + * * It's lazy. + * * It has ties into the Gecko event system, because it has to tell Gecko when + * to switch locale. + * * It relies on using the SharedPreferences file owned by the browser (in + * Fennec's case, "GeckoApp") for performance. + */ +public class BrowserLocaleManager implements LocaleManager { + private static final String LOG_TAG = "GeckoLocales"; + + private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; + private static final String PREF_LOCALE = "locale"; + + private static final String FALLBACK_LOCALE_TAG = "en-US"; + + // These are volatile because we don't impose restrictions + // over which thread calls our methods. + private volatile Locale currentLocale; + private volatile Locale systemLocale = Locale.getDefault(); + + private final AtomicBoolean inited = new AtomicBoolean(false); + private boolean systemLocaleDidChange; + private BroadcastReceiver receiver; + + private static final AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>(); + + @ReflectionTarget + public static LocaleManager getInstance() { + LocaleManager localeManager = instance.get(); + if (localeManager != null) { + return localeManager; + } + + localeManager = new BrowserLocaleManager(); + if (instance.compareAndSet(null, localeManager)) { + return localeManager; + } else { + return instance.get(); + } + } + + @Override + public boolean isEnabled() { + return AppConstants.MOZ_LOCALE_SWITCHER; + } + + /** + * Ensure that you call this early in your application startup, + * and with a context that's sufficiently long-lived (typically + * the application context). + * + * Calling multiple times is harmless. + */ + @Override + public void initialize(final Context context) { + if (!inited.compareAndSet(false, true)) { + return; + } + + receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final Locale current = systemLocale; + + // We don't trust Locale.getDefault() here, because we make a + // habit of mutating it! Use the one Android supplies, because + // that gets regularly reset. + // The default value of systemLocale is fine, because we haven't + // yet swizzled Locale during static initialization. + systemLocale = context.getResources().getConfiguration().locale; + systemLocaleDidChange = true; + + Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale); + } + }; + context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); + } + + @Override + public boolean systemLocaleDidChange() { + return systemLocaleDidChange; + } + + /** + * Every time the system gives us a new configuration, it + * carries the external locale. Fix it. + */ + @Override + public void correctLocale(Context context, Resources res, Configuration config) { + final Locale current = getCurrentLocale(context); + if (current == null) { + Log.d(LOG_TAG, "No selected locale. No correction needed."); + return; + } + + // I know it's tempting to short-circuit here if the config seems to be + // up-to-date, but the rest is necessary. + + config.locale = current; + + // The following two lines are heavily commented in case someone + // decides to chase down performance improvements and decides to + // question what's going on here. + // Both lines should be cheap, *but*... + + // This is unnecessary for basic string choice, but it almost + // certainly comes into play when rendering numbers, deciding on RTL, + // etc. Take it out if you can prove that's not the case. + Locale.setDefault(current); + + // This seems to be a no-op, but every piece of documentation under the + // sun suggests that it's necessary, and it certainly makes sense. + res.updateConfiguration(config, null); + } + + /** + * We can be in one of two states. + * + * If the user has not explicitly chosen a Firefox-specific locale, we say + * we are "mirroring" the system locale. + * + * When we are not mirroring, system locale changes do not impact Firefox + * and are essentially ignored; the user's locale selection is the only + * thing we care about, and we actively correct incoming configuration + * changes to reflect the user's chosen locale. + * + * By contrast, when we are mirroring, system locale changes cause Firefox + * to reflect the new system locale, as if the user picked the new locale. + * + * If we're currently mirroring the system locale, this method returns the + * supplied configuration's locale, unless the current activity locale is + * correct. If we're not currently mirroring, this method updates the + * configuration object to match the user's currently selected locale, and + * returns that, unless the current activity locale is correct. + * + * If the current activity locale is correct, returns null. + * + * The caller is expected to redisplay themselves accordingly. + * + * This method is intended to be called from inside + * <code>onConfigurationChanged(Configuration)</code> as part of a strategy + * to detect and either apply or undo system locale changes. + */ + @Override + public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) { + if (!isMirroringSystemLocale(context)) { + correctLocale(context, resources, configuration); + } + + final Locale changed = configuration.locale; + if (changed.equals(currentActivityLocale)) { + return null; + } + + return changed; + } + + /** + * Gecko needs to know the OS locale to compute a useful Accept-Language + * header. If it changed since last time, send a message to Gecko and + * persist the new value. If unchanged, returns immediately. + * + * @param prefs the SharedPreferences instance to use. Cannot be null. + * @param osLocale the new locale instance. Safe if null. + */ + public static void storeAndNotifyOSLocale(final SharedPreferences prefs, + final Locale osLocale) { + if (osLocale == null) { + return; + } + + final String lastOSLocale = prefs.getString("osLocale", null); + final String osLocaleString = osLocale.toString(); + + if (osLocaleString.equals(lastOSLocale)) { + return; + } + + // Store the Java-native form. + prefs.edit().putString("osLocale", osLocaleString).apply(); + + // The value we send to Gecko should be a language tag, not + // a Java locale string. + final String osLanguageTag = Locales.getLanguageTag(osLocale); + GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag); + } + + @Override + public String getAndApplyPersistedLocale(Context context) { + initialize(context); + + final long t1 = android.os.SystemClock.uptimeMillis(); + final String localeCode = getPersistedLocale(context); + if (localeCode == null) { + return null; + } + + // Note that we don't tell Gecko about this. We notify Gecko when the + // locale is set, not when we update Java. + final String resultant = updateLocale(context, localeCode); + + if (resultant == null) { + // Update the configuration anyway. + updateConfiguration(context, currentLocale); + } + + final long t2 = android.os.SystemClock.uptimeMillis(); + Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms."); + return resultant; + } + + /** + * Returns the set locale if it changed. + * + * Always persists and notifies Gecko. + */ + @Override + public String setSelectedLocale(Context context, String localeCode) { + final String resultant = updateLocale(context, localeCode); + + // We always persist and notify Gecko, even if nothing seemed to + // change. This might happen if you're picking a locale that's the same + // as the current OS locale. The OS locale might change next time we + // launch, and we need the Gecko pref and persisted locale to have been + // set by the time that happens. + persistLocale(context, localeCode); + + // Tell Gecko. + GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context))); + + return resultant; + } + + @Override + public void resetToSystemLocale(Context context) { + // Wipe the pref. + final SharedPreferences settings = getSharedPreferences(context); + settings.edit().remove(PREF_LOCALE).apply(); + + // Apply the system locale. + updateLocale(context, systemLocale); + + // Tell Gecko. + GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, ""); + } + + /** + * This is public to allow for an activity to force the + * current locale to be applied if necessary (e.g., when + * a new activity launches). + */ + @Override + public void updateConfiguration(Context context, Locale locale) { + Resources res = context.getResources(); + Configuration config = res.getConfiguration(); + + // We should use setLocale, but it's unexpectedly missing + // on real devices. + config.locale = locale; + res.updateConfiguration(config, null); + } + + private SharedPreferences getSharedPreferences(Context context) { + return GeckoSharedPrefs.forApp(context); + } + + /** + * @return the persisted locale in Java format: "en_US". + */ + private String getPersistedLocale(Context context) { + final SharedPreferences settings = getSharedPreferences(context); + final String locale = settings.getString(PREF_LOCALE, ""); + + if ("".equals(locale)) { + return null; + } + return locale; + } + + private void persistLocale(Context context, String localeCode) { + final SharedPreferences settings = getSharedPreferences(context); + settings.edit().putString(PREF_LOCALE, localeCode).apply(); + } + + @Override + public Locale getCurrentLocale(Context context) { + if (currentLocale != null) { + return currentLocale; + } + + final String current = getPersistedLocale(context); + if (current == null) { + return null; + } + return currentLocale = Locales.parseLocaleCode(current); + } + + /** + * Updates the Java locale and the Android configuration. + * + * Returns the persisted locale if it differed. + * + * Does not notify Gecko. + * + * @param localeCode a locale string in Java format: "en_US". + * @return if it differed, a locale string in Java format: "en_US". + */ + private String updateLocale(Context context, String localeCode) { + // Fast path. + final Locale defaultLocale = Locale.getDefault(); + if (defaultLocale.toString().equals(localeCode)) { + return null; + } + + final Locale locale = Locales.parseLocaleCode(localeCode); + + return updateLocale(context, locale); + } + + /** + * @return the Java locale string: e.g., "en_US". + */ + private String updateLocale(Context context, final Locale locale) { + // Fast path. + if (Locale.getDefault().equals(locale)) { + return null; + } + + Locale.setDefault(locale); + currentLocale = locale; + + // Update resources. + updateConfiguration(context, locale); + + return locale.toString(); + } + + private boolean isMirroringSystemLocale(final Context context) { + return getPersistedLocale(context) == null; + } + + /** + * Examines <code>multilocale.json</code>, returning the included list of + * locale codes. + * + * If <code>multilocale.json</code> is not present, returns + * <code>null</code>. In that case, consider {@link #getFallbackLocaleTag()}. + * + * multilocale.json currently looks like this: + * + * <code> + * {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB", + * "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi", + * "fr", "ga-IE", "hu", "id", "it", "ja", "ko", + * "lt", "lv", "nb-NO", "nl", "pl", "pt-BR", + * "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th", + * "tr", "uk", "zh-CN", "zh-TW", "en-US"]} + * </code> + */ + public static Collection<String> getPackagedLocaleTags(final Context context) { + final String resPath = "res/multilocale.json"; + final String jarURL = GeckoJarReader.getJarURL(context, resPath); + + final String contents = GeckoJarReader.getText(context, jarURL); + if (contents == null) { + // GeckoJarReader logs and swallows exceptions. + return null; + } + + try { + final JSONObject multilocale = new JSONObject(contents); + final JSONArray locales = multilocale.getJSONArray("locales"); + if (locales == null) { + Log.e(LOG_TAG, "No 'locales' array in multilocales.json!"); + return null; + } + + final Set<String> out = new HashSet<String>(locales.length()); + for (int i = 0; i < locales.length(); ++i) { + // If any item in the array is invalid, this will throw, + // and the entire clause will fail, being caught below + // and returning null. + out.add(locales.getString(i)); + } + + return out; + } catch (JSONException e) { + Log.e(LOG_TAG, "Unable to parse multilocale.json.", e); + return null; + } + } + + /** + * @return the single default locale baked into this application. + * Applicable when there is no multilocale.json present. + */ + @SuppressWarnings("static-method") + public String getFallbackLocaleTag() { + return FALLBACK_LOCALE_TAG; + } +} |