mobile/android/base/BrowserLocaleManager.java

changeset 0
6474c204b198
     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 +}

mercurial