summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-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.java439
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;
+ }
+}