merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 03 Mar 2016 11:53:32 +0100
changeset 324684 7e43bdd93e439b8c8d6e62d48d9de3e293655560
parent 324650 4ea7408b3eef059aa248f4b00328f8fdb4475112 (current diff)
parent 324683 afa990f7034a12b78e1cba02b532a254c2d1990f (diff)
child 324814 2b5237c178ea02133a777396c24dd2b713f2b8ee
push id1128
push userjlund@mozilla.com
push dateWed, 01 Jun 2016 01:31:59 +0000
treeherdermozilla-release@fe0d30de989d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone47.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
mobile/android/base/resources/drawable-hdpi/ic_url_bar_star.png
mobile/android/base/resources/drawable-xhdpi/ic_url_bar_star.png
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1374,54 +1374,47 @@ var BookmarkingUI = {
     options.maxResults = kMaxResults;
     let query = PlacesUtils.history.getNewQuery();
 
     while (aHeaderItem.nextSibling &&
            aHeaderItem.nextSibling.localName == "menuitem") {
       aHeaderItem.nextSibling.remove();
     }
 
-    PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
-                       .asyncExecuteLegacyQueries([query], 1, options, {
-      handleResult: function (aResultSet) {
-        let onItemCommand = function (aEvent) {
-          let item = aEvent.target;
-          openUILink(item.getAttribute("targetURI"), aEvent);
-          CustomizableUI.hidePanelForNode(item);
-        };
+    let onItemCommand = function (aEvent) {
+      let item = aEvent.target;
+      openUILink(item.getAttribute("targetURI"), aEvent);
+      CustomizableUI.hidePanelForNode(item);
+    };
 
-        let fragment = document.createDocumentFragment();
-        let row;
-        while ((row = aResultSet.getNextRow())) {
-          let uri = row.getResultByIndex(1);
-          let title = row.getResultByIndex(2);
-          let icon = row.getResultByIndex(6);
+    let fragment = document.createDocumentFragment();
+    let root = PlacesUtils.history.executeQuery(query, options).root;
+    root.containerOpen = true;
+    for (let i = 0; i < root.childCount; i++) {
+      let node = root.getChild(i);
+      let uri = node.uri;
+      let title = node.title;
+      let icon = node.icon;
 
-          let item =
-            document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
-                                     "menuitem");
-          item.setAttribute("label", title || uri);
-          item.setAttribute("targetURI", uri);
-          item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
-                                     extraCSSClass);
-          item.addEventListener("command", onItemCommand);
-          if (icon) {
-            let iconURL = "moz-anno:favicon:" + icon;
-            item.setAttribute("image", iconURL);
-          }
-          fragment.appendChild(item);
-        }
-        aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
-      },
-      handleError: function (aError) {
-        Cu.reportError("Error while attempting to show recent bookmarks: " + aError);
-      },
-      handleCompletion: function (aReason) {
-      },
-    });
+      let item =
+        document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+                                 "menuitem");
+      item.setAttribute("label", title || uri);
+      item.setAttribute("targetURI", uri);
+      item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
+                                 extraCSSClass);
+      item.addEventListener("command", onItemCommand);
+      if (icon) {
+        let iconURL = "moz-anno:favicon:" + icon;
+        item.setAttribute("image", iconURL);
+      }
+      fragment.appendChild(item);
+    }
+    root.containerOpen = false;
+    aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
   },
 
   /**
    * Handles star styling based on page proxy state changes.
    */
   onPageProxyStateChanged: function BUI_onPageProxyStateChanged(aState) {
     if (!this._shouldUpdateStarState() || !this.star) {
       return;
--- a/browser/base/content/contentSearchUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -247,17 +247,18 @@ ContentSearchUIController.prototype = {
   search: function (aEvent) {
     if (!this.defaultEngine) {
       return; // Not initialized yet.
     }
 
     let searchText = this.input;
     let searchTerms;
     if (this._table.hidden ||
-        aEvent.originalTarget.id == "contentSearchDefaultEngineHeader") {
+        aEvent.originalTarget.id == "contentSearchDefaultEngineHeader" ||
+        aEvent instanceof KeyboardEvent) {
       searchTerms = searchText.value;
     }
     else {
       searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value;
     }
     // Send an event that will perform a search and Firefox Health Report will
     // record that a search from the healthReportKey passed to the constructor.
     let eventData = {
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -285,17 +285,16 @@ tags = mcb
 tags = mcb
 [browser_bug970746.js]
 [browser_bug1015721.js]
 skip-if = os == 'win' || e10s # Bug 1159268 - Need a content-process safe version of synthesizeWheel
 [browser_bug1064280_changeUrlInPinnedTab.js]
 [browser_bug1070778.js]
 [browser_accesskeys.js]
 [browser_canonizeURL.js]
-skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only
 [browser_clipboard.js]
 [browser_contentAreaClick.js]
 [browser_contextmenu.js]
 skip-if = toolkit == "gtk2" || toolkit == "gtk3" # disabled on Linux due to bug 513558
 [browser_ctrlTab.js]
 [browser_datachoices_notification.js]
 skip-if = !datareporting
 [browser_devedition.js]
--- a/browser/base/content/test/general/browser_canonizeURL.js
+++ b/browser/base/content/test/general/browser_canonizeURL.js
@@ -1,13 +1,8 @@
-function test() {
-  waitForExplicitFinish();
-  testNext();
-}
-
 var pairs = [
   ["example", "http://www.example.net/"],
   ["ex-ample", "http://www.ex-ample.net/"],
   ["  example ", "http://www.example.net/"],
   [" example/foo ", "http://www.example.net/foo"],
   [" example/foo bar ", "http://www.example.net/foo%20bar"],
   ["example.net", "http://example.net/"],
   ["http://example", "http://example/"],
@@ -15,42 +10,61 @@ var pairs = [
   ["ex-ample.foo", "http://ex-ample.foo/"],
   ["example.foo/bar ", "http://example.foo/bar"],
   ["1.1.1.1", "http://1.1.1.1/"],
   ["ftp://example", "ftp://example/"],
   ["ftp.example.bar", "ftp://ftp.example.bar/"],
   ["ex ample", Services.search.defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec],
 ];
 
-function testNext() {
-  if (!pairs.length) {
-    finish();
-    return;
-  }
+add_task(function*() {
+  for (let [inputValue, expectedURL] of pairs) {
+    let focusEventPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+    let messagePromise = BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager,
+                                                         "browser_canonizeURL:start");
 
-  let [inputValue, expectedURL] = pairs.shift();
+    let stoppedLoadPromise = ContentTask.spawn(gBrowser.selectedBrowser, [inputValue, expectedURL],
+      function([inputValue, expectedURL]) {
+        return new Promise(resolve => {
+          let wpl = {
+            onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+              if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
+                  aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+                if (!aRequest || !(aRequest instanceof Ci.nsIChannel)) {
+                  return;
+                }
+                aRequest.QueryInterface(Ci.nsIChannel);
+                is(aRequest.originalURI.spec, expectedURL,
+                   "entering '" + inputValue + "' loads expected URL");
 
-  gBrowser.addProgressListener({
-    onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
-        is(aRequest.originalURI.spec, expectedURL,
-           "entering '" + inputValue + "' loads expected URL");
+                webProgress.removeProgressListener(filter);
+                filter.removeProgressListener(wpl);
+                docShell.QueryInterface(Ci.nsIWebNavigation);
+                docShell.stop(docShell.STOP_ALL);
+                resolve();
+              }
+            },
+          };
+          let filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+                           .createInstance(Ci.nsIWebProgress);
+          filter.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL);
 
-        gBrowser.removeProgressListener(this);
-        gBrowser.stop();
-
-        executeSoon(testNext);
+          let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                    .getInterface(Ci.nsIWebProgress);
+          webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+          // We're sending this off to trigger the start of the this test, when all the
+          // listeners are in place:
+          sendAsyncMessage("browser_canonizeURL:start");
+        });
       }
-    }
-  });
+    );
 
-  gURLBar.addEventListener("focus", function onFocus() {
-    gURLBar.removeEventListener("focus", onFocus);
+    gBrowser.selectedBrowser.focus();
+    gURLBar.focus();
+
+    yield Promise.all([focusEventPromise, messagePromise]);
+
     gURLBar.inputField.value = inputValue.slice(0, -1);
     EventUtils.synthesizeKey(inputValue.slice(-1) , {});
     EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
-  });
-
-  gBrowser.selectedBrowser.focus();
-  gURLBar.focus();
-
-}
+    yield stoppedLoadPromise;
+  }
+});
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -100,17 +100,17 @@
         <description class="identity-popup-connection-not-secure"
                      value="&identity.connectionNotSecure;"
                      when-connection="not-secure secure-cert-user-overridden"/>
         <description class="identity-popup-connection-secure"
                      value="&identity.connectionSecure;"
                      when-connection="secure secure-ev"/>
       </vbox>
 
-      <vbox id="identity-popup-securityView-body">
+      <vbox id="identity-popup-securityView-body" flex="1">
         <!-- (EV) Certificate Information -->
         <description id="identity-popup-content-verified-by"
                      when-connection="secure-ev">&identity.connectionVerified1;</description>
         <description id="identity-popup-content-owner"
                      when-connection="secure-ev"
                      class="header"/>
         <description id="identity-popup-content-supplemental"
                      when-connection="secure-ev"/>
@@ -163,17 +163,19 @@
         <button when-mixedcontent="active-blocked"
                 label="&identity.disableMixedContentBlocking.label;"
                 accesskey="&identity.disableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.disableMixedContentProtection()"/>
         <button when-mixedcontent="active-loaded"
                 label="&identity.enableMixedContentBlocking.label;"
                 accesskey="&identity.enableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.enableMixedContentProtection()"/>
+      </vbox>
 
+      <vbox id="identity-popup-securityView-footer">
         <!-- More Security Information -->
         <button label="&identity.moreInfoLinkText2;"
                 oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
       </vbox>
 
     </panelview>
   </panelmultiview>
 </panel>
--- a/browser/components/distribution.js
+++ b/browser/components/distribution.js
@@ -341,72 +341,111 @@ DistributionCustomizer.prototype = {
         partnerAbout = this._ini.getString("Global", "about");
       }
       defaults.set("distribution.about", partnerAbout);
     } catch (e) {
       /* ignore bad prefs due to bug 895473 and move on */
       Cu.reportError(e);
     }
 
