diff --git a/mobile/android/base/BrowserLocaleManager.java b/mobile/android/base/BrowserLocaleManager.java index a30298ca95a2..64816613953b 100644 --- a/mobile/android/base/BrowserLocaleManager.java +++ b/mobile/android/base/BrowserLocaleManager.java @@ -249,6 +249,37 @@ public class BrowserLocaleManager implements LocaleManager { 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 = BrowserLocaleManager.getLanguageTag(osLocale); + final GeckoEvent localeOSEvent = GeckoEvent.createBroadcastEvent("Locale:OS", osLanguageTag); + GeckoAppShell.sendEventToGecko(localeOSEvent); + } + @Override public String getAndApplyPersistedLocale(Context context) { initialize(context); diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 68fb92aa5b3e..52cd0f17febb 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -1213,6 +1213,9 @@ public abstract class GeckoApp // the UI. // This is using a sledgehammer to crack a nut, but it'll do for // now. + // Our OS locale pref will be detected as invalid after the + // restart, and will be propagated to Gecko accordingly, so there's + // no need to touch that here. if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) { Log.i(LOGTAG, "System locale changed. Restarting."); doRestart(); @@ -1324,29 +1327,36 @@ public abstract class GeckoApp final String profilePath = getProfile().getDir().getAbsolutePath(); final EventDispatcher dispatcher = EventDispatcher.getInstance(); - // Both of these are Java-format locale strings: "en_US", not "en-US". - final String osLocale = Locale.getDefault().toString(); - String appLocale = localeManager.getAndApplyPersistedLocale(GeckoApp.this); - Log.d(LOGTAG, "OS locale is " + osLocale + ", app locale is " + appLocale); + // This is the locale prior to fixing it up. + final Locale osLocale = Locale.getDefault(); - if (appLocale == null) { - appLocale = osLocale; + // Both of these are Java-format locale strings: "en_US", not "en-US". + final String osLocaleString = osLocale.toString(); + String appLocaleString = localeManager.getAndApplyPersistedLocale(GeckoApp.this); + Log.d(LOGTAG, "OS locale is " + osLocaleString + ", app locale is " + appLocaleString); + + if (appLocaleString == null) { + appLocaleString = osLocaleString; } mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this, profilePath, dispatcher, - osLocale, - appLocale, + osLocaleString, + appLocaleString, previousSession); - final String uiLocale = appLocale; + final String uiLocale = appLocaleString; ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { GeckoApp.this.onLocaleReady(uiLocale); } }); + + // We use per-profile prefs here, because we're tracking against + // a Gecko pref. The same applies to the locale switcher! + BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(GeckoApp.this), osLocale); } }); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index af72035f20eb..28e245480c8f 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -342,6 +342,7 @@ var BrowserApp = { Services.androidBridge.browserApp = this; + Services.obs.addObserver(this, "Locale:OS", false); Services.obs.addObserver(this, "Locale:Changed", false); Services.obs.addObserver(this, "Tab:Load", false); Services.obs.addObserver(this, "Tab:Selected", false); @@ -1758,6 +1759,34 @@ var BrowserApp = { WebappManager.autoUninstall(JSON.parse(aData)); break; + case "Locale:OS": + // We know the system locale. We use this for generating Accept-Language headers. + console.log("Locale:OS: " + aData); + let currentOSLocale; + try { + currentOSLocale = Services.prefs.getCharPref("intl.locale.os"); + } catch (e) { + } + if (currentOSLocale == aData) { + break; + } + + console.log("New OS locale."); + + // Ensure that this choice is immediately persisted, because + // Gecko won't be told again if it forgets. + Services.prefs.setCharPref("intl.locale.os", aData); + Services.prefs.savePrefFile(null); + + let appLocale; + try { + appLocale = Services.prefs.getCharPref("general.useragent.locale"); + } catch (e) { + } + + this.computeAcceptLanguages(aData, appLocale); + break; + case "Locale:Changed": if (aData) { // The value provided to Locale:Changed should be a BCP47 language tag @@ -1779,6 +1808,16 @@ var BrowserApp = { // Blow away the string cache so that future lookups get the // correct locale. Services.strings.flushBundles(); + + // Make sure we use the right Accept-Language header. + let osLocale; + try { + // This should never not be set at this point, but better safe than sorry. + osLocale = Services.prefs.getCharPref("intl.locale.os"); + } catch (e) { + } + + this.computeAcceptLanguages(osLocale, aData); break; default: @@ -1788,6 +1827,63 @@ var BrowserApp = { } }, + /** + * Set intl.accept_languages accordingly. + * + * After Bug 881510 this will also accept a real Accept-Language choice as + * input; all Accept-Language logic lives here. + * + * osLocale should never be null, but this method is safe regardless. + * appLocale may explicitly be null. + */ + computeAcceptLanguages(osLocale, appLocale) { + let defaultBranch = Services.prefs.getDefaultBranch(null); + let defaultAccept = defaultBranch.getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString).data; + console.log("Default intl.accept_languages = " + defaultAccept); + + // A guard for potential breakage. Bug 438031. + // This should not be necessary, because we're reading from the default branch, + // but better safe than sorry. + if (defaultAccept && defaultAccept.startsWith("chrome://")) { + defaultAccept = null; + } else { + // Ensure lowercase everywhere so we can compare. + defaultAccept = defaultAccept.toLowerCase(); + } + + if (appLocale) { + appLocale = appLocale.toLowerCase(); + } + + if (osLocale) { + osLocale = osLocale.toLowerCase(); + } + + // Eliminate values if they're present in the default. + let chosen; + if (defaultAccept) { + // intl.accept_languages is a comma-separated list, with no q-value params. Those + // are added when the header is generated. + chosen = defaultAccept.split(",") + .map(String.trim) + .filter((x) => (x != appLocale && x != osLocale)); + } else { + chosen = []; + } + + if (osLocale) { + chosen.unshift(osLocale); + } + + if (appLocale && appLocale != osLocale) { + chosen.unshift(appLocale); + } + + let result = chosen.join(","); + console.log("Setting intl.accept_languages to " + result); + Services.prefs.setCharPref("intl.accept_languages", result); + }, + get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");