Bug 1045053 - Part 1: set intl.accept_languages from Android OS/app locale. r=bnicholson
authorRichard Newman <rnewman@mozilla.com>
Wed, 01 Oct 2014 18:09:42 -0700
changeset 210314 641e305c19fe2869133c2dc713dc71571af1fd09
parent 210313 5eb5877d73667148f3a56de978d758a647e5f9cb
child 210315 4898d184d713dc5295bc0bb2d02126db27a3a76c
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersbnicholson
bugs1045053
milestone36.0a1
Bug 1045053 - Part 1: set intl.accept_languages from Android OS/app locale. r=bnicholson
mobile/android/base/BrowserLocaleManager.java
mobile/android/base/GeckoApp.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/BrowserLocaleManager.java
+++ b/mobile/android/base/BrowserLocaleManager.java
@@ -244,16 +244,47 @@ public class BrowserLocaleManager implem
         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 = BrowserLocaleManager.getLanguageTag(osLocale);
+        final GeckoEvent localeOSEvent = GeckoEvent.createBroadcastEvent("Locale:OS", osLanguageTag);
+        GeckoAppShell.sendEventToGecko(localeOSEvent);
+    }
+
     @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;
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1208,16 +1208,19 @@ public abstract class GeckoApp
             Log.e(LOGTAG, "Exception starting favicon cache. Corrupt resources?", e);
         }
 
         // Did the OS locale change while we were backgrounded? If so,
         // we need to die so that Gecko will re-init add-ons that touch
         // 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();
             GeckoAppShell.systemExit();
             return;
         }
 
         if (GeckoThread.isCreated()) {
@@ -1319,39 +1322,46 @@ public abstract class GeckoApp
                 editor.apply();
 
                 // The lifecycle of mHealthRecorder is "shortly after onCreate"
                 // through "onDestroy" -- essentially the same as the lifecycle
                 // of the activity itself.
                 final String profilePath = getProfile().getDir().getAbsolutePath();
                 final EventDispatcher dispatcher = EventDispatcher.getInstance();
 
+                // This is the locale prior to fixing it up.
+                final Locale osLocale = Locale.getDefault();
+
                 // 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);
-
-                if (appLocale == null) {
-                    appLocale = osLocale;
+                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);
             }
         });
 
         GeckoAppShell.setNotificationClient(makeNotificationClient());
         IntentHelper.init(this);
     }
 
     /**
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -337,16 +337,17 @@ var BrowserApp = {
       } catch(ex) { console.log(ex); }
     }, false);
 
     BrowserEventHandler.init();
     ViewportHandler.init();
 
     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);
     Services.obs.addObserver(this, "Tab:Closed", false);
     Services.obs.addObserver(this, "Session:Back", false);
     Services.obs.addObserver(this, "Session:ShowHistory", false);
     Services.obs.addObserver(this, "Session:Forward", false);
     Services.obs.addObserver(this, "Session:Reload", false);
@@ -1753,16 +1754,44 @@ var BrowserApp = {
       case "Webapps:Load":
         this._loadWebapp(JSON.parse(aData));
         break;
 
       case "Webapps:AutoUninstall":
         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
           // understood by Gecko -- for example, "es-ES" or "de".
           console.log("Locale:Changed: " + aData);
           Services.prefs.setCharPref("general.useragent.locale", aData);
         } else {
           // Resetting.
@@ -1774,25 +1803,92 @@ var BrowserApp = {
 
         // Ensure that this choice is immediately persisted, because
         // Gecko won't be told again if it forgets.
         Services.prefs.savePrefFile(null);
 
         // 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:
         dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
         break;
 
     }
   },
 
+  /**
+   * 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");
     return this.defaultBrowserWidth = width;
   },
 
   get layersTileWidth() {
     delete this.layersTileWidth;