Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko; |
michael@0 | 7 | |
michael@0 | 8 | import android.content.BroadcastReceiver; |
michael@0 | 9 | import android.content.Context; |
michael@0 | 10 | import android.content.Intent; |
michael@0 | 11 | import android.content.IntentFilter; |
michael@0 | 12 | import android.content.SharedPreferences; |
michael@0 | 13 | import android.content.res.Configuration; |
michael@0 | 14 | import android.content.res.Resources; |
michael@0 | 15 | import android.util.Log; |
michael@0 | 16 | |
michael@0 | 17 | import java.util.Locale; |
michael@0 | 18 | import java.util.concurrent.atomic.AtomicBoolean; |
michael@0 | 19 | import java.util.concurrent.atomic.AtomicReference; |
michael@0 | 20 | |
michael@0 | 21 | /** |
michael@0 | 22 | * This class manages persistence, application, and otherwise handling of |
michael@0 | 23 | * user-specified locales. |
michael@0 | 24 | * |
michael@0 | 25 | * Of note: |
michael@0 | 26 | * |
michael@0 | 27 | * * It's a singleton, because its scope extends to that of the application, |
michael@0 | 28 | * and definitionally all changes to the locale of the app must go through |
michael@0 | 29 | * this. |
michael@0 | 30 | * * It's lazy. |
michael@0 | 31 | * * It has ties into the Gecko event system, because it has to tell Gecko when |
michael@0 | 32 | * to switch locale. |
michael@0 | 33 | * * It relies on using the SharedPreferences file owned by the browser (in |
michael@0 | 34 | * Fennec's case, "GeckoApp") for performance. |
michael@0 | 35 | */ |
michael@0 | 36 | public class BrowserLocaleManager implements LocaleManager { |
michael@0 | 37 | private static final String LOG_TAG = "GeckoLocales"; |
michael@0 | 38 | |
michael@0 | 39 | private static final String EVENT_LOCALE_CHANGED = "Locale:Changed"; |
michael@0 | 40 | private static final String PREF_LOCALE = "locale"; |
michael@0 | 41 | |
michael@0 | 42 | // This is volatile because we don't impose restrictions |
michael@0 | 43 | // over which thread calls our methods. |
michael@0 | 44 | private volatile Locale currentLocale = null; |
michael@0 | 45 | |
michael@0 | 46 | private AtomicBoolean inited = new AtomicBoolean(false); |
michael@0 | 47 | private boolean systemLocaleDidChange = false; |
michael@0 | 48 | private BroadcastReceiver receiver; |
michael@0 | 49 | |
michael@0 | 50 | private static AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>(); |
michael@0 | 51 | |
michael@0 | 52 | public static LocaleManager getInstance() { |
michael@0 | 53 | LocaleManager localeManager = instance.get(); |
michael@0 | 54 | if (localeManager != null) { |
michael@0 | 55 | return localeManager; |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | localeManager = new BrowserLocaleManager(); |
michael@0 | 59 | if (instance.compareAndSet(null, localeManager)) { |
michael@0 | 60 | return localeManager; |
michael@0 | 61 | } else { |
michael@0 | 62 | return instance.get(); |
michael@0 | 63 | } |
michael@0 | 64 | } |
michael@0 | 65 | |
michael@0 | 66 | /** |
michael@0 | 67 | * Gecko uses locale codes like "es-ES", whereas a Java {@link Locale} |
michael@0 | 68 | * stringifies as "es_ES". |
michael@0 | 69 | * |
michael@0 | 70 | * This method approximates the Java 7 method <code>Locale#toLanguageTag()</code>. |
michael@0 | 71 | * |
michael@0 | 72 | * @return a locale string suitable for passing to Gecko. |
michael@0 | 73 | */ |
michael@0 | 74 | public static String getLanguageTag(final Locale locale) { |
michael@0 | 75 | // If this were Java 7: |
michael@0 | 76 | // return locale.toLanguageTag(); |
michael@0 | 77 | |
michael@0 | 78 | String language = locale.getLanguage(); // Can, but should never be, an empty string. |
michael@0 | 79 | // Modernize certain language codes. |
michael@0 | 80 | if (language.equals("iw")) { |
michael@0 | 81 | language = "he"; |
michael@0 | 82 | } else if (language.equals("in")) { |
michael@0 | 83 | language = "id"; |
michael@0 | 84 | } else if (language.equals("ji")) { |
michael@0 | 85 | language = "yi"; |
michael@0 | 86 | } |
michael@0 | 87 | |
michael@0 | 88 | String country = locale.getCountry(); // Can be an empty string. |
michael@0 | 89 | if (country.equals("")) { |
michael@0 | 90 | return language; |
michael@0 | 91 | } |
michael@0 | 92 | return language + "-" + country; |
michael@0 | 93 | } |
michael@0 | 94 | |
michael@0 | 95 | private static Locale parseLocaleCode(final String localeCode) { |
michael@0 | 96 | int index; |
michael@0 | 97 | if ((index = localeCode.indexOf('-')) != -1 || |
michael@0 | 98 | (index = localeCode.indexOf('_')) != -1) { |
michael@0 | 99 | final String langCode = localeCode.substring(0, index); |
michael@0 | 100 | final String countryCode = localeCode.substring(index + 1); |
michael@0 | 101 | return new Locale(langCode, countryCode); |
michael@0 | 102 | } else { |
michael@0 | 103 | return new Locale(localeCode); |
michael@0 | 104 | } |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | /** |
michael@0 | 108 | * Ensure that you call this early in your application startup, |
michael@0 | 109 | * and with a context that's sufficiently long-lived (typically |
michael@0 | 110 | * the application context). |
michael@0 | 111 | * |
michael@0 | 112 | * Calling multiple times is harmless. |
michael@0 | 113 | */ |
michael@0 | 114 | @Override |
michael@0 | 115 | public void initialize(final Context context) { |
michael@0 | 116 | if (!inited.compareAndSet(false, true)) { |
michael@0 | 117 | return; |
michael@0 | 118 | } |
michael@0 | 119 | |
michael@0 | 120 | receiver = new BroadcastReceiver() { |
michael@0 | 121 | @Override |
michael@0 | 122 | public void onReceive(Context context, Intent intent) { |
michael@0 | 123 | systemLocaleDidChange = true; |
michael@0 | 124 | } |
michael@0 | 125 | }; |
michael@0 | 126 | context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); |
michael@0 | 127 | } |
michael@0 | 128 | |
michael@0 | 129 | @Override |
michael@0 | 130 | public boolean systemLocaleDidChange() { |
michael@0 | 131 | return systemLocaleDidChange; |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | /** |
michael@0 | 135 | * Every time the system gives us a new configuration, it |
michael@0 | 136 | * carries the external locale. Fix it. |
michael@0 | 137 | */ |
michael@0 | 138 | @Override |
michael@0 | 139 | public void correctLocale(Context context, Resources res, Configuration config) { |
michael@0 | 140 | final Locale current = getCurrentLocale(context); |
michael@0 | 141 | if (current == null) { |
michael@0 | 142 | return; |
michael@0 | 143 | } |
michael@0 | 144 | |
michael@0 | 145 | // I know it's tempting to short-circuit here if the config seems to be |
michael@0 | 146 | // up-to-date, but the rest is necessary. |
michael@0 | 147 | |
michael@0 | 148 | config.locale = current; |
michael@0 | 149 | |
michael@0 | 150 | // The following two lines are heavily commented in case someone |
michael@0 | 151 | // decides to chase down performance improvements and decides to |
michael@0 | 152 | // question what's going on here. |
michael@0 | 153 | // Both lines should be cheap, *but*... |
michael@0 | 154 | |
michael@0 | 155 | // This is unnecessary for basic string choice, but it almost |
michael@0 | 156 | // certainly comes into play when rendering numbers, deciding on RTL, |
michael@0 | 157 | // etc. Take it out if you can prove that's not the case. |
michael@0 | 158 | Locale.setDefault(current); |
michael@0 | 159 | |
michael@0 | 160 | // This seems to be a no-op, but every piece of documentation under the |
michael@0 | 161 | // sun suggests that it's necessary, and it certainly makes sense. |
michael@0 | 162 | res.updateConfiguration(config, res.getDisplayMetrics()); |
michael@0 | 163 | } |
michael@0 | 164 | |
michael@0 | 165 | @Override |
michael@0 | 166 | public String getAndApplyPersistedLocale(Context context) { |
michael@0 | 167 | initialize(context); |
michael@0 | 168 | |
michael@0 | 169 | final long t1 = android.os.SystemClock.uptimeMillis(); |
michael@0 | 170 | final String localeCode = getPersistedLocale(context); |
michael@0 | 171 | if (localeCode == null) { |
michael@0 | 172 | return null; |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | // Note that we don't tell Gecko about this. We notify Gecko when the |
michael@0 | 176 | // locale is set, not when we update Java. |
michael@0 | 177 | final String resultant = updateLocale(context, localeCode); |
michael@0 | 178 | |
michael@0 | 179 | if (resultant == null) { |
michael@0 | 180 | // Update the configuration anyway. |
michael@0 | 181 | updateConfiguration(context, currentLocale); |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | final long t2 = android.os.SystemClock.uptimeMillis(); |
michael@0 | 185 | Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms."); |
michael@0 | 186 | return resultant; |
michael@0 | 187 | } |
michael@0 | 188 | |
michael@0 | 189 | /** |
michael@0 | 190 | * Returns the set locale if it changed. |
michael@0 | 191 | * |
michael@0 | 192 | * Always persists and notifies Gecko. |
michael@0 | 193 | */ |
michael@0 | 194 | @Override |
michael@0 | 195 | public String setSelectedLocale(Context context, String localeCode) { |
michael@0 | 196 | final String resultant = updateLocale(context, localeCode); |
michael@0 | 197 | |
michael@0 | 198 | // We always persist and notify Gecko, even if nothing seemed to |
michael@0 | 199 | // change. This might happen if you're picking a locale that's the same |
michael@0 | 200 | // as the current OS locale. The OS locale might change next time we |
michael@0 | 201 | // launch, and we need the Gecko pref and persisted locale to have been |
michael@0 | 202 | // set by the time that happens. |
michael@0 | 203 | persistLocale(context, localeCode); |
michael@0 | 204 | |
michael@0 | 205 | // Tell Gecko. |
michael@0 | 206 | GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, BrowserLocaleManager.getLanguageTag(getCurrentLocale(context))); |
michael@0 | 207 | GeckoAppShell.sendEventToGecko(ev); |
michael@0 | 208 | |
michael@0 | 209 | return resultant; |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | /** |
michael@0 | 213 | * This is public to allow for an activity to force the |
michael@0 | 214 | * current locale to be applied if necessary (e.g., when |
michael@0 | 215 | * a new activity launches). |
michael@0 | 216 | */ |
michael@0 | 217 | @Override |
michael@0 | 218 | public void updateConfiguration(Context context, Locale locale) { |
michael@0 | 219 | Resources res = context.getResources(); |
michael@0 | 220 | Configuration config = res.getConfiguration(); |
michael@0 | 221 | config.locale = locale; |
michael@0 | 222 | res.updateConfiguration(config, res.getDisplayMetrics()); |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | private SharedPreferences getSharedPreferences(Context context) { |
michael@0 | 226 | return GeckoSharedPrefs.forApp(context); |
michael@0 | 227 | } |
michael@0 | 228 | |
michael@0 | 229 | private String getPersistedLocale(Context context) { |
michael@0 | 230 | final SharedPreferences settings = getSharedPreferences(context); |
michael@0 | 231 | final String locale = settings.getString(PREF_LOCALE, ""); |
michael@0 | 232 | |
michael@0 | 233 | if ("".equals(locale)) { |
michael@0 | 234 | return null; |
michael@0 | 235 | } |
michael@0 | 236 | return locale; |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | private void persistLocale(Context context, String localeCode) { |
michael@0 | 240 | final SharedPreferences settings = getSharedPreferences(context); |
michael@0 | 241 | settings.edit().putString(PREF_LOCALE, localeCode).commit(); |
michael@0 | 242 | } |
michael@0 | 243 | |
michael@0 | 244 | private Locale getCurrentLocale(Context context) { |
michael@0 | 245 | if (currentLocale != null) { |
michael@0 | 246 | return currentLocale; |
michael@0 | 247 | } |
michael@0 | 248 | |
michael@0 | 249 | final String current = getPersistedLocale(context); |
michael@0 | 250 | if (current == null) { |
michael@0 | 251 | return null; |
michael@0 | 252 | } |
michael@0 | 253 | return currentLocale = parseLocaleCode(current); |
michael@0 | 254 | } |
michael@0 | 255 | |
michael@0 | 256 | /** |
michael@0 | 257 | * Updates the Java locale and the Android configuration. |
michael@0 | 258 | * |
michael@0 | 259 | * Returns the persisted locale if it differed. |
michael@0 | 260 | * |
michael@0 | 261 | * Does not notify Gecko. |
michael@0 | 262 | */ |
michael@0 | 263 | private String updateLocale(Context context, String localeCode) { |
michael@0 | 264 | // Fast path. |
michael@0 | 265 | final Locale defaultLocale = Locale.getDefault(); |
michael@0 | 266 | if (defaultLocale.toString().equals(localeCode)) { |
michael@0 | 267 | return null; |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | final Locale locale = parseLocaleCode(localeCode); |
michael@0 | 271 | |
michael@0 | 272 | // Fast path. |
michael@0 | 273 | if (defaultLocale.equals(locale)) { |
michael@0 | 274 | return null; |
michael@0 | 275 | } |
michael@0 | 276 | |
michael@0 | 277 | Locale.setDefault(locale); |
michael@0 | 278 | currentLocale = locale; |
michael@0 | 279 | |
michael@0 | 280 | // Update resources. |
michael@0 | 281 | updateConfiguration(context, locale); |
michael@0 | 282 | |
michael@0 | 283 | return locale.toString(); |
michael@0 | 284 | } |
michael@0 | 285 | } |