+    var usedPreferences = [];
+
+    if (sections["Preferences-" + this._locale]) {
+      for (let key of enumerate(this._ini.getKeys("Preferences-" + this._locale))) {
+        try {
+          let value = this._ini.getString("Preferences-" + this._locale, key);
+          if (value) {
+            Preferences.set(key, parseValue(value));
+          }
+          usedPreferences.push(key);
+        } catch (e) { /* ignore bad prefs and move on */ }
+      }
+    }
+
+    if (sections["Preferences-" + this._language]) {
+      for (let key of enumerate(this._ini.getKeys("Preferences-" + this._language))) {
+        if (usedPreferences.indexOf(key) > -1) {
+          continue;
+        }
+        try {
+          let value = this._ini.getString("Preferences-" + this._language, key);
+          if (value) {
+            Preferences.set(key, parseValue(value));
+          }
+          usedPreferences.push(key);
+        } catch (e) { /* ignore bad prefs and move on */ }
+      }
+    }
+
     if (sections["Preferences"]) {
       for (let key of enumerate(this._ini.getKeys("Preferences"))) {
+        if (usedPreferences.indexOf(key) > -1) {
+          continue;
+        }
         try {
-          let value = parseValue(this._ini.getString("Preferences", key));
-          Preferences.set(key, value);
+          let value = this._ini.getString("Preferences", key);
+          if (value) {
+            value = value.replace(/%LOCALE%/g, this._locale);
+            value = value.replace(/%LANGUAGE%/g, this._language);
+            Preferences.set(key, parseValue(value));
+          }
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
     let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"].
       createInstance(Ci.nsIPrefLocalizedString);
 
     var usedLocalizablePreferences = [];
 
     if (sections["LocalizablePreferences-" + this._locale]) {
       for (let key of enumerate(this._ini.getKeys("LocalizablePreferences-" + this._locale))) {
         try {
-          let value = parseValue(this._ini.getString("LocalizablePreferences-" + this._locale, key));
-          if (value !== undefined) {
+          let value = this._ini.getString("LocalizablePreferences-" + this._locale, key);
+          if (value) {
+            value = parseValue(value);
             localizedStr.data = "data:text/plain," + key + "=" + value;
             defaults._prefBranch.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr);
           }
           usedLocalizablePreferences.push(key);
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
     if (sections["LocalizablePreferences-" + this._language]) {
       for (let key of enumerate(this._ini.getKeys("LocalizablePreferences-" + this._language))) {
         if (usedLocalizablePreferences.indexOf(key) > -1) {
           continue;
         }
         try {
-          let value = parseValue(this._ini.getString("LocalizablePreferences-" + this._language, key));
-          if (value !== undefined) {
+          let value = this._ini.getString("LocalizablePreferences-" + this._language, key);
+          if (value) {
+            value = parseValue(value);
             localizedStr.data = "data:text/plain," + key + "=" + value;
             defaults._prefBranch.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr);
           }
           usedLocalizablePreferences.push(key);
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
     if (sections["LocalizablePreferences"]) {
       for (let key of enumerate(this._ini.getKeys("LocalizablePreferences"))) {
         if (usedLocalizablePreferences.indexOf(key) > -1) {
           continue;
         }
         try {
-          let value = parseValue(this._ini.getString("LocalizablePreferences", key));
-          if (value !== undefined) {
+          let value = this._ini.getString("LocalizablePreferences", key);
+          if (value) {
+            value = parseValue(value);
             value = value.replace(/%LOCALE%/g, this._locale);
             value = value.replace(/%LANGUAGE%/g, this._language);
             localizedStr.data = "data:text/plain," + key + "=" + value;
-            defaults._prefBranch.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr);
           }
+          defaults._prefBranch.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr);
         } catch (e) { /* ignore bad prefs and move on */ }
       }
     }
 
     return this._checkCustomizationComplete();
   },
 
   _checkCustomizationComplete: function DIST__checkCustomizationComplete() {
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -120,16 +120,29 @@ global.IconDetails = {
 };
 
 global.makeWidgetId = id => {
   id = id.toLowerCase();
   // FIXME: This allows for collisions.
   return id.replace(/[^a-z0-9_-]/g, "_");
 };
 
+function promisePopupShown(popup) {
+  return new Promise(resolve => {
+    if (popup.state == "open") {
+      resolve();
+    } else {
+      popup.addEventListener("popupshown", function onPopupShown(event) {
+        popup.removeEventListener("popupshown", onPopupShown);
+        resolve();
+      });
+    }
+  });
+}
+
 class BasePopup {
   constructor(extension, viewNode, popupURL) {
     let popupURI = Services.io.newURI(popupURL, null, extension.baseURI);
 
     Services.scriptSecurityManager.checkLoadURIWithPrincipal(
       extension.principal, popupURI,
       Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
@@ -249,16 +262,20 @@ class BasePopup {
       this.browser.addEventListener("load", this, true);
       this.browser.addEventListener("DOMTitleChanged", this, true);
       this.browser.addEventListener("DOMWindowClose", this, true);
     });
   }
 
   // Resizes the browser to match the preferred size of the content.
   resizeBrowser() {
+    if (!this.browser) {
+      return;
+    }
+
     let width, height;
     try {
       let w = {}, h = {};
       this.browser.docShell.contentViewer.getContentSize(w, h);
 
       width = w.value / this.window.devicePixelRatio;
       height = h.value / this.window.devicePixelRatio;
 
@@ -305,17 +322,22 @@ global.PanelPopup = class PanelPopup ext
   }
 
   destroy() {
     super.destroy();
     this.viewNode.remove();
   }
 
   closePopup() {
-    this.viewNode.hidePopup();
+    promisePopupShown(this.viewNode).then(() => {
+      // Make sure we're not already destroyed.
+      if (this.viewNode) {
+        this.viewNode.hidePopup();
+      }
+    });
   }
 };
 
 global.ViewPopup = class ViewPopup extends BasePopup {
   get DESTROY_EVENT() {
     return "ViewHiding";
   }
 
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js
@@ -1,35 +1,42 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 function* testInArea(area) {
+  let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`;
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "background": {
         "page": "data/background.html",
       },
       "browser_action": {
         "default_popup": "popup-a.html",
       },
     },
 
     files: {
-      "popup-a.html": `<script src="popup-a.js"></script>`,
+      "popup-a.html": scriptPage("popup-a.js"),
       "popup-a.js": function() {
         browser.runtime.sendMessage("from-popup-a");
+        browser.runtime.onMessage.addListener(msg => {
+          if (msg == "close-popup") {
+            window.close();
+          }
+        });
       },
 
-      "data/popup-b.html": `<script src="popup-b.js"></script>`,
+      "data/popup-b.html": scriptPage("popup-b.js"),
       "data/popup-b.js": function() {
         browser.runtime.sendMessage("from-popup-b");
       },
 
-      "data/background.html": `<script src="background.js"></script>`,
+      "data/background.html": scriptPage("background.js"),
 
       "data/background.js": function() {
         let sendClick;
         let tests = [
           () => {
             sendClick({expectEvent: false, expectPopup: "a"});
           },
           () => {
@@ -46,50 +53,65 @@ function* testInArea(area) {
             browser.browserAction.setPopup({popup: ""});
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
             browser.browserAction.setPopup({popup: "/popup-a.html"});
-            sendClick({expectEvent: false, expectPopup: "a"});
+            sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
+          },
+          () => {
+            browser.test.sendMessage("next-test", {expectClosed: true});
           },
         ];
 
         let expect = {};
-        sendClick = ({expectEvent, expectPopup}) => {
-          expect = {event: expectEvent, popup: expectPopup};
+        sendClick = ({expectEvent, expectPopup, runNextTest}) => {
+          expect = {event: expectEvent, popup: expectPopup, runNextTest};
           browser.test.sendMessage("send-click");
         };
 
         browser.runtime.onMessage.addListener(msg => {
-          if (expect.popup) {
+          if (msg == "close-popup") {
+            return;
+          } else if (expect.popup) {
             browser.test.assertEq(msg, `from-popup-${expect.popup}`,
                                   "expected popup opened");
           } else {
-            browser.test.fail("unexpected popup");
+            browser.test.fail(`unexpected popup: ${msg}`);
           }
 
           expect.popup = null;
-          browser.test.sendMessage("next-test");
+          if (expect.runNextTest) {
+            expect.runNextTest = false;
+            tests.shift()();
+          } else {
+            browser.test.sendMessage("next-test");
+          }
         });
 
         browser.browserAction.onClicked.addListener(() => {
           if (expect.event) {
             browser.test.succeed("expected click event received");
           } else {
             browser.test.fail("unexpected click event");
           }
 
           expect.event = false;
           browser.test.sendMessage("next-test");
         });
 
         browser.test.onMessage.addListener((msg) => {
+          if (msg == "close-popup") {
+            browser.runtime.sendMessage("close-popup");
+            return;
+          }
+
           if (msg != "next-test") {
             browser.test.fail("Expecting 'next-test' message");
           }
 
           if (tests.length) {
             let test = tests.shift();
             test();
           } else {
@@ -102,23 +124,33 @@ function* testInArea(area) {
     },
   });
 
   extension.onMessage("send-click", () => {
     clickBrowserAction(extension);
   });
 
   let widget;
-  extension.onMessage("next-test", Task.async(function* () {
+  extension.onMessage("next-test", Task.async(function* (expecting = {}) {
     if (!widget) {
       widget = getBrowserActionWidget(extension);
       CustomizableUI.addWidgetToArea(widget.id, area);
     }
+    if (expecting.expectClosed) {
+      let panel = getBrowserActionPopup(extension);
+      ok(panel, "Expect panel to exist");
+      yield promisePopupShown(panel);
 
-    yield closeBrowserAction(extension);
+      extension.sendMessage("close-popup");
+
+      yield promisePopupHidden(panel);
+      ok(true, "Panel is closed");
+    } else {
+      yield closeBrowserAction(extension);
+    }
 
     extension.sendMessage("next-test");
   }));
 
   yield Promise.all([extension.startup(), extension.awaitFinish("browseraction-tests-done")]);
 
   yield extension.unload();
 
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -14,16 +14,21 @@ add_task(function* testPageActionPopup()
         "default_popup": "popup-a.html",
       },
     },
 
     files: {
       "popup-a.html": scriptPage("popup-a.js"),
       "popup-a.js": function() {
         browser.runtime.sendMessage("from-popup-a");
+        browser.runtime.onMessage.addListener(msg => {
+          if (msg == "close-popup") {
+            window.close();
+          }
+        });
       },
 
       "data/popup-b.html": scriptPage("popup-b.js"),
       "data/popup-b.js": function() {
         browser.runtime.sendMessage("from-popup-b");
       },
 
       "data/background.html": scriptPage("background.js"),
@@ -50,50 +55,65 @@ add_task(function* testPageActionPopup()
             browser.pageAction.setPopup({tabId, popup: ""});
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
             sendClick({expectEvent: true, expectPopup: null});
           },
           () => {
             browser.pageAction.setPopup({tabId, popup: "/popup-a.html"});
-            sendClick({expectEvent: false, expectPopup: "a"});
+            sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
+          },
+          () => {
+            browser.test.sendMessage("next-test", {expectClosed: true});
           },
         ];
 
         let expect = {};
-        sendClick = ({expectEvent, expectPopup}) => {
-          expect = {event: expectEvent, popup: expectPopup};
+        sendClick = ({expectEvent, expectPopup, runNextTest}) => {
+          expect = {event: expectEvent, popup: expectPopup, runNextTest};
           browser.test.sendMessage("send-click");
         };
 
         browser.runtime.onMessage.addListener(msg => {
-          if (expect.popup) {
+          if (msg == "close-popup") {
+            return;
+          } else if (expect.popup) {
             browser.test.assertEq(msg, `from-popup-${expect.popup}`,
                                   "expected popup opened");
           } else {
-            browser.test.fail("unexpected popup");
+            browser.test.fail(`unexpected popup: ${msg}`);
           }
 
           expect.popup = null;
-          browser.test.sendMessage("next-test");
+          if (expect.runNextTest) {
+            expect.runNextTest = false;
+            tests.shift()();
+          } else {
+            browser.test.sendMessage("next-test");
+          }
         });
 
         browser.pageAction.onClicked.addListener(() => {
           if (expect.event) {
             browser.test.succeed("expected click event received");
           } else {
             browser.test.fail("unexpected click event");
           }
 
           expect.event = false;
           browser.test.sendMessage("next-test");
         });
 
         browser.test.onMessage.addListener((msg) => {
+          if (msg == "close-popup") {
+            browser.runtime.sendMessage("close-popup");
+            return;
+          }
+
           if (msg != "next-test") {
             browser.test.fail("Expecting 'next-test' message");
           }
 
           if (tests.length) {
             let test = tests.shift();
             test();
           } else {
@@ -113,22 +133,32 @@ add_task(function* testPageActionPopup()
 
   let pageActionId = makeWidgetId(extension.id) + "-page-action";
   let panelId = makeWidgetId(extension.id) + "-panel";
 
   extension.onMessage("send-click", () => {
     clickPageAction(extension);
   });
 
-  extension.onMessage("next-test", Task.async(function* () {
+  extension.onMessage("next-test", Task.async(function* (expecting = {}) {
     let panel = document.getElementById(panelId);
-    if (panel) {
+    if (expecting.expectClosed) {
+      ok(panel, "Expect panel to exist");
+      yield promisePopupShown(panel);
+
+      extension.sendMessage("close-popup");
+
+      yield promisePopupHidden(panel);
+      ok(true, `Panel is closed`);
+    } else if (panel) {
       yield promisePopupShown(panel);
       panel.hidePopup();
+    }
 
+    if (panel) {
       panel = document.getElementById(panelId);
       is(panel, null, "panel successfully removed from document after hiding");
     }
 
     extension.sendMessage("next-test");
   }));
 
 
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
@@ -133,16 +133,19 @@ add_task(function* testWebNavigationFram
     getAllFramesDetails,
     getFrameResults,
   } = yield extension.awaitMessage("webNavigationFrames.done");
 
   is(getAllFramesDetails.length, 3, "expected number of frames found");
   is(getAllFramesDetails.length, collectedDetails.length,
      "number of frames found should equal the number onCompleted events collected");
 
+  is(getAllFramesDetails[0].frameId, 0, "the root frame has the expected frameId");
+  is(getAllFramesDetails[0].parentFrameId, -1, "the root frame has the expected parentFrameId");
+
   // ordered by frameId
   let sortByFrameId = (el1, el2) => {
     let val1 = el1 ? el1.frameId : -1;
     let val2 = el2 ? el2.frameId : -1;
     return val1 - val2;
   };
 
   collectedDetails = collectedDetails.sort(sortByFrameId);
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -2,17 +2,17 @@
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* exported CustomizableUI makeWidgetId focusWindow forceGC
  *          getBrowserActionWidget
  *          clickBrowserAction clickPageAction
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
- *          promisePopupShown
+ *          promisePopupShown promisePopupHidden
  */
 
 var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
 var {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm");
 
 // Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
 // times in debug builds, which results in intermittent timeouts. Until we have
 // a better solution, we force a GC after certain strategic tests, which tend to
@@ -54,25 +54,37 @@ function promisePopupShown(popup) {
         popup.removeEventListener("popupshown", onPopupShown);
         resolve();
       };
       popup.addEventListener("popupshown", onPopupShown);
     }
   });
 }
 
+function promisePopupHidden(popup) {
+  return new Promise(resolve => {
+    let onPopupHidden = event => {
+      popup.removeEventListener("popuphidden", onPopupHidden);
+      resolve();
+    };
+    popup.addEventListener("popuphidden", onPopupHidden);
+  });
+}
+
 function getBrowserActionWidget(extension) {
   return CustomizableUI.getWidget(makeWidgetId(extension.id) + "-browser-action");
 }
 
 function getBrowserActionPopup(extension, win = window) {
   let group = getBrowserActionWidget(extension);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     return win.document.getElementById("customizationui-widget-panel");
+  } else {
+    return win.PanelUI.panel;
   }
   return null;
 }
 
 var clickBrowserAction = Task.async(function* (extension, win = window) {
   let group = getBrowserActionWidget(extension);
   let widget = group.forWindow(win);
 
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -41,15 +41,14 @@ skip-if = e10s # Bug ?????? - test fails
 [browser_library_left_pane_select_hierarchy.js]
 [browser_library_middleclick.js]
 [browser_library_open_leak.js]
 [browser_library_openFlatContainer.js]
 [browser_library_panel_leak.js]
 [browser_library_search.js]
 [browser_library_views_liveupdate.js]
 [browser_markPageAsFollowedLink.js]
-skip-if = e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly (test does EventUtils.sendMouseEvent...)
 [browser_sidebarpanels_click.js]
 skip-if = true # temporarily disabled for breaking the treeview - bug 658744
 [browser_sort_in_library.js]
 [browser_toolbar_migration.js]
 [browser_toolbarbutton_menu_context.js]
 [browser_views_liveupdate.js]
--- a/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
+++ b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
@@ -1,86 +1,67 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
 /**
  * Tests that visits across frames are correctly represented in the database.
  */
 
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser";
 const PAGE_URL = BASE_URL + "/framedPage.html";
 const LEFT_URL = BASE_URL + "/frameLeft.html";
 const RIGHT_URL = BASE_URL + "/frameRight.html";
 
-var gTabLoaded = false;
-var gLeftFrameVisited = false;
+add_task(function* test() {
+  // We must wait for both frames to be loaded and the visits to be registered.
+  let deferredLeftFrameVisit = PromiseUtils.defer();
+  let deferredRightFrameVisit = PromiseUtils.defer();
 
-var observer = {
-  observe: function(aSubject, aTopic, aData)
-  {
-    let url = aSubject.QueryInterface(Ci.nsIURI).spec;
-    if (url == LEFT_URL ) {
-      is(getTransitionForUrl(url), null,
-         "Embed visits should not get a database entry.");
-      gLeftFrameVisited = true;
-      maybeClickLink();
-    }
-    else if (url == RIGHT_URL ) {
-      is(getTransitionForUrl(url), PlacesUtils.history.TRANSITION_FRAMED_LINK,
-         "User activated visits should get a FRAMED_LINK transition.");
-      finish();
-    }
-  },
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
-};
-Services.obs.addObserver(observer, "uri-visit-saved", false);
+  Services.obs.addObserver(function observe(subject) {
+    Task.spawn(function* () {
+      let url = subject.QueryInterface(Ci.nsIURI).spec;
+      if (url == LEFT_URL ) {
+        is((yield getTransitionForUrl(url)), null,
+           "Embed visits should not get a database entry.");
+        deferredLeftFrameVisit.resolve();
+      }
+      else if (url == RIGHT_URL ) {
+        is((yield getTransitionForUrl(url)),
+           PlacesUtils.history.TRANSITION_FRAMED_LINK,
+           "User activated visits should get a FRAMED_LINK transition.");
+        Services.obs.removeObserver(observe, "uri-visit-saved");
+        deferredRightFrameVisit.resolve();
+      }
+    });
+  }, "uri-visit-saved", false);
+
+  // Open a tab and wait for all the subframes to load.
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
 
-function test()
-{
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab(PAGE_URL);
-  let frameCount = 0;
-  gBrowser.selectedBrowser.addEventListener("DOMContentLoaded",
-    function (event)
-    {
-      // Wait for all the frames.
-      if (frameCount++ < 2)
-        return;
-      gBrowser.selectedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, false)
-      gTabLoaded = true;
-      maybeClickLink();
-    }, false
-  );
-}
+  // Wait for the left frame visit to be registered.
+  info("Waiting left frame visit");
+  yield deferredLeftFrameVisit.promise;
+
+  // Click on the link in the left frame to cause a page load in the
+  // right frame.
+  info("Clicking link");
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+    content.frames[0].document.getElementById("clickme").click();
+  });
+
+  // Wait for the right frame visit to be registered.
+  info("Waiting right frame visit");
+  yield deferredRightFrameVisit.promise;
 
-function maybeClickLink() {
-  if (gTabLoaded && gLeftFrameVisited) {
-    // Click on the link in the left frame to cause a page load in the
-    // right frame.
-    EventUtils.sendMouseEvent({type: "click"}, "clickme", content.frames[0]);
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+function* getTransitionForUrl(url) {
+  // Ensure all the transactions completed.
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield db.execute(`
+    SELECT visit_type
+    FROM moz_historyvisits
+    WHERE place_id = (SELECT id FROM moz_places WHERE url = :url)`,
+    { url });
+  if (rows.length) {
+    return rows[0].getResultByName("visit_type");
   }
+  return null;
 }
-
-function getTransitionForUrl(aUrl)
-{
-  let dbConn = PlacesUtils.history
-                          .QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
-  let stmt = dbConn.createStatement(
-    "SELECT visit_type FROM moz_historyvisits WHERE place_id = " +
-      "(SELECT id FROM moz_places WHERE url = :page_url)");
-  stmt.params.page_url = aUrl;
-  try {
-    if (!stmt.executeStep()) {
-      return null;
-    }
-    return stmt.row.visit_type;
-  }
-  finally {
-    stmt.finalize();
-  }
-}
-
-registerCleanupFunction(function ()
-{
-  gBrowser.removeTab(gBrowser.selectedTab);
-  Services.obs.removeObserver(observer, "uri-visit-saved");
-})
--- a/browser/components/tests/unit/distribution.ini
+++ b/browser/components/tests/unit/distribution.ini
@@ -8,16 +8,38 @@ about=Test distribution file
 about.en-US=Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè
 
 [Preferences]
 distribution.test.string="Test String"
 distribution.test.string.noquotes=Test String
 distribution.test.int=777
 distribution.test.bool.true=true
 distribution.test.bool.false=false
+distribution.test.empty=
+
+distribution.test.pref.locale="%LOCALE%"
+distribution.test.pref.language.reset="Preference Set"
+distribution.test.pref.locale.reset="Preference Set"
+distribution.test.pref.locale.set="Preference Set"
+distribution.test.pref.language.set="Preference Set"
+
+[Preferences-en]
+distribution.test.pref.language.en="en"
+distribution.test.pref.language.reset=
+distribution.test.pref.language.set="Language Set"
+distribution.test.pref.locale.set="Language Set"
+
+[Preferences-en-US]
+distribution.test.pref.locale.en-US="en-US"
+distribution.test.pref.locale.reset=
+distribution.test.pref.locale.set="Locale Set"
+
+
+[Preferences-de]
+distribution.test.pref.language.de="de"
 
 [LocalizablePreferences]
 distribution.test.locale="%LOCALE%"
 distribution.test.language.reset="Preference Set"
 distribution.test.locale.reset="Preference Set"
 distribution.test.locale.set="Preference Set"
 distribution.test.language.set="Preference Set"
 
@@ -28,9 +50,9 @@ distribution.test.language.set="Language
 distribution.test.locale.set="Language Set"
 
 [LocalizablePreferences-en-US]
 distribution.test.locale.en-US="en-US"
 distribution.test.locale.reset=
 distribution.test.locale.set="Locale Set"
 
 [LocalizablePreferences-de]
-distribution.test.locale.de="de"
+distribution.test.language.de="de"
--- a/browser/components/tests/unit/test_distribution.js
+++ b/browser/components/tests/unit/test_distribution.js
@@ -66,20 +66,40 @@ add_task(function* () {
   Assert.equal(Services.prefs.getCharPref("distribution.version"), "1.0");
   Assert.equal(Services.prefs.getComplexValue("distribution.about", Ci.nsISupportsString).data, "Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè");
 
   Assert.equal(Services.prefs.getCharPref("distribution.test.string"), "Test String");
   Assert.equal(Services.prefs.getCharPref("distribution.test.string.noquotes"), "Test String");
   Assert.equal(Services.prefs.getIntPref("distribution.test.int"), 777);
   Assert.equal(Services.prefs.getBoolPref("distribution.test.bool.true"), true);
   Assert.equal(Services.prefs.getBoolPref("distribution.test.bool.false"), false);
+
+  Assert.throws(() => Services.prefs.getCharPref("distribution.test.empty"));
+  Assert.throws(() => Services.prefs.getIntPref("distribution.test.empty"));
+  Assert.throws(() => Services.prefs.getBoolPref("distribution.test.empty"));
+
+  Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale"), "en-US");
+  Assert.equal(Services.prefs.getCharPref("distribution.test.pref.language.en"), "en");
+  Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale.en-US"), "en-US");
+  Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.language.de"));
+  // This value was never set because of the empty language specific pref
+  Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.language.reset"));
+  // This value was never set because of the empty locale specific pref
+  Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.locale.reset"));
+  // This value was overridden by a locale specific setting
+  Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale.set"), "Locale Set");
+  // This value was overridden by a language specific setting
+  Assert.equal(Services.prefs.getCharPref("distribution.test.pref.language.set"), "Language Set");
+  // Language should not override locale
+  Assert.notEqual(Services.prefs.getCharPref("distribution.test.pref.locale.set"), "Language Set");
+
   Assert.equal(Services.prefs.getComplexValue("distribution.test.locale", Ci.nsIPrefLocalizedString).data, "en-US");
   Assert.equal(Services.prefs.getComplexValue("distribution.test.language.en", Ci.nsIPrefLocalizedString).data, "en");
   Assert.equal(Services.prefs.getComplexValue("distribution.test.locale.en-US", Ci.nsIPrefLocalizedString).data, "en-US");
-  Assert.throws(() => Services.prefs.getComplexValue("distribution.test.locale.de", Ci.nsIPrefLocalizedString));
+  Assert.throws(() => Services.prefs.getComplexValue("distribution.test.language.de", Ci.nsIPrefLocalizedString));
   // This value was never set because of the empty language specific pref
   Assert.throws(() => Services.prefs.getComplexValue("distribution.test.language.reset", Ci.nsIPrefLocalizedString));
   // This value was never set because of the empty locale specific pref
   Assert.throws(() => Services.prefs.getComplexValue("distribution.test.locale.reset", Ci.nsIPrefLocalizedString));
   // This value was overridden by a locale specific setting
   Assert.equal(Services.prefs.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Locale Set");
   // This value was overridden by a language specific setting
   Assert.equal(Services.prefs.getComplexValue("distribution.test.language.set", Ci.nsIPrefLocalizedString).data, "Language Set");
--- a/browser/modules/ContentClick.jsm
+++ b/browser/modules/ContentClick.jsm
@@ -56,31 +56,31 @@ var ContentClick = {
                                                      , "location"
                                                      , "keyword" ]
                                        }, window);
       return;
     }
 
     // Note: We don't need the sidebar code here.
 
+    // Mark the page as a user followed link.  This is done so that history can
+    // distinguish automatic embed visits from user activated ones.  For example
+    // pages loaded in frames are embed visits and lost with the session, while
+    // visits across frames should be preserved.
+    try {
+      if (!PrivateBrowsingUtils.isWindowPrivate(window))
+        PlacesUIUtils.markPageAsFollowedLink(json.href);
+    } catch (ex) { /* Skip invalid URIs. */ }
+
     // This part is based on handleLinkClick.
     var where = window.whereToOpenLink(json);
     if (where == "current")
       return;
 
     // Todo(903022): code for where == save
 
     let params = { charset: browser.characterSet,
                    referrerURI: browser.documentURI,
                    referrerPolicy: json.referrerPolicy,
                    noReferrer: json.noReferrer };
     window.openLinkIn(json.href, where, params);
-
-    // Mark the page as a user followed link.  This is done so that history can
-    // distinguish automatic embed visits from user activated ones.  For example
-    // pages loaded in frames are embed visits and lost with the session, while
-    // visits across frames should be preserved.
-    try {
-      if (!PrivateBrowsingUtils.isWindowPrivate(window))
-        PlacesUIUtils.markPageAsFollowedLink(json.href);
-    } catch (ex) { /* Skip invalid URIs. */ }
   }
 };
--- a/browser/themes/osx/controlcenter/panel.css
+++ b/browser/themes/osx/controlcenter/panel.css
@@ -12,16 +12,25 @@
 .identity-popup-expander:-moz-focusring {
   padding: 2px;
 }
 
 .identity-popup-expander:-moz-focusring > .button-box {
   @hudButtonFocused@
 }
 
+#identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
+  border-bottom-right-radius: 3.5px;
+}
+
+#identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews:-moz-locale-dir(rtl) {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 3.5px;
+}
+
 #tracking-action-block,
 #tracking-action-unblock,
 #tracking-action-unblock-private,
 #identity-popup-securityView-body > button {
   @hudButton@
   min-height: 30px;
 }
 
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -78,39 +78,38 @@
 }
 
 #identity-popup-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
   transform: translateX(-100%);
 }
 
 #identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
   background: var(--panel-arrowcontent-background);
-  border-bottom-right-radius: 3.5px;
   padding: 0;
 }
 
-#identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews:-moz-locale-dir(rtl) {
-  border-bottom-right-radius: 0;
-  border-bottom-left-radius: 3.5px;
-}
-
 .identity-popup-section:not(:first-child) {
   border-top: 1px solid var(--panel-separator-color);
 }
 
 #identity-popup-securityView,
 #identity-popup-security-content,
 #identity-popup-permissions-content,
 #tracking-protection-content {
+  background-repeat: no-repeat;
+  background-position: 1em 1em;
+  background-size: 24px auto;
+}
+
+#identity-popup-security-content,
+#identity-popup-permissions-content,
+#tracking-protection-content {
   padding: 0.5em 0 1em;
   -moz-padding-start: calc(2em + 24px);
   -moz-padding-end: 1em;
-  background-repeat: no-repeat;
-  background-position: 1em 1em;
-  background-size: 24px auto;
 }
 
 #identity-popup-securityView:-moz-locale-dir(rtl),
 #identity-popup-security-content:-moz-locale-dir(rtl),
 #identity-popup-permissions-content:-moz-locale-dir(rtl),
 #tracking-protection-content:-moz-locale-dir(rtl) {
   background-position: calc(100% - 1em) 1em;
 }
@@ -203,17 +202,16 @@
   color: #418220;
 }
 
 .identity-popup-connection-not-secure {
   color: #d74345;
 }
 
 #identity-popup-securityView {
-  padding-bottom: 2em;
   overflow: hidden;
 }
 
 #identity-popup-securityView,
 #identity-popup-security-content {
   background-image: url(chrome://browser/skin/controlcenter/conn-not-secure.svg);
 }
 
@@ -247,25 +245,56 @@
   background-image: url(chrome://browser/skin/controlcenter/mcb-disabled.svg);
 }
 
 #identity-popup-security-descriptions > description {
   margin-top: 6px;
   color: Graytext;
 }
 
+#identity-popup-securityView-header,
+#identity-popup-securityView-body {
+  -moz-margin-start: calc(2em + 24px);
+  -moz-margin-end: 1em;
+}
+
 #identity-popup-securityView-header {
+  margin-top: 0.5em;
   border-bottom: 1px solid var(--panel-separator-color);
   padding-bottom: 1em;
 }
 
 #identity-popup-securityView-body {
   -moz-padding-end: 1em;
 }
 
+#identity-popup-securityView-footer {
+  margin-top: 1em;
+  background-color: hsla(210,4%,10%,.07);
+}
+
+#identity-popup-securityView-footer > button {
+  -moz-appearance: none;
+  margin: 0;
+  border: none;
+  border-top: 1px solid #ccc;
+  padding: 8px 20px;
+  color: ButtonText;
+  background-color: transparent;
+}
+
+#identity-popup-securityView-footer > button:hover,
+#identity-popup-securityView-footer > button:focus {
+  background-color: hsla(210,4%,10%,.07);
+}
+
+#identity-popup-securityView-footer > button:hover:active {
+  background-color: hsla(210,4%,10%,.12);
+}
+
 #identity-popup-content-verifier ~ description {
   margin-top: 1em;
   color: Graytext;
 }
 
 description#identity-popup-content-verified-by,
 description#identity-popup-content-owner,
 description#identity-popup-content-verifier,
--- a/devtools/bootstrap.js
+++ b/devtools/bootstrap.js
@@ -3,16 +3,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
+function actionOccurred(id) {
+  let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+  let Telemetry = require("devtools/client/shared/telemetry");;
+  let telemetry = new Telemetry();
+  telemetry.actionOccurred(id);
+}
+
 // Helper to listen to a key on all windows
 function MultiWindowKeyListener({ keyCode, ctrlKey, altKey, callback }) {
   let keyListener = function (event) {
     if (event.ctrlKey == !!ctrlKey &&
         event.altKey == !!altKey &&
         event.keyCode === keyCode) {
       callback(event);
 
@@ -145,25 +152,29 @@ function reload(event) {
     let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
     setTimeout(() => {
       let { TargetFactory } = devtools.require("devtools/client/framework/target");
       let { gDevTools } = devtools.require("devtools/client/framework/devtools");
       let target = TargetFactory.forTab(top.gBrowser.selectedTab);
       gDevTools.showToolbox(target);
     }, 1000);
   }
+
+  actionOccurred("reloadAddonReload");
 }
 
 let listener;
 function startup() {
   dump("DevTools addon started.\n");
   listener = new MultiWindowKeyListener({
     keyCode: Ci.nsIDOMKeyEvent.DOM_VK_R, ctrlKey: true, altKey: true,
     callback: reload
   });
   listener.start();
 }
 function shutdown() {
   listener.stop();
   listener = null;
 }
-function install() {}
+function install() {
+  actionOccurred("reloadAddonInstalled");
+}
 function uninstall() {}
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -77,17 +77,20 @@ const EVENTS = {
   // After the stackframes are cleared and debugger won't pause anymore.
   AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
 
   // When the options popup is showing or hiding.
   OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
   OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
 
   // When the widgets layout has been changed.
-  LAYOUT_CHANGED: "Debugger:LayoutChanged"
+  LAYOUT_CHANGED: "Debugger:LayoutChanged",
+
+  // When a worker has been selected.
+  WORKER_SELECTED: "Debugger::WorkerSelected"
 };
 
 // Descriptions for what a stack frame represents after the debugger pauses.
 const FRAME_TYPE = {
   NORMAL: 0,
   CONDITIONAL_BREAKPOINT_EVAL: 1,
   WATCH_EXPRESSIONS_EVAL: 2,
   PUBLIC_CLIENT_EVAL: 3
@@ -103,17 +106,17 @@ Cu.import("resource://devtools/client/sh
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 
 /**
  * Localization convenience methods.
  */
 var L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
 
 Cu.import("resource://devtools/client/shared/browser-loader.js");
-const require = BrowserLoader("resource://devtools/client/debugger/", this).require;
+const require = BrowserLoader("resource://devtools/client/debugger/", window).require;
 XPCOMUtils.defineConstant(this, "require", require);
 const { gDevTools } = require("devtools/client/framework/devtools");
 
 // React
 const React = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
@@ -486,39 +489,40 @@ Workers.prototype = {
     this._tabClient.listWorkers((response) => {
       let workerForms = Object.create(null);
       for (let worker of response.workers) {
         workerForms[worker.actor] = worker;
       }
 
       for (let workerActor in this._workerForms) {
         if (!(workerActor in workerForms)) {
+          DebuggerView.Workers.removeWorker(this._workerForms[workerActor]);
           delete this._workerForms[workerActor];
-          DebuggerView.Workers.removeWorker(workerActor);
         }
       }
 
       for (let workerActor in workerForms) {
         if (!(workerActor in this._workerForms)) {
           let workerForm = workerForms[workerActor];
           this._workerForms[workerActor] = workerForm;
-          DebuggerView.Workers.addWorker(workerActor, workerForm.url);
+          DebuggerView.Workers.addWorker(workerForm);
         }
       }
     });
   },
 
   _onWorkerListChanged: function () {
     this._updateWorkerList();
   },
 
-  _onWorkerSelect: function (workerActor) {
-    DebuggerController.client.attachWorker(workerActor, (response, workerClient) => {
-      gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
-                            "jsdebugger", Toolbox.HostType.WINDOW);
+  _onWorkerSelect: function (workerForm) {
+    DebuggerController.client.attachWorker(workerForm.actor, (response, workerClient) => {
+      let toolbox = gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+                                          "jsdebugger", Toolbox.HostType.WINDOW);
+      window.emit(EVENTS.WORKER_SELECTED, toolbox);
     });
   }
 };
 
 /**
  * ThreadState keeps the UI up to date with the state of the
  * thread (paused/attached/etc.).
  */
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -40,16 +40,19 @@ support-files =
   code_ugly-2.js
   code_ugly-3.js
   code_ugly-4.js
   code_ugly-5.js
   code_ugly-6.js
   code_ugly-7.js
   code_ugly-8
   code_ugly-8^headers^
+  code_worker-source-map.coffee
+  code_worker-source-map.js
+  code_worker-source-map.js.map
   code_WorkerActor.attach-worker1.js
   code_WorkerActor.attach-worker2.js
   code_WorkerActor.attachThread-worker.js
   doc_auto-pretty-print-01.html
   doc_auto-pretty-print-02.html
   doc_binary_search.html
   doc_blackboxing.html
   doc_blackboxing_unblackbox.html
@@ -107,16 +110,17 @@ support-files =
   doc_script-switching-02.html
   doc_split-console-paused-reload.html
   doc_step-many-statements.html
   doc_step-out.html
   doc_terminate-on-tab-close.html
   doc_watch-expressions.html
   doc_watch-expression-button.html
   doc_with-frame.html
+  doc_worker-source-map.html
   doc_WorkerActor.attach-tab1.html
   doc_WorkerActor.attach-tab2.html
   doc_WorkerActor.attachThread-tab.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
@@ -587,16 +591,18 @@ skip-if = e10s && debug
 [browser_dbg_variables-view-webidl.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-01.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-02.js]
 skip-if = e10s && debug
 [browser_dbg_worker-console.js]
 skip-if = e10s && debug
+[browser_dbg_worker-source-map.js]
+skip-if = e10s && debug
 [browser_dbg_worker-window.js]
 skip-if = e10s && debug
 [browser_dbg_WorkerActor.attach.js]
 skip-if = e10s && debug
 [browser_dbg_WorkerActor.attachThread.js]
 skip-if = e10s && debug
 [browser_dbg_split-console-keypress.js]
 skip-if = e10s && debug
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-source-map.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "doc_worker-source-map.html";
+const WORKER_URL = "code_worker-source-map.js";
+const COFFEE_URL = EXAMPLE_URL + "code_worker-source-map.coffee";
+
+function selectWorker(aPanel, aURL) {
+  let panelWin = aPanel.panelWin;
+  let promise = waitForDebuggerEvents(aPanel, panelWin.EVENTS.WORKER_SELECTED);
+  let Workers = panelWin.DebuggerView.Workers;
+  let item = Workers.getItemForAttachment((workerForm) => {
+    return workerForm.url === aURL;
+  });
+  Workers.selectedItem = item;
+  return promise;
+}
+
+function test() {
+  return Task.spawn(function* () {
+    yield pushPrefs(["devtools.debugger.workers", true]);
+
+    let [tab,, panel] = yield initDebugger(TAB_URL);
+    let toolbox = yield selectWorker(panel, WORKER_URL);
+    let workerPanel = toolbox.getCurrentPanel();
+    yield waitForSourceShown(workerPanel, ".coffee");
+    let panelWin = workerPanel.panelWin;
+    let Sources = panelWin.DebuggerView.Sources;
+    let editor = panelWin.DebuggerView.editor;
+    let threadClient = panelWin.gThreadClient;
+
+    isnot(Sources.selectedItem.attachment.source.url.indexOf(".coffee"), -1,
+          "The debugger should show the source mapped coffee source file.");
+    is(Sources.selectedValue.indexOf(".js"), -1,
+       "The debugger should not show the generated js source file.");
+    is(editor.getText().indexOf("isnt"), 211,
+       "The debugger's editor should have the coffee source source displayed.");
+    is(editor.getText().indexOf("function"), -1,
+       "The debugger's editor should not have the JS source displayed.");
+
+    yield threadClient.interrupt();
+    let sourceForm = getSourceForm(Sources, COFFEE_URL);
+    let source = threadClient.source(sourceForm);
+    let response = yield source.setBreakpoint({ line: 5 });
+
+    ok(!response.error,
+      "Should be able to set a breakpoint in a coffee source file.");
+    ok(!response.actualLocation,
+      "Should be able to set a breakpoint on line 5.");
+
+    let promise = new Promise((resolve) => {
+      threadClient.addOneTimeListener("paused", (event, packet) => {
+        is(packet.type, "paused",
+          "We should now be paused again.");
+        is(packet.why.type, "breakpoint",
+          "and the reason we should be paused is because we hit a breakpoint.");
+
+        // Check that we stopped at the right place, by making sure that the
+        // environment is in the state that we expect.
+        is(packet.frame.environment.bindings.variables.start.value, 0,
+           "'start' is 0.");
+        is(packet.frame.environment.bindings.variables.stop.value.type, "undefined",
+           "'stop' hasn't been assigned to yet.");
+        is(packet.frame.environment.bindings.variables.pivot.value.type, "undefined",
+           "'pivot' hasn't been assigned to yet.");
+
+        waitForCaretUpdated(workerPanel, 5).then(resolve);
+      });
+    });
+
+    // This will cause the breakpoint to be hit, and put us back in the
+    // paused state.
+    yield threadClient.resume();
+    callInTab(tab, "binary_search", [0, 2, 3, 5, 7, 10], 5);
+    yield promise;
+
+    yield threadClient.resume();
+    yield toolbox.destroy();
+    yield closeDebuggerAndFinish(panel);
+
+    yield popPrefs();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.coffee
@@ -0,0 +1,22 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+binary_search = (items, value) ->
+
+  start = 0
+  stop  = items.length - 1
+  pivot = Math.floor (start + stop) / 2
+
+  while items[pivot] isnt value and start < stop
+
+    # Adjust the search area.
+    stop  = pivot - 1 if value < items[pivot]
+    start = pivot + 1 if value > items[pivot]
+
+    # Recalculate the pivot.
+    pivot = Math.floor (stop + start) / 2
+
+  # Make sure we've found the correct value.
+  if items[pivot] is value then pivot else -1
+
+self.onmessage = (event) ->
+  data = event.data
+  binary_search(data.items, data.value)
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.js
@@ -0,0 +1,35 @@
+// Generated by CoffeeScript 1.10.0
+(function() {
+  var binary_search;
+
+  binary_search = function(items, value) {
+    var pivot, start, stop;
+    start = 0;
+    stop = items.length - 1;
+    pivot = Math.floor((start + stop) / 2);
+    while (items[pivot] !== value && start < stop) {
+      if (value < items[pivot]) {
+        stop = pivot - 1;
+      }
+      if (value > items[pivot]) {
+        start = pivot + 1;
+      }
+      pivot = Math.floor((stop + start) / 2);
+    }
+    if (items[pivot] === value) {
+      return pivot;
+    } else {
+      return -1;
+    }
+  };
+
+  self.onmessage = function(event) {
+    console.log("EUTA");
+    var data;
+    data = event.data;
+    return binary_search(data.items, data.value);
+  };
+
+}).call(this);
+
+//# sourceMappingURL=code_worker-source-map.js.map
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_worker-source-map.js.map
@@ -0,0 +1,10 @@
+{
+  "version": 3,
+  "file": "code_worker-source-map.js",
+  "sourceRoot": "",
+  "sources": [
+    "code_worker-source-map.coffee"
+  ],
+  "names": [],
+  "mappings": ";AACA;AAAA,MAAA;;EAAA,aAAA,GAAgB,SAAC,KAAD,EAAQ,KAAR;AAEd,QAAA;IAAA,KAAA,GAAQ;IACR,IAAA,GAAQ,KAAK,CAAC,MAAN,GAAe;IACvB,KAAA,GAAQ,IAAI,CAAC,KAAL,CAAW,CAAC,KAAA,GAAQ,IAAT,CAAA,GAAiB,CAA5B;AAER,WAAM,KAAM,CAAA,KAAA,CAAN,KAAkB,KAAlB,IAA4B,KAAA,GAAQ,IAA1C;MAGE,IAAqB,KAAA,GAAQ,KAAM,CAAA,KAAA,CAAnC;QAAA,IAAA,GAAQ,KAAA,GAAQ,EAAhB;;MACA,IAAqB,KAAA,GAAQ,KAAM,CAAA,KAAA,CAAnC;QAAA,KAAA,GAAQ,KAAA,GAAQ,EAAhB;;MAGA,KAAA,GAAQ,IAAI,CAAC,KAAL,CAAW,CAAC,IAAA,GAAO,KAAR,CAAA,GAAiB,CAA5B;IAPV;IAUA,IAAG,KAAM,CAAA,KAAA,CAAN,KAAgB,KAAnB;aAA8B,MAA9B;KAAA,MAAA;aAAyC,CAAC,EAA1C;;EAhBc;;EAkBhB,IAAI,CAAC,SAAL,GAAiB,SAAC,KAAD;AACf,QAAA;IAAA,IAAA,GAAO,KAAK,CAAC;WACb,aAAA,CAAc,IAAI,CAAC,KAAnB,EAA0B,IAAI,CAAC,KAA/B;EAFe;AAlBjB"
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_worker-source-map.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8"/>
+    <script>
+      var worker = new Worker("code_worker-source-map.js");
+
+      function binary_search(items, value) {
+        worker.postMessage({
+          items: items,
+          value: value
+        });
+      }
+    </script>
+  </head>
+  <body>
+  </body>
+</html>
--- a/devtools/client/debugger/views/workers-view.js
+++ b/devtools/client/debugger/views/workers-view.js
@@ -23,30 +23,32 @@ WorkersView.prototype = Heritage.extend(
 
     this.widget = new SideMenuWidget(document.getElementById("workers"), {
       showArrows: true,
     });
     this.emptyText = L10N.getStr("noWorkersText");
     this.widget.addEventListener("select", this._onWorkerSelect, false);
   },
 
-  addWorker: function (actor, name) {
+  addWorker: function (workerForm) {
     let element = document.createElement("label");
     element.className = "plain dbg-worker-item";
-    element.setAttribute("value", name);
+    element.setAttribute("value", workerForm.url);
     element.setAttribute("flex", "1");
 
-    this.push([element, actor], {});
+    this.push([element, workerForm.actor], {
+      attachment: workerForm
+    });
   },
 
-  removeWorker: function (actor) {
-    this.remove(this.getItemByValue(actor));
+  removeWorker: function (workerForm) {
+    this.remove(this.getItemByValue(workerForm.actor));
   },
 
   _onWorkerSelect: function () {
     if (this.selectedItem !== null) {
-      DebuggerController.Workers._onWorkerSelect(this.selectedItem.value);
+      DebuggerController.Workers._onWorkerSelect(this.selectedItem.attachment);
       this.selectedItem = null;
     }
   }
 });
 
 DebuggerView.Workers = new WorkersView();
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -907,16 +907,20 @@ Toolbox.prototype = {
     }
 
     this.setToolboxButtonsVisibility();
 
     // Old servers don't have a GCLI Actor, so just return
     if (!this.target.hasActor("gcli")) {
       return promise.resolve();
     }
+    // Disable gcli in browser toolbox until there is usages of it
+    if (this.target.chrome) {
+      return promise.resolve();
+    }
 
     const options = {
       environment: CommandUtils.createEnvironment(this, '_target')
     };
     return CommandUtils.createRequisition(this.target, options).then(requisition => {
       this._requisition = requisition;
 
       const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -228,17 +228,25 @@ Telemetry.prototype = {
     webideImportProject: {
       histogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT",
       userHistogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_PER_USER_FLAG",
     },
     custom: {
       histogram: "DEVTOOLS_CUSTOM_OPENED_COUNT",
       userHistogram: "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
-    }
+    },
+    reloadAddonInstalled: {
+      histogram: "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT",
+      userHistogram: "DEVTOOLS_RELOAD_ADDON_INSTALLED_PER_USER_FLAG",
+    },
+    reloadAddonReload: {
+      histogram: "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT",
+      userHistogram: "DEVTOOLS_RELOAD_ADDON_RELOAD_PER_USER_FLAG",
+    },
   },
 
   /**
    * Add an entry to a histogram.
    *
    * @param  {String} id
    *         Used to look up the relevant histogram ID and log true to that
    *         histogram.
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -1145,16 +1145,18 @@ public final class BrowserDatabaseHelper
                 case 25:
                     upgradeDatabaseFrom24to25(db);
                     break;
 
                 case 26:
                     upgradeDatabaseFrom25to26(db);
                     break;
 
+                // case 27 occurs in UrlMetadataTable.onUpgrade
+
                 case 28:
                     upgradeDatabaseFrom27to28(db);
                     break;
 
                 case 29:
                     upgradeDatabaseFrom28to29(db);
             }
         }
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.db;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
@@ -689,82 +690,79 @@ public class BrowserProvider extends Sha
         // tricks to generate these:
         // 1. The list of free ids is small, hence we can do a self-join to generate rowids.
         // 2. The topsites list is larger, hence we use a temporary table, which automatically provides rowids.
 
         final SQLiteDatabase db = getWritableDatabase(uri);
 
         final String TABLE_TOPSITES = "topsites";
 
-        final String totalLimit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
-        final String suggestedGridLimit = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT);
+        final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+        final String gridLimitParam = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT);
 
-        final String[] suggestedGridLimitArgs = new String[] {
-                suggestedGridLimit
-        };
+        final int totalLimit;
+        final int suggestedGridLimit;
 
-        final String[] totalLimitArgs = new String[] {
-                totalLimit
-        };
+        if (limitParam == null) {
+            totalLimit = 50;
+        } else {
+            totalLimit = Integer.parseInt(limitParam, 10);
+        }
 
-        final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.PARENT + " == ?";
-        final String[] pinnedSitesArgs = new String[] {
-                String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID)
-        };
+        if (gridLimitParam == null) {
+            suggestedGridLimit = getContext().getResources().getInteger(R.integer.number_of_top_sites);
+        } else {
+            suggestedGridLimit = Integer.parseInt(gridLimitParam, 10);
+        }
+
+        final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID;
 
         // Ideally we'd use a recursive CTE to generate our sequence, e.g. something like this worked at one point:
         // " WITH RECURSIVE" +
         // " cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 6)" +
         // However that requires SQLite >= 3.8.3 (available on Android >= 5.0), so in the meantime
         // we use a temporary numbers table.
         // Note: SQLite rowids are 1-indexed, whereas we're expecting 0-indexed values for the position. Our numbers
         // table starts at position = 0, which ensures the correct results here.
         final String freeIDSubquery =
                 " SELECT count(free_ids.position) + 1 AS rowid, numbers.position AS " + Bookmarks.POSITION +
                 " FROM (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS numbers" +
                 " LEFT OUTER JOIN " +
                 " (SELECT position FROM numbers WHERE position NOT IN (SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + ")) AS free_ids" +
                 " ON numbers.position > free_ids.position" +
                 " GROUP BY numbers.position" +
                 " ORDER BY numbers.position ASC" +
-                " LIMIT ?";
-
-        final String[] freeIDArgs = DBUtils.concatenateSelectionArgs(
-                pinnedSitesArgs,
-                pinnedSitesArgs,
-                suggestedGridLimitArgs);
+                " LIMIT " + suggestedGridLimit;
 
         // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites,
         // and about: pages.
         final String ignoreForTopSitesWhereClause =
                 "(" + Combined.HISTORY_ID + " IS NOT -1)" +
                 " AND " +
                 Combined.URL + " NOT IN (SELECT " +
                 Bookmarks.URL + " FROM bookmarks WHERE " +
-                DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < ? AND " +
+                DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " +
                 DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)" +
                 " AND " +
                 "(" + Combined.URL + " NOT LIKE ?)";
 
         final String[] ignoreForTopSitesArgs = new String[] {
-                String.valueOf(Bookmarks.FIXED_ROOT_ID),
                 AboutPages.URL_FILTER
         };
 
-
         // Stuff the suggested sites into SQL: this allows us to filter pinned and topsites out of the suggested
         // sites list as part of the final query (as opposed to walking cursors in java)
         final SuggestedSites suggestedSites = GeckoProfile.get(getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE)).getDB().getSuggestedSites();
 
         StringBuilder suggestedSitesBuilder = new StringBuilder();
         // We could access the underlying data here, however SuggestedSites also performs filtering on the suggested
         // sites list, which means we'd need to process the lists within SuggestedSites in any case. If we're doing
         // that processing, there is little real between us using a MatrixCursor, or a Map (or List) instead of the
         // MatrixCursor.
-        final Cursor suggestedSitesCursor = suggestedSites.get(Integer.parseInt(suggestedGridLimit));
+        final Cursor suggestedSitesCursor = suggestedSites.get(suggestedGridLimit);
 
         String[] suggestedSiteArgs = new String[0];
 
         boolean hasProcessedAnySuggestedSites = true;
 
         final int idColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks._ID);
         final int urlColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.URL);
         final int titleColumnIndex = suggestedSitesCursor.getColumnIndexOrThrow(Bookmarks.TITLE);
@@ -786,21 +784,18 @@ public class BrowserProvider extends Sha
                                                                     suggestedSitesCursor.getString(idColumnIndex),
                                                                     suggestedSitesCursor.getString(urlColumnIndex),
                                                                     suggestedSitesCursor.getString(titleColumnIndex)
                                                             });
         }
 
         // To restrict suggested sites to the grid, we simply subtract the number of topsites (which have already had
         // the pinned sites filtered out), and the number of pinned sites.
-        // SQLite completely ignores negative limits, hence we need to manually totalLimit to 0 in this case.
-        final String suggestedLimitClause = " LIMIT MAX(0, (? - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) ";
-
-        final String[] suggestedLimitArgs = DBUtils.concatenateSelectionArgs(suggestedGridLimitArgs,
-                                                                             pinnedSitesArgs);
+        // SQLite completely ignores negative limits, hence we need to manually limit to 0 in this case.
+        final String suggestedLimitClause = " LIMIT MAX(0, (" + suggestedGridLimit + " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) ";
 
         db.beginTransaction();
         try {
             db.execSQL("DROP TABLE IF EXISTS " + TABLE_TOPSITES);
 
             db.execSQL("CREATE TEMP TABLE " + TABLE_TOPSITES + " AS" +
                        " SELECT " +
                        Bookmarks._ID + ", " +
@@ -808,20 +803,19 @@ public class BrowserProvider extends Sha
                        Combined.HISTORY_ID + ", " +
                        Bookmarks.URL + ", " +
                        Bookmarks.TITLE + ", " +
                        Combined.HISTORY_ID + ", " +
                        TopSites.TYPE_TOP + " AS " + TopSites.TYPE +
                        " FROM " + Combined.VIEW_NAME +
                        " WHERE " + ignoreForTopSitesWhereClause +
                        " ORDER BY " + BrowserContract.getFrecencySortOrder(true, false) +
-                       " LIMIT ?",
+                       " LIMIT " + totalLimit,
 
-                       DBUtils.appendSelectionArgs(ignoreForTopSitesArgs,
-                                                   totalLimitArgs));
+                       ignoreForTopSitesArgs);
 
             if (!hasProcessedAnySuggestedSites) {
                 db.execSQL("INSERT INTO " + TABLE_TOPSITES +
                            // We need to LIMIT _after_ selecting the relevant suggested sites, which requires us to
                            // use an additional internal subquery, since we cannot LIMIT a subquery that is part of UNION ALL.
                            // Hence the weird SELECT * FROM (SELECT ...relevant suggested sites... LIMIT ?)
                            " SELECT * FROM (SELECT " +
                            Bookmarks._ID + ", " +
@@ -833,29 +827,37 @@ public class BrowserProvider extends Sha
                            TopSites.TYPE_SUGGESTED + " as " + TopSites.TYPE +
                            " FROM ( " + suggestedSitesBuilder.toString() + " )" +
                            " WHERE " +
                            Bookmarks.URL + " NOT IN (SELECT url FROM " + TABLE_TOPSITES + ")" +
                            " AND " +
                            Bookmarks.URL + " NOT IN (SELECT url " + pinnedSitesFromClause + ")" +
                            suggestedLimitClause + " )",
 
-                           DBUtils.concatenateSelectionArgs(suggestedSiteArgs,
-                                                            pinnedSitesArgs,
-                                                            suggestedLimitArgs));
+                           suggestedSiteArgs);
             }
 
+            // If we retrieve more topsites than we have free positions for in the freeIdSubquery,
+            // we will have topsites that don't receive a position when joining TABLE_TOPSITES
+            // with freeIdSubquery. Hence we need to coalesce the position with a generated position.
+            // We know that the difference in positions will be at most suggestedGridLimit, hence we
+            // can add that to the rowid to generate a safe position.
+            // I.e. if we have 6 pinned sites then positions 0..5 are filled, the JOIN results in
+            // the first N rows having positions 6..(N+6), so row N+1 should receive a position that is at
+            // least N+1+6, which is equal to rowid + 6.
             final SQLiteCursor c = (SQLiteCursor) db.rawQuery(
                         "SELECT " +
                         Bookmarks._ID + ", " +
                         TopSites.BOOKMARK_ID + ", " +
                         TopSites.HISTORY_ID + ", " +
                         Bookmarks.URL + ", " +
                         Bookmarks.TITLE + ", " +
-                        Bookmarks.POSITION + ", " +
+                        "COALESCE(" + Bookmarks.POSITION + ", " +
+                            DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") + " + " + suggestedGridLimit +
+                        ")" + " AS " + Bookmarks.POSITION + ", " +
                         Combined.HISTORY_ID + ", " +
                         TopSites.TYPE +
                         " FROM " + TABLE_TOPSITES +
                         " LEFT OUTER JOIN " + // TABLE_IDS +
                         "(" + freeIDSubquery + ") AS id_results" +
                         " ON " + DBUtils.qualifyColumn(TABLE_TOPSITES, "rowid") +
                         " = " + DBUtils.qualifyColumn("id_results", "rowid") +
 
@@ -866,22 +868,21 @@ public class BrowserProvider extends Sha
                         Bookmarks._ID + " AS " + TopSites.BOOKMARK_ID + ", " +
                         " -1 AS " + TopSites.HISTORY_ID + ", " +
                         Bookmarks.URL + ", " +
                         Bookmarks.TITLE + ", " +
                         Bookmarks.POSITION + ", " +
                         "NULL AS " + Combined.HISTORY_ID + ", " +
                         TopSites.TYPE_PINNED + " as " + TopSites.TYPE +
                         " FROM " + TABLE_BOOKMARKS +
-                        " WHERE " + Bookmarks.PARENT + " == ? " +
+                        " WHERE " + Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID +
 
                         " ORDER BY " + Bookmarks.POSITION,
 
-                         DBUtils.appendSelectionArgs(freeIDArgs,
-                                                     pinnedSitesArgs));
+                        null);
 
             c.setNotificationUri(getContext().getContentResolver(),
                                  BrowserContract.AUTHORITY_URI);
 
             // Force the cursor to be compiled and the cursor-window filled now:
             // (A) without compiling the cursor now we won't have access to the TEMP table which
             // is removed as soon as we close our connection.
             // (B) this might also mitigate the situation causing this crash where we're accessing
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
@@ -13,16 +13,17 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
 import android.support.v4.util.LruCache;
 import android.util.Log;
@@ -74,40 +75,26 @@ public class LocalURLMetadata implements
         }
 
 
         try {
             JSONObject icons;
             if (obj.has("touchIconList") &&
                     (icons = obj.getJSONObject("touchIconList")).length() > 0) {
                 int preferredSize = GeckoAppShell.getPreferredIconSize();
-                int bestSizeFound = -1;
 
                 Iterator<String> keys = icons.keys();
 
                 ArrayList<Integer> sizes = new ArrayList<Integer>(icons.length());
                 while (keys.hasNext()) {
                     sizes.add(new Integer(keys.next()));
                 }
 
-                Collections.sort(sizes);
-                for (int size : sizes) {
-                    if (size >= preferredSize) {
-                        bestSizeFound = size;
-                        break;
-                    }
-                }
-
-                // If all icons are smaller than the preferred size then we don't have an icon
-                // selected yet (bestSizeFound == -1), therefore just take the largest (last) icon.
-                if (bestSizeFound == -1) {
-                    bestSizeFound = sizes.get(sizes.size() - 1);
-                }
-
-                String iconURL = icons.getString(Integer.toString(bestSizeFound));
+                final int bestSize = Favicons.selectBestSizeFromList(sizes, preferredSize);
+                final String iconURL = icons.getString(Integer.toString(bestSize));
 
                 data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL);
             }
         } catch (JSONException e) {
             Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e);
         }
 
         return Collections.unmodifiableMap(data);
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
@@ -140,16 +140,42 @@ public class Favicons {
      */
     public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) {
         pageURLMappings.put(pageURL, faviconURL);
     }
 
     private static FaviconCache faviconsCache;
 
     /**
+     * Select the closest icon size from a list of icon sizes.
+     * We just find the first icon that is larger than the preferred size if available, or otherwise select the
+     * largest icon (if all icons are smaller than the preferred size).
+     *
+     * @return The closes icon size, or -1 if no sizes are supplied.
+     */
+    public static int selectBestSizeFromList(final List<Integer> sizes, final int preferredSize) {
+        Collections.sort(sizes);
+
+        for (int size : sizes) {
+            if (size >= preferredSize) {
+                return size;
+            }
+        }
+
+        // If all icons are smaller than the preferred size then we don't have an icon
+        // selected yet, therefore just take the largest (last) icon.
+        if (sizes.size() > 0) {
+            return sizes.get(sizes.size() - 1);
+        } else {
+            // This isn't ideal, however current code assumes this as an error value for now.
+            return -1;
+        }
+    }
+
+    /**
      * Returns either NOT_LOADING, or LOADED if the onFaviconLoaded call could
      * be made on the main thread.
      * If no listener is provided, NOT_LOADING is returned.
      */
     static int dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image,
             final OnFaviconLoadedListener listener) {
         if (listener == null) {
             return NOT_LOADING;
@@ -579,11 +605,11 @@ public class Favicons {
      *
      * Deduces the favicon URL from the browser database, guessing if necessary.
      *
      * @param url page URL to get a large favicon image for.
      * @param onFaviconLoadedListener listener to call back with the result.
      */
     public static void getPreferredSizeFaviconForPage(Context context, String url, String iconURL, OnFaviconLoadedListener onFaviconLoadedListener) {
         int preferredSize = GeckoAppShell.getPreferredIconSize();
-        loadUncachedFavicon(context, url, iconURL, 0, preferredSize, onFaviconLoadedListener);
+        loadUncachedFavicon(context, url, iconURL, LoadFaviconTask.FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS, preferredSize, onFaviconLoadedListener);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
@@ -20,22 +20,24 @@ import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
 
 /**
  * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
  * cache.
@@ -45,16 +47,24 @@ import static org.mozilla.gecko.util.IOU
 public class LoadFaviconTask {
     private static final String LOGTAG = "LoadFaviconTask";
 
     // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon
     // from executing concurrently.
     private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<>();
 
     public static final int FLAG_PERSIST = 1;
+    /**
+     * Bypass all caches - this is used to directly retrieve the requested icon. Without this flag,
+     * favicons will first be pushed into the memory cache (and possibly permanent cache if using FLAG_PERSIST),
+     * where they will be downscaled to the maximum cache size, before being retrieved from the cache (resulting
+     * in a possibly smaller icon size).
+     */
+    public static final int FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS = 2;
+
     private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
     // The default size of the buffer to use for downloading Favicons in the event no size is given
     // by the server.
     public static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
 
     private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
     private final Context context;
     private final int id;
@@ -412,20 +422,23 @@ public class LoadFaviconTask {
             // chain onto the same parent task.
             loadsInFlight.put(faviconURL, this);
         }
 
         if (isCancelled()) {
             return null;
         }
 
+        LoadFaviconResult loadedBitmaps = null;
         // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
-        LoadFaviconResult loadedBitmaps = loadFaviconFromDb(db);
-        if (loadedBitmaps != null) {
-            return pushToCacheAndGetResult(loadedBitmaps);
+        if ((flags & FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS) == 0) {
+            loadedBitmaps = loadFaviconFromDb(db);
+            if (loadedBitmaps != null) {
+                return pushToCacheAndGetResult(loadedBitmaps);
+            }
         }
 
         if (onlyFromLocal || isCancelled()) {
             return null;
         }
 
         // Let's see if it's in a JAR.
         image = fetchJARFavicon(faviconURL);
@@ -440,21 +453,35 @@ public class LoadFaviconTask {
         } catch (URISyntaxException e) {
             Log.e(LOGTAG, "The provided favicon URL is not valid");
             return null;
         } catch (Exception e) {
             Log.e(LOGTAG, "Couldn't download favicon.", e);
         }
 
         if (loadedBitmaps != null) {
-            // Fetching bytes to store can fail. saveFaviconToDb will
-            // do the right thing, but we still choose to cache the
-            // downloaded icon in memory.
-            saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
-            return pushToCacheAndGetResult(loadedBitmaps);
+            if ((flags & FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS) == 0) {
+                // Fetching bytes to store can fail. saveFaviconToDb will
+                // do the right thing, but we still choose to cache the
+                // downloaded icon in memory.
+                saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
+                return pushToCacheAndGetResult(loadedBitmaps);
+            } else {
+                final Map<Integer, Bitmap> iconMap = new HashMap<>();
+                final List<Integer> sizes = new ArrayList<>();
+
+                while (loadedBitmaps.getBitmaps().hasNext()) {
+                    final Bitmap b = loadedBitmaps.getBitmaps().next();
+                    iconMap.put(b.getWidth(), b);
+                    sizes.add(b.getWidth());
+                }
+
+                int bestSize = Favicons.selectBestSizeFromList(sizes, targetWidth);
+                return iconMap.get(bestSize);
+            }
         }
 
         if (isUsingDefaultURL) {
             Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
         if (isCancelled()) {
@@ -540,21 +567,25 @@ public class LoadFaviconTask {
                 // image now, or the call into the cache in processResult will fetch the right one.
                 t.processResult(image);
             }
         }
     }
 
     private void processResult(Bitmap image) {
         Favicons.removeLoadTask(id);
-        Bitmap scaled = image;
+        final Bitmap scaled;
 
         // Notify listeners, scaling if required.
-        if (targetWidth != -1 && image != null &&  image.getWidth() != targetWidth) {
+        if ((flags & FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS) != 0) {
+            scaled = Bitmap.createScaledBitmap(image, targetWidth, targetWidth, true);
+        } else if (targetWidth != -1 && image != null &&  image.getWidth() != targetWidth) {
             scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
+        } else {
+            scaled = image;
         }
 
         Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener);
     }
 
     void onCancelled() {
         Favicons.removeLoadTask(id);
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -20,29 +20,30 @@ import org.mozilla.gecko.widget.FaviconV
 
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class TwoLinePageRow extends LinearLayout
                             implements Tabs.OnTabsChangedListener {
 
     protected static final int NO_ICON = 0;
 
     private final TextView mTitle;
     private final TextView mUrl;
+    private final ImageView mStatusIcon;
 
     private int mSwitchToTabIconId;
-    private int mPageTypeIconId;
 
     private final FaviconView mFavicon;
 
     private boolean mShowIcons;
     private int mLoadFaviconJobId = Favicons.NOT_LOADING;
 
     // Only holds a reference to the FaviconView itself, so if the row gets
     // discarded while a task is outstanding, we'll leak less memory.
@@ -85,21 +86,24 @@ public class TwoLinePageRow extends Line
     }
 
     public TwoLinePageRow(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         setGravity(Gravity.CENTER_VERTICAL);
 
         LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this);
+        // Merge layouts lose their padding, so set it dynamically.
+        setPadding(0, 0, (int) getResources().getDimension(R.dimen.page_row_padding_right), 0);
+
         mTitle = (TextView) findViewById(R.id.title);
         mUrl = (TextView) findViewById(R.id.url);
+        mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark);
 
         mSwitchToTabIconId = NO_ICON;
-        mPageTypeIconId = NO_ICON;
         mShowIcons = true;
 
         mFavicon = (FaviconView) findViewById(R.id.icon);
         mFaviconListener = new UpdateViewFaviconLoadedListener(mFavicon);
     }
 
     @Override
     protected void onAttachedToWindow() {
@@ -168,26 +172,22 @@ public class TwoLinePageRow extends Line
     }
 
     protected void setSwitchToTabIcon(int iconId) {
         if (mSwitchToTabIconId == iconId) {
             return;
         }
 
         mSwitchToTabIconId = iconId;
-        mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, mPageTypeIconId, 0);
+        mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0);
     }
 
-    private void setPageTypeIcon(int iconId) {
-        if (mPageTypeIconId == iconId) {
-            return;
-        }
-
-        mPageTypeIconId = iconId;
-        mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, mPageTypeIconId, 0);
+    private void showBookmarkIcon(boolean toShow) {
+        final int visibility = toShow ? VISIBLE : GONE;
+        mStatusIcon.setVisibility(visibility);
     }
 
     /**
      * Stores the page URL, so that we can use it to replace "Switch to tab" if the open
      * tab changes or is closed.
      */
     private void updateDisplayedUrl(String url) {
         mPageUrl = url;
@@ -226,23 +226,20 @@ public class TwoLinePageRow extends Line
     public void update(String title, String url) {
         update(title, url, 0);
     }
 
     protected void update(String title, String url, long bookmarkId) {
         if (mShowIcons) {
             // The bookmark id will be 0 (null in database) when the url
             // is not a bookmark.
-            if (bookmarkId == 0) {
-                setPageTypeIcon(NO_ICON);
-            } else {
-                setPageTypeIcon(R.drawable.ic_url_bar_star);
-            }
+            final boolean isBookmark = bookmarkId != 0;
+            showBookmarkIcon(isBookmark);
         } else {
-            setPageTypeIcon(NO_ICON);
+            showBookmarkIcon(false);
         }
 
         // Use the URL instead of an empty title for consistency with the normal URL
         // bar view - this is the equivalent of getDisplayTitle() in Tab.java
         setTitle(TextUtils.isEmpty(title) ? url : title);
 
         // No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
         if (url.equals(mPageUrl)) {
deleted file mode 100644
index bf18ebeb035c34c5fd29a866987abced913c4c5b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..641a0e1b5dc2fe2049f3f8ed19576d8ae9602ed8
GIT binary patch
literal 600
zc$@)P0;m0nP)<h;3K|Lk000e1NJLTq001Qb001Ni1^@s64#M-M0006XNkl<Zcmd6r
z0}NYH6o&uVRxoeDY}>ZNc?;%fij&#8XSQwIwzqp;J3F4Y;v^aB-dBCeAGGha-!)*H
z<*y5X_&gNff^wEbN}%)(Ph1kOT+&(OsQO3fkC<+s>{{C9FU6OTG!{C`g7~+ul!UBS
zND8Fa`O5qW>9a^UOP`kp@y}pV5^^*}LQ%Xlq>zM9B4I3lSp=lNhn13W10)3Ec@YUo
z_z2?4=6Bh#NY@APmq?`~3$0#I&OETiviCVaToVheeo#COr1wLy<InIn4e=|GJ__PF
zP}X3OvtCeH`GWK)C_VtCpPB!$xvVs^;&E8Qg&+{0NVrJQ@O`*kX&_CLGE(|0goQjH
zWu%D5;}68slP|?<;Buw)UCUGwrM6+SD;<214o18(@lB@pdxSC~7-Fk<a{{G~V}+F&
zVOiwL0OCCf=+x(Bm57-Au1qqa#jnY(VUf;du>XFQ6-u9pgVbHmZ|8?G<fvj-BK8Lh
za&(D9Um9vtN{@-dBsmwhP<nYBqz>7YIv0oLvusN7hd9*KFPSh6k5PH!H%~n)4<i;j
z>O@-uk?-3bU15Z>`b4~bEwXA!>4|qmI9H7^m6{ml$FE`$w+vjvd@_vG3K&7WDahz~
z2*o{Qs}qWaR%;Mn5AqwQO{KQ@l<qkc50vdqq<F;T+(-JckJuU0tDa^c{oa#no{Tab
m#=;3#ddOK%t|32(Xb9X`rPzV9yB`_=0000<MNUMnLSTa8%@#WV
deleted file mode 100644
index 5f6f96db027d017d004a6c3b333d19fd0537b080..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/mobile/android/base/resources/layout/two_line_page_row.xml
+++ b/mobile/android/base/resources/layout/two_line_page_row.xml
@@ -9,34 +9,43 @@
        tools:context=".BrowserApp">
 
     <org.mozilla.gecko.widget.FaviconView android:id="@+id/icon"
                                           android:layout_width="@dimen/favicon_bg"
                                           android:layout_height="@dimen/favicon_bg"
                                           android:layout_margin="16dp"
                                           tools:background="@drawable/favicon_globe"/>
 
-    <LinearLayout android:layout_width="match_parent"
+    <LinearLayout android:layout_width="0dp"
                   android:layout_height="wrap_content"
+                  android:layout_weight="1"
                   android:layout_gravity="center_vertical"
-                  android:orientation="vertical"
-                  android:paddingRight="25dp">
+                  android:paddingRight="10dp"
+                  android:orientation="vertical">
 
         <org.mozilla.gecko.widget.FadedSingleColorTextView
                 android:id="@+id/title"
                 style="@style/Widget.TwoLinePageRow.Title"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                gecko:fadeWidth="30dp"
+                gecko:fadeWidth="90dp"
                 tools:text="This is a long test title"/>
 
-        <TextView android:id="@+id/url"
+        <org.mozilla.gecko.widget.FadedSingleColorTextView android:id="@+id/url"
                   style="@style/Widget.TwoLinePageRow.Url"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:drawablePadding="5dp"
                   android:maxLength="1024"
+                  gecko:fadeWidth="90dp"
                   tools:text="http://test.com/test"
-                  tools:drawableLeft="@drawable/ic_url_bar_tab"
-                  tools:drawableRight="@drawable/ic_url_bar_star"/>
+                  tools:drawableLeft="@drawable/ic_url_bar_tab"/>
 
     </LinearLayout>
+
+    <ImageView android:id="@+id/status_icon_bookmark"
+               android:layout_width="20dp"
+               android:layout_height="20dp"
+               android:layout_gravity="center"
+               android:visibility="gone"
+               android:src="@drawable/bookmarked_star"/>
+
 </merge>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -74,16 +74,18 @@
     <dimen name="browser_toolbar_site_security_padding_vertical">7dp</dimen>
     <dimen name="browser_toolbar_site_security_padding_horizontal">7dp</dimen>
 
     <!-- If one of these values changes, they all should. -->
     <dimen name="browser_toolbar_site_security_margin_bottom">.5dp</dimen>
     <dimen name="site_security_unknown_inset_top">1dp</dimen>
     <dimen name="site_security_unknown_inset_bottom">-1dp</dimen>
 
+    <dimen name="page_row_padding_right">15dp</dimen>
+
     <!-- Regular page row on about:home -->
     <dimen name="page_row_height">64dp</dimen>
 
     <!-- Group/heading page row on about:home -->
     <dimen name="page_group_height">56dp</dimen>
 
     <!-- Reading list row on about:home -->
     <dimen name="reading_list_row_height">128dp</dimen>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -121,17 +121,16 @@
     <style name="Widget.TwoLinePageRow.Title">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
     </style>
 
     <style name="Widget.TwoLinePageRow.Url">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
         <item name="android:includeFontPadding">false</item>
         <item name="android:singleLine">true</item>
-        <item name="android:ellipsize">middle</item>
     </style>
 
     <style name="Widget.ReadingListRow" />
 
     <style name="Widget.ReadingListRow.Title">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
         <item name="android:maxLines">2</item>
         <item name="android:ellipsize">end</item>
@@ -408,17 +407,19 @@
     </style>
 
     <style name="TextAppearance.Widget.Home" />
 
     <style name="TextAppearance.Widget.Home.Header" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
-    <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance.Medium"/>
+    <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance">
+        <item name="android:textSize">16dp</item>
+    </style>
 
     <style name="TextAppearance.Widget.Home.ItemDescription" parent="TextAppearance.Micro">
         <item name="android:textColor">@color/tabs_tray_icon_grey</item>
     </style>
 
     <style name="TextAppearance.Widget.HomeBanner" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorHint</item>
     </style>
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -58,18 +58,19 @@ var ActionBarHandler = {
     // Open a closed ActionBar if carets actually visible.
     if (!this._selectionID && e.caretVisuallyVisible) {
       this._init();
       return;
     }
 
     // Else, update an open ActionBar.
     if (this._selectionID) {
-      if ([this._targetElement, this._contentWindow] ===
-          [Services.focus.focusedElement, Services.focus.focusedWindow]) {
+      let [element, win] = this._getSelectionTargets();
+      if (this._targetElement === element &&
+          this._contentWindow === win) {
         // We have the same focused window/element as before. Trigger "TextSelection:ActionbarStatus"
         // message only if available actions differ from when last we checked.
         this._sendActionBarActions();
       } else {
         // We have a new focused window/element pair.
         this._uninit(false);
         this._init();
       }
@@ -220,18 +221,24 @@ var ActionBarHandler = {
    * the previous.
    * @param By default we only send an ActionBarStatus update message if
    *        there is a change from the previous state. sendAlways can be
    *        set by init() for example, where we want to always send the
    *        current state.
    */
   _sendActionBarActions: function(sendAlways) {
     let actions = this._getActionBarActions();
+    let actionCountUnchanged = this._actionBarActions &&
+      actions.length === this._actionBarActions.length;
+    let actionsMatch = actionCountUnchanged &&
+      this._actionBarActions.every((e,i) => {
+        return e.id === actions[i].id;
+      });
 
-    if (sendAlways || actions !== this._actionBarActions) {
+    if (sendAlways || !actionsMatch) {
       Messaging.sendRequest({
         type: "TextSelection:ActionbarStatus",
         actions: actions,
       });
     }
 
     this._actionBarActions = actions;
   },
--- a/services/sync/modules/SyncedTabs.jsm
+++ b/services/sync/modules/SyncedTabs.jsm
@@ -203,16 +203,19 @@ let SyncedTabsInternal = {
         Preferences.set("services.sync.lastTabFetch", Math.floor(Date.now() / 1000));
         Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
         break;
       case "weave:service:start-over":
         // start-over needs to notify so consumers find no tabs.
         Preferences.reset("services.sync.lastTabFetch");
         Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
         break;
+      case "nsPref:changed":
+        Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
+        break;
       default:
         break;
     }
   },
 
   // Returns true if Sync is configured to Sync tabs, false otherwise
   get isConfiguredToSyncTabs() {
     if (!weaveXPCService.ready) {
@@ -227,16 +230,20 @@ let SyncedTabsInternal = {
   get hasSyncedThisSession() {
     let engine = Weave.Service.engineManager.get("tabs");
     return engine && engine.hasSyncedThisSession;
   },
 };
 
 Services.obs.addObserver(SyncedTabsInternal, "weave:engine:sync:finish", false);
 Services.obs.addObserver(SyncedTabsInternal, "weave:service:start-over", false);
+// Observe the pref the indicates the state of the tabs engine has changed.
+// This will force consumers to re-evaluate the state of sync and update
+// accordingly.
+Services.prefs.addObserver("services.sync.engine.tabs", SyncedTabsInternal, false);
 
 // The public interface.
 this.SyncedTabs = {
   // A mock-point for tests.
   _internal: SyncedTabsInternal,
 
   // We make the topic for the observer notification public.
   TOPIC_TABS_CHANGED,
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -587,16 +587,37 @@ this.BrowserTestUtils = {
 
       mm.sendAsyncMessage("Test:SynthesizeMouse",
                           {target, targetFn, x: offsetX, y: offsetY, event: event},
                           {object: cpowObject});
     });
   },
 
   /**
+   * Wait for a message to be fired from a particular message manager
+   *
+   * @param {nsIMessageManager} messageManager
+   *                            The message manager that should be used.
+   * @param {String}            message
+   *                            The message we're waiting for.
+   * @param {Function}          checkFn (optional)
+   *                            Optional function to invoke to check the message.
+   */
+  waitForMessage(messageManager, message, checkFn) {
+    return new Promise(resolve => {
+      messageManager.addMessageListener(message, function onMessage(msg) {
+        if (!checkFn || checkFn(msg)) {
+          messageManager.removeMessageListener(message, onMessage);
+          resolve();
+        }
+      });
+    });
+  },
+
+  /**
    *  Version of synthesizeMouse that uses the center of the target as the mouse
    *  location. Arguments and the return value are the same.
    */
   synthesizeMouseAtCenter(target, event, browser)
   {
     // Use a flag to indicate to center rather than having a separate message.
     event.centered = true;
     return BrowserTestUtils.synthesizeMouse(target, 0, 0, event, browser);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7016,16 +7016,30 @@
   "DEVTOOLS_CUSTOM_OPENED_COUNT": {
     "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
     "expires_in_version": "never",
     "kind": "count",
     "bug_numbers": [1247985],
     "description": "Number of times a custom developer tool has been opened.",
     "releaseChannelCollection": "opt-out"
   },
+  "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+    "expires_in_version": "55",
+    "kind": "count",
+    "description": "Number of times the reload addon has been installed.",
+    "bug_numbers": [1248435]
+  },
+  "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+    "expires_in_version": "55",
+    "kind": "count",
+    "description": "Number of times the tools have been reloaded by the reload addon.",
+    "bug_numbers": [1248435]
+  },
   "DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Number of users that have opened the DevTools toolbox."
   },
   "DEVTOOLS_OPTIONS_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
@@ -7199,16 +7213,30 @@
     "kind": "flag",
     "description": "Number of users that have imported a project into the DevTools WebIDE."
   },
   "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Number of users that have opened a custom developer tool via the toolbox button."
   },
+  "DEVTOOLS_RELOAD_ADDON_INSTALLED_PER_USER_FLAG": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+    "expires_in_version": "55",
+    "kind": "flag",
+    "description": "Records once per browser version if the reload add-on is installed.",
+    "bug_numbers": [1248435]
+  },
+  "DEVTOOLS_RELOAD_ADDON_RELOAD_PER_USER_FLAG": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+    "expires_in_version": "55",
+    "kind": "flag",
+    "description": "Records once per browser version if the tools have been reloaded via the reload add-on.",
+    "bug_numbers": [1248435]
+  },
   "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000000,
     "n_buckets": 100,
     "description": "How long has the toolbox been active (seconds)"
   },
   "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS": {
@@ -8447,16 +8475,25 @@
   },
   "LOOP_ROOM_SESSION_WITHCHAT": {
     "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
     "expires_in_version": "50",
     "kind": "count",
     "releaseChannelCollection": "opt-out",
     "description": "Number of sessions where at least one chat message was exchanged"
   },
+  "LOOP_INFOBAR_ACTION_BUTTONS": {
+    "alert_emails": ["firefox-dev@mozilla.org", "mbanner@mozilla.com"],
+    "expires_in_version": "51",
+    "kind": "enumerated",
+    "n_values": 4,
+    "releaseChannelCollection": "opt-out",
+    "bug_numbers": [1245486],
+    "description": "Number times info bar buttons are clicked (0=PAUSED, 1=CREATED)"
+  },
   "E10S_STATUS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 12,
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [1241294],
     "description": "Why e10s is enabled or disabled (0=ENABLED_BY_USER, 1=ENABLED_BY_DEFAULT, 2=DISABLED_BY_USER, 3=DISABLED_IN_SAFE_MODE, 4=DISABLED_FOR_ACCESSIBILITY, 5=DISABLED_FOR_MAC_GFX, 6=DISABLED_FOR_BIDI, 7=DISABLED_FOR_ADDONS)"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function doUpdate(update) {
+  const { classes: Cc, interfaces: Ci, results: Cr } = Components;
+
+  let listener = {
+    QueryInterface: function(iid)
+    {
+      if (iid.equals(Ci.nsISupports) ||
+          iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+        return this;
+
+      throw Cr.NS_ERROR_NO_INTERFACE;
+    },
+    updateUrlRequested: function(url) { },
+    streamFinished: function(status) { },
+    updateError: function(errorCode) {
+      sendAsyncMessage("updateError", { errorCode });
+    },
+    updateSuccess: function(requestedTimeout) {
+      sendAsyncMessage("loadTestFrame");
+    }
+  };
+
+  let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+                  .getService(Ci.nsIUrlClassifierDBService);
+
+  dbService.beginUpdate(listener, "test-malware-simple,test-unwanted-simple", "");
+  dbService.beginStream("", "");
+  dbService.updateStream(update);
+  dbService.finishStream();
+  dbService.finishUpdate();
+}
+
+addMessageListener("doUpdate", ({ testUpdate }) => {
+  doUpdate(testUpdate);
+});
--- a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
-skip-if = buildapp == 'b2g' || e10s
+skip-if = buildapp == 'b2g'
 support-files =
   classifiedAnnotatedPBFrame.html
+  classifierCommon.js
   classifierFrame.html
   cleanWorker.js
   good.js
   evil.css
   evil.js
   evil.js^headers^
   evilWorker.js
   import.css
--- a/toolkit/components/url-classifier/tests/mochitest/test_classifier.html
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier.html
@@ -8,77 +8,57 @@
 
 <p id="display"></p>
 <div id="content" style="display: none">
 </div>
 <pre id="test">
 
 <script class="testbody" type="text/javascript">
 
-var Cc = SpecialPowers.Cc;
-var Ci = SpecialPowers.Ci;
 var firstLoad = true;
 
 // Add some URLs to the malware database.
 var testData = "malware.example.com/";
 var testUpdate =
   "n:1000\ni:test-malware-simple\nad:1\n" +
   "a:524:32:" + testData.length + "\n" +
   testData;
 
 testData = "unwanted.example.com/";
 testUpdate +=
   "n:1000\ni:test-unwanted-simple\nad:1\n" +
   "a:524:32:" + testData.length + "\n" +
   testData;
 
-var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
-                .getService(Ci.nsIUrlClassifierDBService);
-
 function loadTestFrame() {
   document.getElementById("testFrame").src = "classifierFrame.html";
 }
 
-function doUpdate(update) {
-  var listener = {
-    QueryInterface: SpecialPowers.wrapCallback(function(iid)
-    {
-      if (iid.equals(Ci.nsISupports) ||
-          iid.equals(Ci.nsIUrlClassifierUpdateObserver))
-        return this;
+const CLASSIFIER_COMMON_URL = SimpleTest.getTestFileURL("classifierCommon.js");
+let classifierCommonScript = SpecialPowers.loadChromeScript(CLASSIFIER_COMMON_URL);
+
+// Expected finish() call is in "classifierFrame.html".
+SimpleTest.waitForExplicitFinish();
 
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    }),
-    updateUrlRequested: function(url) { },
-    streamFinished: function(status) { },
-    updateError: function(errorCode) {
-      ok(false, "Couldn't update classifier.");
-      // Abort test.
-      SimpleTest.finish();
-    },
-    updateSuccess: function(requestedTimeout) {
-      SpecialPowers.pushPrefEnv(
-        {"set" : [["browser.safebrowsing.malware.enabled", true]]},
-        loadTestFrame);
-    }
-  };
-
-  dbService.beginUpdate(listener, "test-malware-simple,test-unwanted-simple", "");
-  dbService.beginStream("", "");
-  dbService.updateStream(update);
-  dbService.finishStream();
-  dbService.finishUpdate();
-}
+classifierCommonScript.addMessageListener("loadTestFrame", () => {
+  SpecialPowers.pushPrefEnv(
+    {"set" : [["browser.safebrowsing.malware.enabled", true]]},
+    loadTestFrame);
+});
+classifierCommonScript.addMessageListener("updateError", ({ errorCode }) => {
+  ok(false, "Couldn't update classifier. Error code: " + errorCode);
+  // Abort test.
+  SimpleTest.finish();
+});
 
 SpecialPowers.pushPrefEnv(
   {"set" : [["urlclassifier.malwareTable", "test-malware-simple,test-unwanted-simple"],
             ["urlclassifier.phishTable", "test-phish-simple"]]},
-  function() { doUpdate(testUpdate); });
-
-// Expected finish() call is in "classifierFrame.html".
-SimpleTest.waitForExplicitFinish();
+  function() {
+    classifierCommonScript.sendAsyncMessage("doUpdate", { testUpdate });
+  });
 
 </script>
 
 </pre>
 <iframe id="testFrame" onload=""></iframe>
 </body>
 </html>
--- a/toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html
@@ -8,85 +8,65 @@
 
 <p id="display"></p>
 <div id="content" style="display: none">
 </div>
 <pre id="test">
 
 <script class="testbody" type="text/javascript">
 
-var Cc = SpecialPowers.Cc;
-var Ci = SpecialPowers.Ci;
-
 // Add some URLs to the malware database.
 var testData = "example.com/tests/toolkit/components/url-classifier/tests/mochitest/evilWorker.js";
 var testUpdate =
   "n:1000\ni:test-malware-simple\nad:550\n" +
   "a:550:32:" + testData.length + "\n" +
   testData;
 
 testData = "example.com/tests/toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js";
 testUpdate +=
   "n:1000\ni:test-unwanted-simple\nad:550\n" +
   "a:550:32:" + testData.length + "\n" +
   testData;
 
-var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
-                .getService(Ci.nsIUrlClassifierDBService);
-
-function doUpdate(update) {
-  var listener = {
-    QueryInterface: SpecialPowers.wrapCallback(function(iid)
-    {
-      if (iid.equals(Ci.nsISupports) ||
-          iid.equals(Ci.nsIUrlClassifierUpdateObserver))
-        return this;
-
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    }),
-    updateUrlRequested: function(url) { },
-    streamFinished: function(status) { },
-    updateError: function(errorCode) {
-      ok(false, "Couldn't update classifier.");
-      // Abort test.
-      SimpleTest.finish();
-    },
-    updateSuccess: function(requestedTimeout) {
-      SpecialPowers.pushPrefEnv(
-        {"set" : [["browser.safebrowsing.malware.enabled", true]]},
-        function loadTestFrame() {
-          document.getElementById("testFrame").src =
-            "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/workerFrame.html";
-        }
-      );
-    }
-  };
-
-  dbService.beginUpdate(listener, "test-malware-simple,test-unwanted-simple", "");
-  dbService.beginStream("", "");
-  dbService.updateStream(update);
-  dbService.finishStream();
-  dbService.finishUpdate();
+function loadTestFrame() {
+  document.getElementById("testFrame").src =
+    "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/workerFrame.html";
 }
 
 function onmessage(event)
 {
   var pieces = event.data.split(':');
   if (pieces[0] == "finish") {
     SimpleTest.finish();
     return;
   }
 
   is(pieces[0], "success", pieces[1]);
 }
 
+const CLASSIFIER_COMMON_URL = SimpleTest.getTestFileURL("classifierCommon.js");
+let classifierCommonScript = SpecialPowers.loadChromeScript(CLASSIFIER_COMMON_URL);
+
+classifierCommonScript.addMessageListener("loadTestFrame", () => {
+  SpecialPowers.pushPrefEnv(
+    {"set" : [["browser.safebrowsing.malware.enabled", true]]},
+    loadTestFrame);
+});
+classifierCommonScript.addMessageListener("updateError", ({ errorCode }) => {
+  ok(false, "Couldn't update classifier. Error code: " + errorCode);
+  // Abort test.
+  SimpleTest.finish();
+});
+
 SpecialPowers.pushPrefEnv(
   {"set" : [["urlclassifier.malwareTable", "test-malware-simple,test-unwanted-simple"],
             ["urlclassifier.phishTable", "test-phish-simple"]]},
-  function() { doUpdate(testUpdate); });
+  function() {
+    classifierCommonScript.sendAsyncMessage("doUpdate", { testUpdate });
+  });
 
 window.addEventListener("message", onmessage, false);
 
 SimpleTest.waitForExplicitFinish();
 
 </script>
 
 </pre>
--- a/uriloader/exthandler/tests/mochitest/mochitest.ini
+++ b/uriloader/exthandler/tests/mochitest/mochitest.ini
@@ -1,12 +1,12 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g'
 support-files =
   handlerApp.xhtml
   handlerApps.js
+  unsafeBidi_chromeScript.js
   unsafeBidiFileName.sjs
 
 [test_badMimeType.html]
 [test_handlerApps.xhtml]
 skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938
 [test_unsafeBidiChars.xhtml]
-skip-if = e10s
--- a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
+++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
@@ -1,27 +1,28 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>Test for Handling of unsafe bidi chars</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
-<body onload="load();">
+<body>
 <p id="display"></p>
 <iframe id="test"></iframe>
 <script type="text/javascript">
 <![CDATA[
 
-var unsafeBidiChars = {
-  LRE: "\xe2\x80\xaa",
-  RLE: "\xe2\x80\xab",
-  PDF: "\xe2\x80\xac",
-  LRO: "\xe2\x80\xad",
-  RLO: "\xe2\x80\xae"
-};
+var unsafeBidiChars = [
+  "\xe2\x80\xaa", // LRE
+  "\xe2\x80\xab", // RLE
+  "\xe2\x80\xac", // PDF
+  "\xe2\x80\xad", // LRO
+  "\xe2\x80\xae"  // RLO
+];
 
 var tests = [
   "{1}.test",
   "{1}File.test",
   "Fi{1}le.test",
   "File{1}.test",
   "File.{1}test",
   "File.te{1}st",
@@ -32,105 +33,45 @@ var tests = [
 function replace(name, x) {
   return name.replace(/\{1\}/, x);
 }
 
 function sanitize(name) {
   return replace(name, '_');
 }
 
-var gTests = [];
-function make_test(param, expected) {
-  gTests.push({
-    param: param,
-    expected: expected,
-  });
-}
-
-SimpleTest.waitForExplicitFinish();
-
-function load() {
-  var iframe = document.getElementById("test");
-  var gCallback = null;
-  function run_test(test, cb) {
-    var url = "unsafeBidiFileName.sjs?name=" + encodeURIComponent(test.param);
-    gCallback = cb;
-    iframe.src = url;
-  }
-
-  var gCounter = -1;
-  function run_next_test() {
-    if (++gCounter == gTests.length)
-      finish_test();
-    else
-      run_test(gTests[gCounter], run_next_test);
-  }
-
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
-  const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1";
-  const HELPERAPP_DIALOG_CID = SpecialPowers.wrap(SpecialPowers.Components).ID(SpecialPowers.Cc[HELPERAPP_DIALOG_CONTRACT].number);
-
-  const FAKE_CID = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].
-    getService(SpecialPowers.Ci.nsIUUIDGenerator).generateUUID();
+add_task(function* () {
+  let url = SimpleTest.getTestFileURL("unsafeBidi_chromeScript.js");
+  let chromeScript = SpecialPowers.loadChromeScript(url);
 
-  function HelperAppLauncherDialog() {}
-  HelperAppLauncherDialog.prototype = {
-    REASON_CANTHANDLE: 0,
-    REASON_SERVERREQUEST: 1,
-    REASON_TYPESNIFFED: 2,
-    show: function(aLauncher, aWindowContext, aReason) {
-      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-      var test = gTests[gCounter];
-      is(aLauncher.suggestedFileName, test.expected,
-         "The filename should be correctly sanitized");
-      gCallback();
-    },
-    QueryInterface: function(aIID) {
-      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-      if (aIID.equals(SpecialPowers.Ci.nsISupports) ||
-          aIID.equals(SpecialPowers.Ci.nsIHelperAppLauncherDialog))
-        return this;
-      throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
-    }
-  };
-
-  var factory = {
-    createInstance: function(aOuter, aIID) {
-      netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-      if (aOuter != null)
-        throw SpecialPowers.Cr.NS_ERROR_NO_AGGREGATION;
-      return new HelperAppLauncherDialog().QueryInterface(aIID);
-    }
-  };
-
-  SpecialPowers.wrap(SpecialPowers.Components).manager
-            .QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar)
-            .registerFactory(FAKE_CID, "",
-                             HELPERAPP_DIALOG_CONTRACT,
-                             factory);
-
-  function finish_test() {
-    SpecialPowers.wrap(SpecialPowers.Components).manager
-              .QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar)
-              .registerFactory(HELPERAPP_DIALOG_CID, "",
-                               HELPERAPP_DIALOG_CONTRACT,
-                               null);
-    SimpleTest.finish();
-  }
-
-  var i,j;
-
-  for (i = 0; i < tests.length; ++i) {
-    for (j in unsafeBidiChars) {
-      make_test(replace(tests[i], unsafeBidiChars[j]),
-                sanitize(tests[i]));
+  for (let test of tests) {
+    for (let char of unsafeBidiChars) {
+      let promiseName = new Promise(function(resolve) {
+        chromeScript.addMessageListener("suggestedFileName",
+                                        function listener(data) {
+          chromeScript.removeMessageListener("suggestedFileName", listener);
+          resolve(data);
+        });
+      });
+      let name = replace(test, char);
+      let expected = sanitize(test);
+      document.getElementById("test").src =
+        "unsafeBidiFileName.sjs?name=" + encodeURIComponent(name);
+      is((yield promiseName), expected, "got the expected sanitized name");
     }
   }
 
-  run_next_test();
-}
+  let promise = new Promise(function(resolve) {
+    chromeScript.addMessageListener("unregistered", function listener() {
+      chromeScript.removeMessageListener("unregistered", listener);
+      resolve();
+    });
+  });
+  chromeScript.sendAsyncMessage("unregister");
+  yield promise;
+
+  chromeScript.destroy();
+});
 
 ]]>
 </script>
 </body>
 </html>
-
new file mode 100644
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js
@@ -0,0 +1,28 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID =
+  Components.ID(Cc[HELPERAPP_DIALOG_CONTRACT].number);
+
+const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
+                   getService(Ci.nsIUUIDGenerator).generateUUID();
+
+function HelperAppLauncherDialog() {}
+HelperAppLauncherDialog.prototype = {
+  show: function(aLauncher, aWindowContext, aReason) {
+    sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog])
+};
+
+var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(FAKE_CID, "", HELPERAPP_DIALOG_CONTRACT,
+                          XPCOMUtils._getFactory(HelperAppLauncherDialog));
+
+addMessageListener("unregister", function() {
+  registrar.registerFactory(HELPERAPP_DIALOG_CID, "",
+                            HELPERAPP_DIALOG_CONTRACT, null);
+  sendAsyncMessage("unregistered");
+});