merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 09 Jan 2015 14:16:30 +0100
changeset 222927 7c03738e7a95a7d77f9cad311c02ea8d6d87e605
parent 222926 9f11ecffb3dfc1457e42218897817763fee5f4b0 (current diff)
parent 222828 4d9cadf294342e7053d61e95b35013d732c7b4ca (diff)
child 222928 e6756043d930ee61206b6927a63642b31df6e8ca
child 222968 9c11b5f0a4c3f0c4ed0f3dff11f3112fce459dda
child 223014 a5e88c805f02b20b1e36a43459b3317d443a67f3
push id10731
push usercbook@mozilla.com
push dateFri, 09 Jan 2015 14:51:37 +0000
treeherderfx-team@e6756043d930 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
merge fx-team to mozilla-central a=merge
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1461,16 +1461,18 @@
 
             let wasActive = document.activeElement == aBrowser;
 
             // Unhook our progress listener.
             let tab = this.getTabForBrowser(aBrowser);
             let index = tab._tPos;
             let filter = this.mTabFilters[index];
             aBrowser.webProgress.removeProgressListener(filter);
+            // Make sure the browser is destroyed so it unregisters from observer notifications
+            aBrowser.destroy();
 
             // Change the "remote" attribute.
             let parent = aBrowser.parentNode;
             let permanentKey = aBrowser.permanentKey;
             parent.removeChild(aBrowser);
             aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
             // Tearing down the browser gives a new permanentKey but we want to
             // keep the old one. Re-set it explicitly after unbinding from DOM.
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -118,16 +118,17 @@ skip-if = e10s # Bug 1093153 - no about:
 [browser_addKeywordSearch.js]
 [browser_search_favicon.js]
 [browser_alltabslistener.js]
 [browser_autocomplete_a11y_label.js]
 skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_oldschool_wrap.js]
+[browser_autocomplete_tag_star_visibility.js]
 [browser_backButtonFitts.js]
 skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
 skip-if = buildapp == 'mulet' || toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug 1094205 - places doesn't return the right thing in e10s mode, for some reason
 [browser_bug304198.js]
 skip-if = e10s
 [browser_bug321000.js]
@@ -192,18 +193,17 @@ skip-if = e10s # Bug 866413 - PageInfo d
 [browser_bug521216.js]
 [browser_bug533232.js]
 [browser_bug537013.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls replaceTabWithWindow)
 [browser_bug537474.js]
 skip-if = e10s # Bug 1102020 - test tries to use browserDOMWindow.openURI to open a link, and gets a null rv where it expects a window
 [browser_bug550565.js]
 [browser_bug553455.js]
-skip-if = true # Bug 1094312
-#skip-if = buildapp == 'mulet' || e10s # Bug 1066070 - I don't think either popup notifications nor addon install stuff works on mulet? ; for e10s, indefinite waiting halfway through the test, tracked in bug 1093586
+skip-if = buildapp == 'mulet' # Bug 1066070 - I don't think either popup notifications nor addon install stuff works on mulet?
 [browser_bug555224.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug555767.js]
 skip-if = e10s # Bug 1093373 - relies on browser.sessionHistory
 [browser_bug556061.js]
 [browser_bug559991.js]
 [browser_bug561623.js]
 skip-if = e10s
@@ -227,17 +227,16 @@ skip-if = e10s # Bug 1056146 - zoom test
 [browser_bug580638.js]
 [browser_bug580956.js]
 [browser_bug581242.js]
 [browser_bug581253.js]
 skip-if = e10s # Bug 1093756 - can't bookmark the data: url in e10s somehow
 [browser_bug581947.js]
 skip-if = e10s
 [browser_bug585558.js]
-skip-if = true # Bug 1094312 - Disabling browser_bug553455.js made this permafail
 [browser_bug585785.js]
 [browser_bug585830.js]
 [browser_bug590206.js]
 [browser_bug592338.js]
 skip-if = e10s # Bug 653065 - Make the lightweight theme web installer ready for e10s
 [browser_bug594131.js]
 [browser_bug595507.js]
 skip-if = e10s # Bug 1093677 - automated form submission from the test doesn't seem to quite work yet
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_autocomplete_tag_star_visibility.js
@@ -0,0 +1,105 @@
+add_task(function*() {
+  // This test is only relevant if UnifiedComplete is enabled.
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+
+  registerCleanupFunction(() => {
+    PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+    Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
+    Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+  });
+
+  function* addTagItem(tagName) {
+    let uri = NetUtil.newURI(`http://example.com/this/is/tagged/${tagName}`);
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         uri,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         `test ${tagName}`);
+    PlacesUtils.tagging.tagURI(uri, [tagName]);
+    yield PlacesTestUtils.addVisits([{uri: uri, title: `Test page with tag ${tagName}`}]);
+  }
+
+  // We use different tags for each part of the test, as otherwise the
+  // autocomplete code tries to be smart by using the previously cached element
+  // without updating it (since all parameters it knows about are the same).
+
+  let testcases = [{
+    description: "Test with suggest.bookmark=true",
+    tagName: "tagtest1",
+    prefs: {
+      "suggest.bookmark": true,
+    },
+    input: "tagtest1",
+    expected: {
+      type: "bookmark-tag",
+      typeImageVisible: true,
+    },
+  }, {
+    description: "Test with suggest.bookmark=false",
+    tagName: "tagtest2",
+    prefs: {
+      "suggest.bookmark": false,
+    },
+    input: "tagtest2",
+    expected: {
+      type: "tag",
+      typeImageVisible: false,
+    },
+  }, {
+    description: "Test with suggest.bookmark=true (again)",
+    tagName: "tagtest3",
+    prefs: {
+      "suggest.bookmark": true,
+    },
+    input: "tagtest3",
+    expected: {
+      type: "bookmark-tag",
+      typeImageVisible: true,
+    },
+  }, {
+    description: "Test with bookmark restriction token",
+    tagName: "tagtest4",
+    prefs: {
+      "suggest.bookmark": true,
+    },
+    input: "* tagtest4",
+    expected: {
+      type: "bookmark-tag",
+      typeImageVisible: true,
+    },
+  }, {
+    description: "Test with history restriction token",
+    tagName: "tagtest5",
+    prefs: {
+      "suggest.bookmark": true,
+    },
+    input: "^ tagtest5",
+    expected: {
+      type: "tag",
+      typeImageVisible: false,
+    },
+  }];
+
+
+  for (let testcase of testcases) {
+    info(`Test case: ${testcase.description}`);
+
+    yield addTagItem(testcase.tagName);
+    for (let prefName of Object.keys(testcase.prefs)) {
+      Services.prefs.setBoolPref(`browser.urlbar.${prefName}`, testcase.prefs[prefName]);
+    }
+
+    yield promiseAutocompleteResultPopup(testcase.input);
+    let result = gURLBar.popup.richlistbox.children[1];
+    ok(result && !result.collasped, "Should have result");
+
+    is(result.getAttribute("type"), testcase.expected.type, "Result should have expected type");
+    if (testcase.expected.typeImageVisible) {
+      is_element_visible(result._typeImage, "Type image should be visible");
+    } else {
+      is_element_hidden(result._typeImage, "Type image should be hidden");
+    }
+
+    gURLBar.popup.hidePopup();
+    yield promisePopupHidden(gURLBar.popup);
+  }
+});
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -2,41 +2,52 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
 const TESTROOT2 = "http://example.org/browser/toolkit/mozapps/extensions/test/xpinstall/";
 const SECUREROOT = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
 const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PROGRESS_NOTIFICATION = "addon-progress-notification";
 
 var rootDir = getRootDirectory(gTestPath);
 var path = rootDir.split('/');
 var chromeName = path[0] + '//' + path[2];
 var croot = chromeName + "/content/browser/toolkit/mozapps/extensions/test/xpinstall/";
 var jar = getJar(croot);
 if (jar) {
   var tmpdir = extractJarToTmp(jar);
   croot = 'file://' + tmpdir.path + '/';
 }
 const CHROMEROOT = croot;
 
 var gApp = document.getElementById("bundle_brand").getString("brandShortName");
 var gVersion = Services.appinfo.version;
 var check_notification;
 
-function wait_for_notification(aCallback) {
-  info("Waiting for notification");
+function wait_for_progress_notification(aCallback) {
+  wait_for_notification(PROGRESS_NOTIFICATION, aCallback, "popupshowing");
+}
+
+function wait_for_notification(aId, aCallback, aEvent = "popupshown") {
+  info("Waiting for " + aId + " notification");
   check_notification = function() {
-    PopupNotifications.panel.removeEventListener("popupshown", check_notification, false);
-    info("Saw notification");
+    // Ignore the progress notification unless that is the notification we want
+    if (aId != PROGRESS_NOTIFICATION && PopupNotifications.panel.childNodes[0].id == PROGRESS_NOTIFICATION)
+      return;
+
+    PopupNotifications.panel.removeEventListener(aEvent, check_notification, false);
+    info("Saw a notification");
     is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+    if (PopupNotifications.panel.childNodes.length)
+      is(PopupNotifications.panel.childNodes[0].id, aId, "Should have seen the right notification");
     aCallback(PopupNotifications.panel);
   };
-  PopupNotifications.panel.addEventListener("popupshown", check_notification, false);
+  PopupNotifications.panel.addEventListener(aEvent, check_notification, false);
 }
 
 function wait_for_notification_close(aCallback) {
   info("Waiting for notification to close");
   PopupNotifications.panel.addEventListener("popuphidden", function() {
     PopupNotifications.panel.removeEventListener("popuphidden", arguments.callee, false);
     aCallback();
   }, false);
@@ -98,19 +109,18 @@ function setup_redirect(aSettings) {
   req.send(null);
 }
 
 var TESTS = [
 function test_disabled_install() {
   Services.prefs.setBoolPref("xpinstall.enabled", false);
 
   // Wait for the disabled notification
-  wait_for_notification(function(aPanel) {
+  wait_for_notification("xpinstall-disabled-notification", function(aPanel) {
     let notification = aPanel.childNodes[0];
-    is(notification.id, "xpinstall-disabled-notification", "Should have seen installs disabled");
     is(notification.button.label, "Enable", "Should have seen the right button");
     is(notification.getAttribute("label"),
        "Software installation is currently disabled. Click Enable and try again.");
 
     wait_for_notification_close(function() {
       try {
         ok(Services.prefs.getBoolPref("xpinstall.enabled"), "Installation should be enabled");
       }
@@ -136,31 +146,29 @@ function test_disabled_install() {
     "XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_blocked_install() {
   // Wait for the blocked notification
-  wait_for_notification(function(aPanel) {
+  wait_for_notification("addon-install-blocked-notification", function(aPanel) {
     let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked");
     is(notification.button.label, "Allow", "Should have seen the right button");
     is(notification.getAttribute("label"),
        gApp + " prevented this site (example.com) from asking you to install " +
        "software on your computer.",
        "Should have seen the right message");
 
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "XPI Test will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         AddonManager.getAllInstalls(function(aInstalls) {
         is(aInstalls.length, 1, "Should be one pending install");
           aInstalls[0].cancel();
@@ -187,26 +195,22 @@ function test_blocked_install() {
     "XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_whitelisted_install() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "XPI Test will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         AddonManager.getAllInstalls(function(aInstalls) {
           is(aInstalls.length, 1, "Should be one pending install");
           aInstalls[0].cancel();
@@ -228,24 +232,20 @@ function test_whitelisted_install() {
     "XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_failed_download() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the failed notification
-    wait_for_notification(function(aPanel) {
+    wait_for_notification("addon-install-failed-notification", function(aPanel) {
       let notification = aPanel.childNodes[0];
-      is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
       is(notification.getAttribute("label"),
          "The add-on could not be downloaded because of a connection failure " +
          "on example.com.",
          "Should have seen the right message");
 
       Services.perms.remove("example.com", "install");
       wait_for_notification_close(runNextTest);
       gBrowser.removeTab(gBrowser.selectedTab);
@@ -259,24 +259,20 @@ function test_failed_download() {
     "XPI": "missing.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_corrupt_file() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the failed notification
-    wait_for_notification(function(aPanel) {
+    wait_for_notification("addon-install-failed-notification", function(aPanel) {
       let notification = aPanel.childNodes[0];
-      is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
       is(notification.getAttribute("label"),
          "The add-on downloaded from example.com could not be installed " +
          "because it appears to be corrupt.",
          "Should have seen the right message");
 
       Services.perms.remove("example.com", "install");
       wait_for_notification_close(runNextTest);
       gBrowser.removeTab(gBrowser.selectedTab);
@@ -290,24 +286,20 @@ function test_corrupt_file() {
     "XPI": "corrupt.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_incompatible() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the failed notification
-    wait_for_notification(function(aPanel) {
+    wait_for_notification("addon-install-failed-notification", function(aPanel) {
       let notification = aPanel.childNodes[0];
-      is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
       is(notification.getAttribute("label"),
          "XPI Test could not be installed because it is not compatible with " +
          gApp + " " + gVersion + ".",
          "Should have seen the right message");
 
       Services.perms.remove("example.com", "install");
       wait_for_notification_close(runNextTest);
       gBrowser.removeTab(gBrowser.selectedTab);
@@ -321,26 +313,22 @@ function test_incompatible() {
     "XPI": "incompatible.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_restartless() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.getAttribute("label"),
            "XPI Test has been installed successfully.",
            "Should have seen the right message");
 
         AddonManager.getAllInstalls(function(aInstalls) {
           is(aInstalls.length, 0, "Should be no pending installs");
 
           AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function(aAddon) {
@@ -364,26 +352,22 @@ function test_restartless() {
     "XPI": "restartless.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_multiple() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "2 add-ons will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         AddonManager.getAllInstalls(function(aInstalls) {
           is(aInstalls.length, 1, "Should be one pending install");
           aInstalls[0].cancel();
@@ -410,26 +394,22 @@ function test_multiple() {
     "Restartless XPI": "restartless.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_url() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "XPI Test will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         AddonManager.getAllInstalls(function(aInstalls) {
           is(aInstalls.length, 1, "Should be one pending install");
           aInstalls[0].cancel();
@@ -480,23 +460,20 @@ function test_wronghost() {
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.addEventListener("load", function() {
     if (gBrowser.currentURI.spec != TESTROOT2 + "enabled.html")
       return;
 
     gBrowser.removeEventListener("load", arguments.callee, true);
 
     // Wait for the progress notification
-    wait_for_notification(function(aPanel) {
-      let notification = aPanel.childNodes[0];
-      is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+    wait_for_progress_notification(function(aPanel) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-failed-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
         is(notification.getAttribute("label"),
            "The add-on downloaded from example.com could not be installed " +
            "because it appears to be corrupt.",
            "Should have seen the right message");
 
         wait_for_notification_close(runNextTest);
         gBrowser.removeTab(gBrowser.selectedTab);
       });
@@ -504,26 +481,22 @@ function test_wronghost() {
 
     gBrowser.loadURI(TESTROOT + "corrupt.xpi");
   }, true);
   gBrowser.loadURI(TESTROOT2 + "enabled.html");
 },
 
 function test_reload() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "XPI Test will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         function test_fail() {
           ok(false, "Reloading should not have hidden the notification");
         }
@@ -561,26 +534,22 @@ function test_reload() {
     "Unsigned XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_theme() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
         is(notification.button.label, "Restart Now", "Should have seen the right button");
         is(notification.getAttribute("label"),
            "Theme Test will be installed after you restart " + gApp + ".",
            "Should have seen the right message");
 
         AddonManager.getAddonByID("{972ce4c6-7e08-4474-a285-3208198ce6fd}", function(aAddon) {
           ok(aAddon.userDisabled, "Should be switching away from the default theme.");
           // Undo the pending theme switch
@@ -608,28 +577,23 @@ function test_theme() {
     "Theme XPI": "theme.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_renotify_blocked() {
   // Wait for the blocked notification
-  wait_for_notification(function(aPanel) {
+  wait_for_notification("addon-install-blocked-notification", function(aPanel) {
     let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked");
 
     wait_for_notification_close(function () {
       info("Timeouts after this probably mean bug 589954 regressed");
       executeSoon(function () {
-        wait_for_notification(function(aPanel) {
-          let notification = aPanel.childNodes[0];
-          is(notification.id, "addon-install-blocked-notification",
-             "Should have seen the install blocked - 2nd time");
-
+        wait_for_notification("addon-install-blocked-notification", function(aPanel) {
           AddonManager.getAllInstalls(function(aInstalls) {
           is(aInstalls.length, 2, "Should be two pending installs");
             aInstalls[0].cancel();
             aInstalls[1].cancel();
 
             info("Closing browser tab");
             wait_for_notification_close(runNextTest);
             gBrowser.removeTab(gBrowser.selectedTab);
@@ -648,45 +612,33 @@ function test_renotify_blocked() {
     "XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_renotify_installed() {
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
-    let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+  wait_for_progress_notification(function(aPanel) {
     // Wait for the install confirmation dialog
     wait_for_install_dialog(function(aWindow) {
       // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
-        let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
-
+      wait_for_notification("addon-install-complete-notification", function(aPanel) {
         // Dismiss the notification
         wait_for_notification_close(function () {
           // Install another
           executeSoon(function () {
             // Wait for the progress notification
-            wait_for_notification(function(aPanel) {
-              let notification = aPanel.childNodes[0];
-              is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
+            wait_for_progress_notification(function(aPanel) {
               // Wait for the install confirmation dialog
               wait_for_install_dialog(function(aWindow) {
                 info("Timeouts after this probably mean bug 589954 regressed");
 
                 // Wait for the complete notification
-                wait_for_notification(function(aPanel) {
-                  let notification = aPanel.childNodes[0];
-                  is(notification.id, "addon-install-complete-notification", "Should have seen the second install complete");
-
+                wait_for_notification("addon-install-complete-notification", function(aPanel) {
                   AddonManager.getAllInstalls(function(aInstalls) {
                   is(aInstalls.length, 1, "Should be one pending installs");
                     aInstalls[0].cancel();
 
                     Services.perms.remove("example.com", "install");
                     wait_for_notification_close(runNextTest);
                     gBrowser.removeTab(gBrowser.selectedTab);
                   });
@@ -714,96 +666,111 @@ function test_renotify_installed() {
   var triggers = encodeURIComponent(JSON.stringify({
     "XPI": "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_cancel_restart() {
+  function complete_install(callback) {
+    let url = TESTROOT + "slowinstall.sjs?continue=true"
+    NetUtil.asyncFetch(url, callback || (() => {}));
+  }
+
   // Wait for the progress notification
-  wait_for_notification(function(aPanel) {
+  wait_for_notification(PROGRESS_NOTIFICATION, function(aPanel) {
     let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
-
     // Close the notification
     let anchor = document.getElementById("addons-notification-icon");
     anchor.click();
     // Reopen the notification
     anchor.click();
 
     ok(PopupNotifications.isPanelOpen, "Notification should still be open");
     is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
     isnot(notification, aPanel.childNodes[0], "Should have reconstructed the notification UI");
     notification = aPanel.childNodes[0];
     is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
     let button = document.getAnonymousElementByAttribute(notification, "anonid", "cancel");
 
-    // Cancel the download
-    EventUtils.synthesizeMouse(button, 2, 2, {});
+    // Wait for the install to fully cancel
+    let install = notification.notification.options.installs[0];
+    install.addListener({
+      onDownloadCancelled: function() {
+        install.removeListener(this);
 
-    // Notification should have changed to cancelled
-    notification = aPanel.childNodes[0];
-    is(notification.id, "addon-install-cancelled-notification", "Should have seen the cancelled notification");
+        executeSoon(function() {
+          ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+          is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+          isnot(notification, aPanel.childNodes[0], "Should have reconstructed the notification UI");
+          notification = aPanel.childNodes[0];
+          is(notification.id, "addon-install-cancelled-notification", "Should have seen the cancelled notification");
+
+          // Wait for the install confirmation dialog
+          wait_for_install_dialog(function(aWindow) {
+            // Wait for the complete notification
+            wait_for_notification("addon-install-complete-notification", function(aPanel) {
+              let notification = aPanel.childNodes[0];
+              is(notification.button.label, "Restart Now", "Should have seen the right button");
+              is(notification.getAttribute("label"),
+                 "XPI Test will be installed after you restart " + gApp + ".",
+                 "Should have seen the right message");
 
-    // Wait for the install confirmation dialog
-    wait_for_install_dialog(function(aWindow) {
-      // Wait for the complete notification
-      wait_for_notification(function(aPanel) {
-        let notification = aPanel.childNodes[0];
-        is(notification.id, "addon-install-complete-notification", "Should have seen the install complete");
-        is(notification.button.label, "Restart Now", "Should have seen the right button");
-        is(notification.getAttribute("label"),
-           "XPI Test will be installed after you restart " + gApp + ".",
-           "Should have seen the right message");
+              AddonManager.getAllInstalls(function(aInstalls) {
+                is(aInstalls.length, 1, "Should be one pending install");
+                aInstalls[0].cancel();
+
+                Services.perms.remove("example.com", "install");
+                wait_for_notification_close(runNextTest);
+                gBrowser.removeTab(gBrowser.selectedTab);
+              });
+            });
+
+            aWindow.document.documentElement.acceptDialog();
+          });
 
-        AddonManager.getAllInstalls(function(aInstalls) {
-          is(aInstalls.length, 1, "Should be one pending install");
-          aInstalls[0].cancel();
+          // Restart the download
+          EventUtils.synthesizeMouseAtCenter(notification.button, {});
 
-          Services.perms.remove("example.com", "install");
-          wait_for_notification_close(runNextTest);
-          gBrowser.removeTab(gBrowser.selectedTab);
+          // Should be back to a progress notification
+          ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+          is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+          notification = aPanel.childNodes[0];
+          is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+
+          complete_install();
         });
-      });
-
-      aWindow.document.documentElement.acceptDialog();
+      }
     });
 
-    // Restart the download
-    EventUtils.synthesizeMouse(notification.button, 20, 10, {});
-
-    // Should be back to a progress notification
-    ok(PopupNotifications.isPanelOpen, "Notification should still be open");
-    is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
-    notification = aPanel.childNodes[0];
-    is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+    // Cancel the download
+    EventUtils.synthesizeMouseAtCenter(button, {});
   });
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
 
   var triggers = encodeURIComponent(JSON.stringify({
-    "XPI": "unsigned.xpi"
+    "XPI": "slowinstall.sjs?file=unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 },
 
 function test_failed_security() {
   Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
 
   setup_redirect({
     "Location": TESTROOT + "unsigned.xpi"
   });
 
   // Wait for the blocked notification
-  wait_for_notification(function(aPanel) {
+  wait_for_notification("addon-install-blocked-notification", function(aPanel) {
     let notification = aPanel.childNodes[0];
-    is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked");
 
     // Click on Allow
     EventUtils.synthesizeMouse(notification.button, 20, 10, {});
 
     // Notification should have changed to progress notification
     ok(PopupNotifications.isPanelOpen, "Notification should still be open");
     is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
     notification = aPanel.childNodes[0];
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -508,18 +508,21 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * FxA sign in/up link component.
    */
   var AuthLink = React.createClass({displayName: "AuthLink",
+    mixins: [sharedMixins.WindowCloseMixin],
+
     handleSignUpLinkClick: function() {
       navigator.mozLoop.logInToFxA();
+      this.closeWindow();
     },
 
     render: function() {
       if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
         return null;
       }
       return (
         React.createElement("p", {className: "signin-link"}, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -508,18 +508,21 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * FxA sign in/up link component.
    */
   var AuthLink = React.createClass({
+    mixins: [sharedMixins.WindowCloseMixin],
+
     handleSignUpLinkClick: function() {
       navigator.mozLoop.logInToFxA();
+      this.closeWindow();
     },
 
     render: function() {
       if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
         return null;
       }
       return (
         <p className="signin-link">
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ b/browser/components/loop/content/shared/js/feedbackViews.js
@@ -177,33 +177,34 @@ loop.shared.views.FeedbackView = (functi
     },
 
     getInitialState: function() {
       return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
     },
 
     componentDidMount: function() {
       this._timer = setInterval(function() {
+      if (this.state.countdown == 1) {
+        clearInterval(this._timer);
+        if (this.props.onAfterFeedbackReceived) {
+          this.props.onAfterFeedbackReceived();
+        }
+        return;
+      }
         this.setState({countdown: this.state.countdown - 1});
       }.bind(this), 1000);
     },
 
     componentWillUnmount: function() {
       if (this._timer) {
         clearInterval(this._timer);
       }
     },
 
     render: function() {
-      if (this.state.countdown < 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-      }
       return (
         React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")}, 
           React.createElement("p", {className: "info thank-you"}, 
             l10n.get("feedback_window_will_close_in2", {
               countdown: this.state.countdown,
               num: this.state.countdown
             }))
         )
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ b/browser/components/loop/content/shared/js/feedbackViews.jsx
@@ -177,33 +177,34 @@ loop.shared.views.FeedbackView = (functi
     },
 
     getInitialState: function() {
       return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
     },
 
     componentDidMount: function() {
       this._timer = setInterval(function() {
+      if (this.state.countdown == 1) {
+        clearInterval(this._timer);
+        if (this.props.onAfterFeedbackReceived) {
+          this.props.onAfterFeedbackReceived();
+        }
+        return;
+      }
         this.setState({countdown: this.state.countdown - 1});
       }.bind(this), 1000);
     },
 
     componentWillUnmount: function() {
       if (this._timer) {
         clearInterval(this._timer);
       }
     },
 
     render: function() {
-      if (this.state.countdown < 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-      }
       return (
         <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
           <p className="info thank-you">{
             l10n.get("feedback_window_will_close_in2", {
               countdown: this.state.countdown,
               num: this.state.countdown
             })}</p>
         </FeedbackLayout>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -248,40 +248,60 @@ describe("loop.panel", function() {
 
           expect(callTab.getDOMNode().classList.contains("selected"))
             .to.be.true;
         });
       });
     });
 
     describe("AuthLink", function() {
+
+      beforeEach(function() {
+        navigator.mozLoop.calls = { clearCallInProgress: function() {} };
+      });
+
+      afterEach(function() {
+        delete navigator.mozLoop.logInToFxA;
+        delete navigator.mozLoop.calls;
+        navigator.mozLoop.fxAEnabled = true;
+      });
+
       it("should trigger the FxA sign in/up process when clicking the link",
         function() {
           navigator.mozLoop.loggedInToFxA = false;
           navigator.mozLoop.logInToFxA = sandbox.stub();
 
           var view = createTestPanelView();
 
           TestUtils.Simulate.click(
             view.getDOMNode().querySelector(".signin-link a"));
 
           sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
         });
 
+      it("should close the panel after clicking the link",
+        function() {
+          navigator.mozLoop.loggedInToFxA = false;
+          navigator.mozLoop.logInToFxA = sandbox.stub();
+
+          var view = createTestPanelView();
+
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector(".signin-link a"));
+
+          sinon.assert.calledOnce(fakeWindow.close);
+        });
+
       it("should be hidden if FxA is not enabled",
         function() {
           navigator.mozLoop.fxAEnabled = false;
           var view = TestUtils.renderIntoDocument(
             React.createElement(loop.panel.AuthLink));
           expect(view.getDOMNode()).to.be.null;
       });
-
-      afterEach(function() {
-        navigator.mozLoop.fxAEnabled = true;
-      });
     });
 
     describe("SettingsDropdown", function() {
       beforeEach(function() {
         navigator.mozLoop.logInToFxA = sandbox.stub();
         navigator.mozLoop.logOutFromFxA = sandbox.stub();
         navigator.mozLoop.openFxASettings = sandbox.stub();
       });
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -83,31 +83,35 @@
   <vbox id="trackingprotectionbox" hidden="true">
     <hbox align="center">
       <checkbox id="trackingProtection"
                 preference="privacy.trackingprotection.enabled"
                 accesskey="&trackingProtection.accesskey;"
                 label="&trackingProtection.label;" />
       <image id="trackingProtectionImage"/>
     </hbox>
-    <label id="trackingProtectionLearnMore"
-           class="text-link"
-           value="&trackingProtectionLearnMore.label;"/>
-    <separator/>
+    <hbox align="center"
+          class="indent">
+      <label id="trackingProtectionLearnMore"
+             class="text-link"
+             value="&trackingProtectionLearnMore.label;"/>
+    </hbox>
   </vbox>
-  <checkbox id="privacyDoNotTrackCheckbox"
-            label="&dntTrackingNotOkay.label2;"
-            accesskey="&dntTrackingNotOkay.accesskey;"
-            preference="privacy.donottrackheader.enabled"/>
-  <separator class="thin"/>
   <vbox>
-    <hbox pack="end">
-      <spacer flex="1"/>
-      <label class="text-link" id="doNotTrackInfo"
-            href="https://www.mozilla.org/dnt">
+    <hbox align="center">
+      <checkbox id="privacyDoNotTrackCheckbox"
+                label="&dntTrackingNotOkay.label2;"
+                accesskey="&dntTrackingNotOkay.accesskey;"
+                preference="privacy.donottrackheader.enabled"/>
+    </hbox>
+    <hbox align="center"
+          class="indent">
+      <label id="doNotTrackInfo"
+             class="text-link"
+             href="https://www.mozilla.org/dnt">
         &doNotTrackInfo.label;
       </label>
     </hbox>
   </vbox>
 </groupbox>
 
 <!-- History -->
 <groupbox id="historyGroup" data-category="panePrivacy" hidden="true">
--- a/browser/components/preferences/privacy.xul
+++ b/browser/components/preferences/privacy.xul
@@ -90,34 +90,40 @@
     <groupbox id="trackingGroup" align="start">
       <caption label="&tracking.label;"/>
       <vbox id="trackingprotectionbox" hidden="true">
         <hbox align="center">
           <checkbox id="trackingProtection"
                     preference="privacy.trackingprotection.enabled"
                     accesskey="&trackingProtection.accesskey;"
                     label="&trackingProtection.label;" />
-          <image id="trackingProtectionImage" src="chrome://browser/skin/bad-content-blocked-16.png"/>
+          <image id="trackingProtectionImage"
+                 src="chrome://browser/skin/bad-content-blocked-16.png"/>
         </hbox>
-        <label id="trackingProtectionLearnMore"
-               class="text-link"
-               value="&trackingProtectionLearnMore.label;"/>
-        <separator/>
+        <hbox align="center"
+              class="indent">
+          <label id="trackingProtectionLearnMore"
+                 class="text-link"
+                 value="&trackingProtectionLearnMore.label;"/>
+        </hbox>
       </vbox>
-      <checkbox id="privacyDoNotTrackCheckbox"
-                label="&dntTrackingNotOkay.label2;"
-                accesskey="&dntTrackingNotOkay.accesskey;"
-                preference="privacy.donottrackheader.enabled"/>
-      <separator class="thin"/>
       <vbox>
-        <hbox pack="end">
-          <spacer flex="1"/>
-          <label class="text-link" id="doNotTrackInfo"
-                href="https://www.mozilla.org/dnt"
-                value="&doNotTrackInfo.label;"/>
+        <hbox align="center">
+          <checkbox id="privacyDoNotTrackCheckbox"
+                    label="&dntTrackingNotOkay.label2;"
+                    accesskey="&dntTrackingNotOkay.accesskey;"
+                    preference="privacy.donottrackheader.enabled"/>
+        </hbox>
+        <hbox align="center"
+              class="indent">
+          <label id="doNotTrackInfo"
+                 class="text-link"
+                 href="https://www.mozilla.org/dnt">
+            &doNotTrackInfo.label;
+          </label>
         </hbox>
       </vbox>
 
     </groupbox>
 
     <!-- History -->
     <groupbox id="historyGroup">
       <caption label="&history.label;"/>
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -4,33 +4,38 @@
 
 Components.utils.import("resource://services-sync/main.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
   return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+  "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
+  "resource://services-sync/FxaMigrator.jsm");
+
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
 const PAGE_NEEDS_UPDATE = 2;
 const PAGE_PLEASE_WAIT = 3;
 const FXA_PAGE_LOGGED_OUT = 4;
 const FXA_PAGE_LOGGED_IN = 5;
 
 // Indexes into the "login status" deck.
 // We are in a successful verified state - everything should work!
 const FXA_LOGIN_VERIFIED = 0;
 // We have logged in to an unverified account.
 const FXA_LOGIN_UNVERIFIED = 1;
 // We are logged in locally, but the server rejected our credentials.
 const FXA_LOGIN_FAILED = 2;
 
 let gSyncPane = {
-  _stringBundle: null,
   prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
               "engine.tabs", "engine.history"],
 
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
   },
 
   set page(val) {
@@ -84,44 +89,60 @@ let gSyncPane = {
 
   _init: function () {
     let topics = ["weave:service:login:error",
                   "weave:service:login:finish",
                   "weave:service:start-over:finish",
                   "weave:service:setup-complete",
                   "weave:service:logout:finish",
                   FxAccountsCommon.ONVERIFIED_NOTIFICATION];
+    let migrateTopic = "fxa-migration:state-changed";
 
     // Add the observers now and remove them on unload
     //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
     //        of `this`. Fix in a followup. (bug 583347)
     topics.forEach(function (topic) {
       Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this);
     }, this);
+    // The FxA migration observer is a special case.
+    Weave.Svc.Obs.add(migrateTopic, this.updateMigrationState, this);
+
     window.addEventListener("unload", function() {
-      topics.forEach(function (topic) {
+      topics.forEach(topic => {
         Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
-      }, gSyncPane);
-    }, false);
+      });
+      Weave.Svc.Obs.remove(migrateTopic, this.updateMigrationState, this);
+    }.bind(this), false);
 
-    this._stringBundle =
-      Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
+    XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
+      return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
+    });
+
+    XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
+      return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+    });
+
     this.updateWeavePrefs();
   },
 
   updateWeavePrefs: function () {
+    // ask the migration module to broadcast its current state (and nothing will
+    // happen if it's not loaded - which is good, as that means no migration
+    // is pending/necessary) - we don't want to suck that module in just to
+    // find there's nothing to do.
+    Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
+
     let service = Components.classes["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
     // service.fxAccountsEnabled is false iff sync is already configured for
     // the legacy provider.
     if (service.fxAccountsEnabled) {
       // determine the fxa status...
       this.page = PAGE_PLEASE_WAIT;
-      Components.utils.import("resource://gre/modules/FxAccounts.jsm");
       fxAccounts.getSignedInUser().then(data => {
         if (!data) {
           this.page = FXA_PAGE_LOGGED_OUT;
           return;
         }
         this.page = FXA_PAGE_LOGGED_IN;
         // We are logged in locally, but maybe we are in a state where the
         // server rejected our credentials (eg, password changed on the server)
@@ -170,16 +191,88 @@ let gSyncPane = {
     } else {
       this.page = PAGE_HAS_ACCOUNT;
       document.getElementById("accountName").value = Weave.Service.identity.account;
       document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP-normal").hidden = this._usingCustomServer;
     }
   },
 
+  updateMigrationState: function(subject, state) {
+    let selIndex;
+    let container = document.getElementById("sync-migration");
+    switch (state) {
+      case fxaMigrator.STATE_USER_FXA: {
+        let sb = this._accountsStringBundle;
+        // There are 2 cases here - no email address means it is an offer on
+        // the first device (so the user is prompted to create an account).
+        // If there is an email address it is the "join the party" flow, so the
+        // user is prompted to sign in with the address they previously used.
+        let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
+        let elt = document.getElementById("sync-migrate-upgrade-description");
+        elt.textContent = email ?
+                          sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
+                                                  [email], 1) :
+                          sb.GetStringFromName("needUserLong");
+
+        // The "upgrade" button.
+        let button = document.getElementById("sync-migrate-upgrade");
+        button.setAttribute("label",
+                            sb.GetStringFromName(email
+                                                 ? "signInAfterUpgradeOnOtherDevice.label"
+                                                 : "upgradeToFxA.label"));
+        button.setAttribute("accesskey",
+                            sb.GetStringFromName(email
+                                                 ? "signInAfterUpgradeOnOtherDevice.accessKey"
+                                                 : "upgradeToFxA.accessKey"));
+        // The "unlink" button - this is only shown for first migration
+        button = document.getElementById("sync-migrate-unlink");
+        if (email) {
+          button.hidden = true;
+        } else {
+          button.setAttribute("label", sb.GetStringFromName("unlinkMigration.label"));
+          button.setAttribute("accesskey", sb.GetStringFromName("unlinkMigration.accessKey"));
+        }
+        selIndex = 0;
+        break;
+      }
+      case fxaMigrator.STATE_USER_FXA_VERIFIED: {
+        let sb = this._accountsStringBundle;
+        let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
+        let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
+        let elt = document.getElementById("sync-migrate-verify-description");
+        elt.textContent = label;
+        // The "resend" button.
+        let button = document.getElementById("sync-migrate-resend");
+        button.setAttribute("label", sb.GetStringFromName("resendVerificationEmail.label"));
+        button.setAttribute("accesskey", sb.GetStringFromName("resendVerificationEmail.accessKey"));
+        // The "forget" button.
+        button = document.getElementById("sync-migrate-forget");
+        button.setAttribute("label", sb.GetStringFromName("forgetMigration.label"));
+        button.setAttribute("accesskey", sb.GetStringFromName("forgetMigration.accessKey"));
+        selIndex = 1;
+        break;
+      }
+      default:
+        if (state) { // |null| is expected, but everything else is not.
+          Cu.reportError("updateMigrationState has unknown state: " + state);
+        }
+        if (!container.hidden) {
+          window.innerHeight -= container.clientHeight;
+          container.hidden = true;
+        }
+        return;
+    }
+    document.getElementById("sync-migration-deck").selectedIndex = selIndex;
+    if (container.hidden) {
+      container.hidden = false;
+      window.innerHeight += container.clientHeight;
+    }
+  },
+
   startOver: function (showDialog) {
     if (showDialog) {
       let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
                   Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
                   Services.prompt.BUTTON_POS_1_DEFAULT;
       let buttonChoice =
         Services.prompt.confirmEx(window,
                                   this._stringBundle.GetStringFromName("syncUnlink.title"),
@@ -277,17 +370,16 @@ let gSyncPane = {
   },
 
   manageFirefoxAccount: function() {
     let url = Services.prefs.getCharPref("identity.fxaccounts.settings.uri");
     this.openContentInBrowser(url);
   },
 
   verifyFirefoxAccount: function() {
-    Components.utils.import("resource://gre/modules/FxAccounts.jsm");
     fxAccounts.resendVerificationEmail().then(() => {
       fxAccounts.getSignedInUser().then(data => {
         let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
         let title = sb.GetStringFromName("verificationSentTitle");
         let heading = sb.formatStringFromName("verificationSentHeading",
                                               [data.email], 1);
         let description = sb.GetStringFromName("verificationSentDescription");
 
@@ -318,17 +410,16 @@ let gSyncPane = {
                         (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
                         ps.BUTTON_POS_1_DEFAULT;
       let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags,
                                               continueLabel, null, null, null, {});
       if (pressed != 0) { // 0 is the "continue" button
         return;
       }
     }
-    Components.utils.import('resource://gre/modules/FxAccounts.jsm');
     fxAccounts.signOut().then(() => {
       this.updateWeavePrefs();
     });
   },
 
   openQuotaDialog: function () {
     let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
     if (win) {
@@ -351,10 +442,48 @@ let gSyncPane = {
       window.openDialog("chrome://browser/content/sync/addDevice.xul",
                         "syncAddDevice", "centerscreen,chrome,resizable=no");
     }
   },
 
   resetSync: function () {
     this.openSetup("reset");
   },
+
+  // click handlers for the FxA migration.
+  migrateUpgrade: function() {
+    fxaMigrator.getFxAccountCreationOptions().then(({url, options}) => {
+      this.openContentInBrowser(url, options);
+    });
+  },
+
+  migrateForget: function() {
+    fxaMigrator.forgetFxAccount();
+  },
+
+  migrateResend: function() {
+    fxaMigrator.resendVerificationMail(window);
+  },
+
+  // When the "Unlink" button in the migration header is selected we display
+  // a slightly different message.
+  startOverMigration: function () {
+    let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+                Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL +
+                Services.prompt.BUTTON_POS_1_DEFAULT;
+    let sb = this._accountsStringBundle;
+    let buttonChoice =
+      Services.prompt.confirmEx(window,
+                                sb.GetStringFromName("unlinkVerificationTitle"),
+                                sb.GetStringFromName("unlinkVerificationDescription"),
+                                flags,
+                                sb.GetStringFromName("unlinkVerificationConfirm"),
+                                null, null, null, {});
+
+    // If the user selects cancel, just bail
+    if (buttonChoice == 1)
+      return;
+
+    Weave.Service.startOver();
+    this.updateWeavePrefs();
+  },
 };
 
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -31,16 +31,40 @@
     </preferences>
 
 
     <script type="application/javascript"
             src="chrome://browser/content/preferences/sync.js"/>
     <script type="application/javascript"
             src="chrome://browser/content/sync/utils.js"/>
 
+      <vbox id="sync-migration" flex="1" hidden="true">
+
+        <deck id="sync-migration-deck">
+          <!-- When we are in the "need FxA user" state -->
+          <hbox align="center">
+            <description id="sync-migrate-upgrade-description" flex="1"/>
+            <spacer flex="1"/>
+            <button id="sync-migrate-unlink"
+                    onclick="event.stopPropagation(); gSyncPane.startOverMigration();"/>
+            <button id="sync-migrate-upgrade"
+                    onclick="event.stopPropagation(); gSyncPane.migrateUpgrade();"/>
+          </hbox>
+
+          <!-- When we are in the "need the user to be verified" state -->
+          <hbox align="center">
+            <description id="sync-migrate-verify-description" flex="1"/>
+            <spacer flex="1"/>
+            <button id="sync-migrate-forget"
+                    onclick="event.stopPropagation(); gSyncPane.migrateForget();"/>
+            <button id="sync-migrate-resend"
+                    onclick="event.stopPropagation(); gSyncPane.migrateResend();"/>
+          </hbox>
+        </deck>
+      </vbox>
 
       <deck id="weavePrefsDeck">
 
         <!-- These panels are for the "legacy" sync provider -->
         <vbox id="noAccount" align="center">
           <spacer flex="1"/>
           <description id="syncDesc">
             &weaveDesc.label;
--- a/browser/devtools/fontinspector/font-inspector.js
+++ b/browser/devtools/fontinspector/font-inspector.js
@@ -93,18 +93,19 @@ FontInspector.prototype = {
         !this.inspector.selection.isConnected() ||
         !this.inspector.selection.isElementNode() ||
         this.chromeDoc.body.classList.contains("dim")) {
       return;
     }
 
     this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
 
-    let fillStyle = (Services.prefs.getCharPref("devtools.theme") == "light") ?
-        "black" : "white";
+    // Assume light theme colors as the default (see also bug 1118179).
+    let fillStyle = (Services.prefs.getCharPref("devtools.theme") == "dark") ?
+        "white" : "black";
     let options = {
       includePreviews: true,
       previewFillStyle: fillStyle
     }
     let fonts = [];
     if (showAllFonts){
       fonts = yield this.pageStyle.getAllUsedFontFaces(options)
                       .then(null, console.error);
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -28,16 +28,18 @@ devtools.lazyRequireGetter(this, "Memory
 devtools.lazyRequireGetter(this, "Waterfall",
   "devtools/timeline/waterfall", true);
 devtools.lazyRequireGetter(this, "MarkerDetails",
   "devtools/timeline/marker-details", true);
 devtools.lazyRequireGetter(this, "CallView",
   "devtools/profiler/tree-view", true);
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/profiler/tree-model", true);
+devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/timeline/global", true);
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "LineGraphWidget",
   "resource:///modules/devtools/Graphs.jsm");
 
 // Events emitted by various objects in the panel.
 const EVENTS = {
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -11,17 +11,17 @@ let WaterfallView = {
    * Sets up the view with event binding.
    */
   initialize: Task.async(function *() {
     this._onRecordingStarted = this._onRecordingStarted.bind(this);
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
 
-    this.graph = new Waterfall($("#waterfall-graph"), $("#details-pane"));
+    this.graph = new Waterfall($("#waterfall-graph"), $("#details-pane"), TIMELINE_BLUEPRINT);
     this.markerDetails = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
 
     this.graph.on("selected", this._onMarkerSelected);
     this.graph.on("unselected", this._onMarkerSelected);
     this.markerDetails.on("resize", this._onResize);
 
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -75,17 +75,17 @@ let OverviewView = {
     this.framerateGraph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
     yield this.framerateGraph.ready();
   }),
 
   /**
    * Sets up the markers overivew graph.
    */
   _showMarkersGraph: Task.async(function *() {
-    this.markersOverview = new MarkersOverview($("#markers-overview"));
+    this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
     this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
     this.markersOverview.bodyHeight = MARKERS_GRAPH_BODY_HEIGHT;
     this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
     yield this.markersOverview.ready();
 
     CanvasGraphUtils.linkAnimation(this.framerateGraph, this.markersOverview);
     CanvasGraphUtils.linkSelection(this.framerateGraph, this.markersOverview);
   }),
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -70,17 +70,17 @@ function Waterfall(parent, container, bl
   this._parent.appendChild(this._listContents);
 
   this.setupKeys();
 
   this._isRTL = this._getRTL();
 
   // Lazy require is a bit slow, and these are hot objects.
   this._l10n = L10N;
-  this._blueprint = blueprint
+  this._blueprint = blueprint;
   this._setNamedTimeout = setNamedTimeout;
   this._clearNamedTimeout = clearNamedTimeout;
 
   // Selected row index. By default, we want the first
   // row to be selected.
   this._selectedRowIdx = 0;
 
   // Default rowCount
--- a/browser/themes/linux/preferences/preferences.css
+++ b/browser/themes/linux/preferences/preferences.css
@@ -184,9 +184,30 @@ label.small {
   margin: 5px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+/**
+ * Sync migration
+ */
+#sync-migration {
+  border: 1px solid rgba(0, 0, 0, 0.32);
+  background-color: InfoBackground;
+  color: InfoText;
+  text-shadow: none;
+  margin: 5px 0 0 0;
+  animation: fadein 3000ms;
+}
+
+#sync-migration description {
+  margin: 8px;
+}
+
+@keyframes fadein {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+
 %endif
--- a/browser/themes/osx/preferences/preferences.css
+++ b/browser/themes/osx/preferences/preferences.css
@@ -245,9 +245,30 @@ html|a.inline-link:-moz-focusring {
   margin: 12px 4px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+/**
+ * Sync migration
+ */
+#sync-migration {
+  border: 1px solid rgba(0, 0, 0, 0.32);
+  background-color: InfoBackground;
+  color: InfoText;
+  text-shadow: none;
+  margin: 5px 0 0 0;
+  animation: fadein 3000ms;
+}
+
+#sync-migration description {
+  margin: 8px;
+}
+
+@keyframes fadein {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+
 %endif
--- a/browser/themes/windows/preferences/preferences.css
+++ b/browser/themes/windows/preferences/preferences.css
@@ -171,9 +171,30 @@ label.small {
   margin: 6px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+/**
+ * Sync migration
+ */
+#sync-migration {
+  border: 1px solid rgba(0, 0, 0, 0.32);
+  background-color: InfoBackground;
+  color: InfoText;
+  text-shadow: none;
+  margin: 5px 0 0 0;
+  animation: fadein 3000ms;
+}
+
+#sync-migration description {
+  margin: 8px;
+}
+
+@keyframes fadein {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+
 %endif
--- a/mobile/android/modules/WebappManager.jsm
+++ b/mobile/android/modules/WebappManager.jsm
@@ -6,16 +6,17 @@
 
 this.EXPORTED_SYMBOLS = ["WebappManager"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl";
 
 Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
@@ -132,40 +133,39 @@ this.WebappManager = {
     // Populate the query part of the URL with the manifest URL parameter.
     let params = {
       manifestUrl: aManifestUrl,
     };
     generatorUrl.query =
       [p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&");
     debug("downloading APK from " + generatorUrl.spec);
 
-    let file = Cc["@mozilla.org/download-manager;1"].
-               getService(Ci.nsIDownloadManager).
-               defaultDownloadsDirectory.
-               clone();
-    file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
-    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-    debug("downloading APK to " + file.path);
+    Downloads.getSystemDownloadsDirectory().then(function(downloadsDir) {
+      let file = new FileUtils.File(downloadsDir);
+      file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
+      file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+      debug("downloading APK to " + file.path);
 
-    let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
-    worker.onmessage = function(event) {
-      let { type, message } = event.data;
+      let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
+      worker.onmessage = function(event) {
+        let { type, message } = event.data;
+
+        worker.terminate();
 
-      worker.terminate();
+        if (type == "success") {
+          deferred.resolve(file.path);
+        } else { // type == "failure"
+          debug("error downloading APK: " + message);
+          deferred.reject(message);
+        }
+      }
 
-      if (type == "success") {
-        deferred.resolve(file.path);
-      } else { // type == "failure"
-        debug("error downloading APK: " + message);
-        deferred.reject(message);
-      }
-    }
-
-    // Trigger the download.
-    worker.postMessage({ url: generatorUrl.spec, path: file.path });
+      // Trigger the download.
+      worker.postMessage({ url: generatorUrl.spec, path: file.path });
+    });
 
     return deferred.promise;
   },
 
   _deleteAppcachePath: function(aManifest) {
     // We don't yet support pre-installing an appcache because it isn't clear
     // how to do it without degrading the user experience (since users expect
     // apps to be available after the system tells them they've been installed,
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -1,15 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.search;
 
+import java.net.MalformedURLException;
 import java.net.URISyntaxException;
+import java.net.URL;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.search.providers.SearchEngine;
 
 import android.content.Intent;
@@ -35,16 +37,18 @@ public class PostSearchFragment extends 
     private static final String LOG_TAG = "PostSearchFragment";
 
     private SearchEngine engine;
 
     private ProgressBar progressBar;
     private WebView webview;
     private View errorView;
 
+    private String resultsPageHost;
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false);
 
         progressBar = (ProgressBar) mainView.findViewById(R.id.progress_bar);
 
         webview = (WebView) mainView.findViewById(R.id.webview);
@@ -67,16 +71,17 @@ public class PostSearchFragment extends 
     }
 
     public void startSearch(SearchEngine engine, String query) {
         this.engine = engine;
 
         final String url = engine.resultsUriForQuery(query);
         // Only load urls if the url is different than the webview's current url.
         if (!TextUtils.equals(webview.getUrl(), url)) {
+            resultsPageHost = null;
             webview.loadUrl(Constants.ABOUT_BLANK);
             webview.loadUrl(url);
         }
     }
 
     /**
      * A custom WebViewClient that intercepts every page load. This allows
      * us to decide whether to load the url here, or send it to Android
@@ -90,24 +95,32 @@ public class PostSearchFragment extends 
         @Override
         public void onPageStarted(WebView view, final String url, Bitmap favicon) {
             // Reset the error state.
             networkError = false;
         }
 
         @Override
         public boolean shouldOverrideUrlLoading(WebView view, String url) {
-            // Ignore about:blank URL loads.
-            if (TextUtils.equals(url, Constants.ABOUT_BLANK)) {
+            // Ignore about:blank URL loads and the first results page we try to load.
+            if (TextUtils.equals(url, Constants.ABOUT_BLANK) || resultsPageHost == null) {
                 return false;
             }
 
-            // If the URL is a results page, don't override the URL load, but
+            String host = null;
+            try {
+                host = new URL(url).getHost();
+            } catch (MalformedURLException e) {
+                Log.e(LOG_TAG, "Error getting host from URL loading in webview", e);
+            }
+
+            // If the host name is the same as the results page, don't override the URL load, but
             // do update the query in the search bar if possible.
-            if (engine.isSearchResultsPage(url)) {
+            if (TextUtils.equals(resultsPageHost, host)) {
+                // This won't work for results pages that redirect (e.g. Google in different country)
                 final String query = engine.queryForResultsUrl(url);
                 if (!TextUtils.isEmpty(query)) {
                     ((AcceptsSearchQuery) getActivity()).onQueryChange(query);
                 }
                 return false;
             }
 
             try {
@@ -127,17 +140,17 @@ public class PostSearchFragment extends 
 
                 startActivity(i);
                 return true;
             } catch (URISyntaxException e) {
                 Log.e(LOG_TAG, "Error parsing intent URI", e);
             }
 
             return false;
-}
+        }
 
         @Override
         public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
             Log.e(LOG_TAG, "Error loading search results: " + description);
 
             networkError = true;
 
             if (errorView == null) {
@@ -161,16 +174,24 @@ public class PostSearchFragment extends 
 
         @Override
         public void onPageFinished(WebView view, String url) {
             // Make sure the error view is hidden if the network error was fixed.
             if (errorView != null) {
                 errorView.setVisibility(networkError ? View.VISIBLE : View.GONE);
                 webview.setVisibility(networkError ? View.GONE : View.VISIBLE);
             }
+
+            if (!TextUtils.equals(url, Constants.ABOUT_BLANK) && resultsPageHost == null) {
+                try {
+                    resultsPageHost = new URL(url).getHost();
+                } catch (MalformedURLException e) {
+                    Log.e(LOG_TAG, "Error getting host from results page URL", e);
+                }
+            }
         }
     }
 
     /**
      * A custom WebChromeClient that allows us to inject CSS into
      * the head of the HTML and to monitor pageload progress.
      *
      * We use the WebChromeClient because it provides a hook to the titleReceived
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java
+++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngine.java
@@ -215,24 +215,16 @@ public class SearchEngine {
         return shortName;
     }
 
     public String getIconURL() {
         return iconURL;
     }
 
     /**
-     * Determine whether a particular url belongs to this search engine. If not,
-     * the url will be sent to Fennec.
-     */
-    public boolean isSearchResultsPage(String url) {
-        return getResultsUri().getAuthority().equalsIgnoreCase(Uri.parse(url).getAuthority());
-    }
-
-    /**
      * Finds the search query encoded in a given results URL.
      *
      * @param url Current results URL.
      * @return The search query, or an empty string if a query couldn't be found.
      */
     public String queryForResultsUrl(String url) {
         final Uri resultsUri = getResultsUri();
         final Set<String> names = StringUtils.getQueryParameterNames(resultsUri);
--- a/services/sync/modules/FxaMigrator.jsm
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -365,54 +365,72 @@ Migrator.prototype = {
    */
 
   // Open a UI for the user to create a Firefox Account.  This should only be
   // called while we are in the STATE_USER_FXA state.  When the user completes
   // the creation we'll see an ONLOGIN_NOTIFICATION notification from FxA and
   // we'll move to either the STATE_USER_FXA_VERIFIED state or we'll just
   // complete the migration if they login as an already verified user.
   createFxAccount: Task.async(function* (win) {
+    let {url, options} = yield this.getFxAccountCreationOptions();
+    win.switchToTabHavingURI(url, true, options);
+    // An FxA observer will fire when the user completes this, which will
+    // cause us to move to the next "user blocked" state and notify via our
+    // observer notification.
+  }),
+
+  // Returns an object with properties "url" and "options", suitable for
+  // opening FxAccounts to create/signin to FxA suitable for the migration
+  // state.  The caller of this is responsible for the actual opening of the
+  // page.
+  // This should only be called while we are in the STATE_USER_FXA state.  When
+  // the user completes the creation we'll see an ONLOGIN_NOTIFICATION
+  // notification from FxA and we'll move to either the STATE_USER_FXA_VERIFIED
+  // state or we'll just complete the migration if they login as an already
+  // verified user.
+  getFxAccountCreationOptions: Task.async(function* (win) {
     // warn if we aren't in the expected state - but go ahead anyway!
     if (this._state != this.STATE_USER_FXA) {
-      this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
+      this.log.warn("getFxAccountCreationOptions called in an unexpected state: ${}", this._state);
     }
     // We need to obtain the sentinel and apply any prefs that might be
     // specified *before* attempting to setup FxA as the prefs might
     // specify custom servers etc.
     let sentinel = yield this._getSyncMigrationSentinel();
     if (sentinel && sentinel.prefs) {
       this._applySentinelPrefs(sentinel.prefs);
     }
     // If we already have a sentinel then we assume the user has previously
     // created the specified account, so just ask to sign-in.
     let action = sentinel ? "signin" : "signup";
     // See if we can find a default account name to use.
     let email = yield this._getDefaultAccountName(sentinel);
     let tail = email ? "&email=" + encodeURIComponent(email) : "";
+    // A special flag so server-side metrics can tell this is part of migration.
+    tail += "&migration=sync11";
     // We want to ask FxA to offer a "Customize Sync" checkbox iff any engines
     // are disabled.
     let customize = !this._allEnginesEnabled();
     tail += "&customizeSync=" + customize;
 
-    win.switchToTabHavingURI("about:accounts?action=" + action + tail, true,
-                             {ignoreFragment: true, replaceQueryString: true});
-    // An FxA observer will fire when the user completes this, which will
-    // cause us to move to the next "user blocked" state and notify via our
-    // observer notification.
+    return {
+      url: "about:accounts?action=" + action + tail,
+      options: {ignoreFragment: true, replaceQueryString: true}
+    };
   }),
 
   // Ask the FxA servers to re-send a verification mail for the currently
   // logged in user. This should only be called while we are in the
   // STATE_USER_FXA_VERIFIED state.  When the user clicks on the link in
   // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
   // to complete the migration.
   resendVerificationMail: Task.async(function * (win) {
     // warn if we aren't in the expected state - but go ahead anyway!
     if (this._state != this.STATE_USER_FXA_VERIFIED) {
-      this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
+      this.log.warn("resendVerificationMail called in an unexpected state: ${}", this._state);
     }
     let ok = true;
     try {
       yield fxAccounts.resendVerificationEmail();
     } catch (ex) {
       this.log.error("Failed to resend verification mail: ${}", ex);
       ok = false;
     }
@@ -437,17 +455,17 @@ Migrator.prototype = {
 
   // "forget" about the current Firefox account. This should only be called
   // while we are in the STATE_USER_FXA_VERIFIED state.  After this we will
   // see an ONLOGOUT_NOTIFICATION, which will cause the migrator to return back
   // to the STATE_USER_FXA state, from where they can choose a different account.
   forgetFxAccount: Task.async(function * () {
     // warn if we aren't in the expected state - but go ahead anyway!
     if (this._state != this.STATE_USER_FXA_VERIFIED) {
-      this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
+      this.log.warn("forgetFxAccount called in an unexpected state: ${}", this._state);
     }
     return fxAccounts.signOut();
   }),
 
 }
 
 // We expose a singleton
 this.EXPORTED_SYMBOLS = ["fxaMigrator"];
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -106,16 +106,40 @@ try {
           .getService(Components.interfaces.nsICrashReporter)) {
       crashReporter.UpdateCrashEventsDir();
       crashReporter.minidumpPath = do_get_minidumpdir();
     }
   }
 }
 catch (e) { }
 
+// Configure a console listener so messages sent to it are logged as part
+// of the test.
+try {
+  let levelNames = {}
+  for (let level of ["debug", "info", "warn", "error"]) {
+    levelNames[Components.interfaces.nsIConsoleMessage[level]] = level;
+  }
+
+  let listener = {
+    QueryInterface : function(iid) {
+      if (!iid.equals(Components.interfaces.nsISupports) &&
+          !iid.equals(Components.interfaces.nsIConsoleListener)) {
+        throw Components.results.NS_NOINTERFACE;
+      }
+      return this;
+    },
+    observe : function (msg) {
+      do_print("CONSOLE_MESSAGE: (" + levelNames[msg.logLevel] + ") " + msg.toString());
+    }
+  };
+  Components.classes["@mozilla.org/consoleservice;1"]
+            .getService(Components.interfaces.nsIConsoleService)
+            .registerListener(listener);
+} catch (e) {}
 /**
  * Date.now() is not necessarily monotonically increasing (insert sob story
  * about times not being the right tool to use for measuring intervals of time,
  * robarnold can tell all), so be wary of error by erring by at least
  * _timerFuzz ms.
  */
 const _timerFuzz = 15;
 
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -1281,17 +1281,19 @@ Search.prototype = {
     // We have to determine the right style to display.  Tags show the tag icon,
     // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
     // the result does not fall into any of those, it just gets the favicon.
     if (!match.style) {
       // It is possible that we already have a style set (from a keyword
       // search or because of the user's preferences), so only set it if we
       // haven't already done so.
       if (showTags) {
-        match.style = "tag";
+        // If we're not suggesting bookmarks, then this shouldn't
+        // display as one.
+        match.style = this.hasBehavior("bookmark") ? "bookmark-tag" : "tag";
       }
       else if (bookmarked) {
         match.style = "bookmark";
       }
     }
 
     if (action)
       match.style = "action " + action;
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -166,17 +166,17 @@ function* check_autocomplete(test) {
           style = style.sort();
         else
           style = ["favicon"];
 
         do_log_info("Checking against expected '" + uri.spec + "', '" + title + "'...");
         // Got a match on both uri and title?
         if (stripPrefix(uri.spec) == stripPrefix(value) && title == comment) {
           do_log_info("Got a match at index " + j + "!");
-          let actualStyle = controller.getStyleAt(i).split(/\W+/).sort();
+          let actualStyle = controller.getStyleAt(i).split(/\s+/).sort();
           if (style)
             Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style");
 
           // Make it undefined so we don't process it again
           matches[j] = undefined;
           if (uri.spec.startsWith("moz-action:")) {
             Assert.ok(actualStyle.indexOf("action") != -1, "moz-action results should always have 'action' in their style");
           }
--- a/toolkit/components/places/tests/unifiedcomplete/test_416211.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_416211.js
@@ -11,12 +11,12 @@ add_task(function* test_tag_match_has_bo
   do_log_info("Make sure the tag match gives the bookmark title");
   let uri = NetUtil.newURI("http://theuri/");
   yield promiseAddVisits({ uri: uri, title: "Page title" });
   addBookmark({ uri: uri,
                 title: "Bookmark title",
                 tags: [ "superTag" ]});
   yield check_autocomplete({
     search: "superTag",
-    matches: [ { uri: uri, title: "Bookmark title", tags: [ "superTag" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri, title: "Bookmark title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ]
   });
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_416214.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_416214.js
@@ -18,20 +18,20 @@ add_task(function* test_tag_match_url() 
   do_log_info("Make sure tag matches return the right url as well as '+' remain escaped");
   let uri1 = NetUtil.newURI("http://escaped/ユニコード");
   let uri2 = NetUtil.newURI("http://asciiescaped/blocking-firefox3%2B");
   yield promiseAddVisits([ { uri: uri1, title: "title" },
   						   { uri: uri2, title: "title" } ]);
   addBookmark({ uri: uri1,
                 title: "title",
                 tags: [ "superTag" ],
-                style: [ "tag" ] });
+                style: [ "bookmark-tag" ] });
   addBookmark({ uri: uri2,
                 title: "title",
                 tags: [ "superTag" ],
-                style: [ "tag" ] });
+                style: [ "bookmark-tag" ] });
   yield check_autocomplete({
     search: "superTag",
-    matches: [ { uri: uri1, title: "title", tags: [ "superTag" ], style: [ "tag" ] },
-     		   { uri: uri2, title: "title", tags: [ "superTag" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri1, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] },
+     		   { uri: uri2, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ]
   });
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_418257.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_418257.js
@@ -28,38 +28,38 @@ add_task(function* test_javascript_match
                 tags: [ "tag1", "tag3" ] });
   addBookmark({ uri: uri4,
                 title: "tagged",
                 tags: [ "tag1", "tag2", "tag3" ] });
 
   do_log_info("Make sure tags come back in the title when matching tags");
   yield check_autocomplete({
     search: "page1 tag",
-    matches: [ { uri: uri1, title: "tagged", tags: [ "tag1" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri1, title: "tagged", tags: [ "tag1" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Check tags in title for page2");
   yield check_autocomplete({
     search: "page2 tag",
-    matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Make sure tags appear even when not matching the tag");
   yield check_autocomplete({
     search: "page3",
-    matches: [ { uri: uri3, title: "tagged", tags: [ "tag1", "tag3" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri3, title: "tagged", tags: [ "tag1", "tag3" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Multiple tags come in commas for page4");
   yield check_autocomplete({
     search: "page4",
-    matches: [ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Extra test just to make sure we match the title");
   yield check_autocomplete({
     search: "tag2",
-    matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "tag" ] },
-               { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] },
+               { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_special_search.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js
@@ -58,20 +58,20 @@ add_task(function* test_special_searches
 
   do_log_info("Star restrict");
   yield check_autocomplete({
     search: "*",
     matches: [ { uri: uri5, title: "title", style: [ "bookmark" ] },
                { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Tag restrict");
   yield check_autocomplete({
     search: "+",
     matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
                { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
                { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
@@ -133,33 +133,33 @@ add_task(function* test_special_searches
 
   do_log_info("foo * -> is star");
   resetRestrict("history");
   yield check_autocomplete({
     search: "foo *",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo | -> is star (change pref)");
   changeRestrict("bookmark", "|");
   yield check_autocomplete({
     search: "foo |",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo # -> in title");
   resetRestrict("bookmark");
   yield check_autocomplete({
     search: "foo #",
     matches: [ { uri: uri2, title: "foo.bar" },
                { uri: uri4, title: "foo.bar" },
@@ -246,17 +246,17 @@ add_task(function* test_special_searches
   });
 
   // Test various pairs of special searches
   do_log_info("foo ^ * -> history, is star");
   resetRestrict("typed");
   yield check_autocomplete({
     search: "foo ^ *",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo ^ # -> history, in title");
   yield check_autocomplete({
     search: "foo ^ #",
     matches: [ { uri: uri2, title: "foo.bar" },
                { uri: uri4, title: "foo.bar" },
                { uri: uri6, title: "foo.bar" },
@@ -284,44 +284,44 @@ add_task(function* test_special_searches
                { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
   });
 
   do_log_info("foo * # -> is star, in title");
   yield check_autocomplete({
     search: "foo * #",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo * @ -> is star, in url");
   yield check_autocomplete({
     search: "foo * @",
     matches: [ { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo * + -> same as +");
   yield check_autocomplete({
     search: "foo * +",
-    matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo * ~ -> is star, is typed");
   yield check_autocomplete({
     search: "foo * ~",
-    matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+    matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo # @ -> in title, in url");
   yield check_autocomplete({
     search: "foo # @",
     matches: [ { uri: uri4, title: "foo.bar" },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
@@ -388,58 +388,58 @@ add_task(function* test_special_searches
   yield check_autocomplete({
     search: "foo",
     matches: [ { uri: uri2, title: "foo.bar" },
                { uri: uri3, title: "title" },
                { uri: uri4, title: "foo.bar" },
                { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo -> default history, is star, is typed");
   setSuggestPrefsToFalse();
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
   Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
   Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
   yield check_autocomplete({
     search: "foo",
     matches: [ { uri: uri4, title: "foo.bar" },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo -> is star");
   setSuggestPrefsToFalse();
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
   Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
   yield check_autocomplete({
     search: "foo",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("foo -> is star, is typed");
   setSuggestPrefsToFalse();
   // only typed should be ignored
   Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
   Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
   yield check_autocomplete({
     search: "foo",
     matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
                { uri: uri7, title: "title", style: [ "bookmark" ] },
                { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
-               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
-               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }  ]
+               { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+               { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }  ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js
@@ -47,64 +47,64 @@ add_task(function* test_escape() {
   // match only on word boundaries
   Services.prefs.setIntPref("browser.urlbar.matchBehavior", 2);
 
   do_log_info("Match 'match' at the beginning or after / or on a CamelCase");
   yield check_autocomplete({
     search: "match",
     matches: [ { uri: uri1, title: "title1" },
                { uri: uri3, title: "matchme2" },
-               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "tag" ] },
+               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
                { uri: uri10, title: "title1" } ]
   });
 
   do_log_info("Match 'dont' at the beginning or after /");
   yield check_autocomplete({
     search: "dont",
     matches: [ { uri: uri2, title: "title1" },
                { uri: uri4, title: "dontmatchme3" },
-               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "tag" ] } ]
+               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Match 'match' at the beginning or after / or on a CamelCase");
   yield check_autocomplete({
     search: "2",
     matches: [ { uri: uri3, title: "matchme2" },
                { uri: uri4, title: "dontmatchme3" },
-               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "tag" ] },
-               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "tag" ] } ]
+               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ]
   });
 
   do_log_info("Match 't' at the beginning or after /");
   yield check_autocomplete({
     search: "t",
     matches: [ { uri: uri1, title: "title1" },
                { uri: uri2, title: "title1" },
                { uri: uri3, title: "matchme2" },
                { uri: uri4, title: "dontmatchme3" },
-               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "tag" ] },
-               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "tag" ] },
+               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
                { uri: uri10, title: "title1" } ]
   });
 
   do_log_info("Match 'word' after many consecutive word boundaries");
   yield check_autocomplete({
     search: "word",
     matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ]
   });
 
   do_log_info("Match a word boundary '/' for everything");
   yield check_autocomplete({
     search: "/",
     matches: [ { uri: uri1, title: "title1" },
                { uri: uri2, title: "title1" },
                { uri: uri3, title: "matchme2" },
                { uri: uri4, title: "dontmatchme3" },
-               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "tag" ] },
-               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "tag" ] },
+               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
                { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" },
                { uri: uri8, title: katakana.join("") },
                { uri: uri9, title: ideograph.join("") },
                { uri: uri10, title: "title1" } ]
   });
 
   do_log_info("Match word boundaries '()_+' that are among word boundaries");
   yield check_autocomplete({
@@ -159,15 +159,15 @@ add_task(function* test_escape() {
   Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1);
 
   yield check_autocomplete({
     search: "tch",
     matches: [ { uri: uri1, title: "title1" },
                { uri: uri2, title: "title1" },
                { uri: uri3, title: "matchme2" },
                { uri: uri4, title: "dontmatchme3" },
-               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "tag" ] },
-               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "tag" ] },
+               { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+               { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
                { uri: uri10, title: "title1" } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/printing/content/printUtils.js
+++ b/toolkit/components/printing/content/printUtils.js
@@ -446,16 +446,25 @@ var PrintUtils = {
 
     if (this._webProgressPP.value) {
       mm.addMessageListener("Printing:Preview:StateChange", this);
       mm.addMessageListener("Printing:Preview:ProgressChange", this);
     }
 
     let onEntered = (message) => {
       mm.removeMessageListener("Printing:PrintPreview:Entered", onEntered);
+
+      if (message.data.failed) {
+        // Something went wrong while putting the document into print preview
+        // mode. Bail out.
+        this._listener.onEnter();
+        this._listener.onExit();
+        return;
+      }
+
       // Stash the focused element so that we can return to it after exiting
       // print preview.
       gFocusedElement = document.commandDispatcher.focusedElement;
 
       let printPreviewTB = document.getElementById("print-preview-toolbar");
       if (printPreviewTB) {
         printPreviewTB.updateToolbar();
         ppBrowser.collapsed = false;
@@ -515,17 +524,17 @@ var PrintUtils = {
     let printPreviewTB = document.getElementById("print-preview-toolbar");
     this._listener.getNavToolbox().parentNode.removeChild(printPreviewTB);
 
     let fm = Components.classes["@mozilla.org/focus-manager;1"]
                        .getService(Components.interfaces.nsIFocusManager);
     if (gFocusedElement)
       fm.setFocus(gFocusedElement, fm.FLAG_NOSCROLL);
     else
-      window.content.focus();
+      this._sourceBrowser.focus();
     gFocusedElement = null;
 
     this._listener.onExit();
   },
 
   onKeyDownPP: function (aEvent)
   {
     // Esc exits the PP
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -20,16 +20,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
   "resource://gre/modules/Deprecated.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SearchStaticData",
   "resource://gre/modules/SearchStaticData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+  "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+  "resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gTextToSubURI",
                                    "@mozilla.org/intl/texttosuburi;1",
                                    "nsITextToSubURI");
 
 Cu.importGlobalProperties(["XMLHttpRequest"]);
 
 // A text encoder to UTF8, used whenever we commit the
@@ -458,71 +462,123 @@ function isUSTimezone() {
 // the hacky method above, so isUS() can avoid the hacky timezone method.
 // If it fails we don't touch that pref so isUS() does its normal thing.
 let ensureKnownCountryCode = Task.async(function* () {
   // If we have a country-code already stored in our prefs we trust it.
   try {
     Services.prefs.getCharPref("browser.search.countryCode");
     return; // pref exists, so we've done this before.
   } catch(e) {}
-  // we don't have it cached, so fetch it.
-  let cc = yield fetchCountryCode();
-  if (cc) {
-    // we got one - stash it away
-    Services.prefs.setCharPref("browser.search.countryCode", cc);
-    // and update our "isUS" cache pref if it is US - that will prevent a
-    // fallback to the timezone check.
-    // However, only do this if the locale also matches.
-    if (getLocale() == "en-US") {
-      Services.prefs.setBoolPref("browser.search.isUS", (cc == "US"));
-    }
-    // and telemetry...
-    let isTimezoneUS = isUSTimezone();
-    if (cc == "US" && !isTimezoneUS) {
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE").add(1);
-    }
-    if (cc != "US" && isTimezoneUS) {
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY").add(1);
-    }
+  // We don't have it cached, so fetch it. fetchCountryCode() will call
+  // storeCountryCode if it gets a result (even if that happens after the
+  // promise resolves)
+  yield fetchCountryCode();
+  // If gInitialized is true then the search service was forced to perform
+  // a sync initialization during our XHR - capture this via telemetry.
+  Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT").add(gInitialized);
+});
+
+// Store the result of the geoip request as well as any other values and
+// telemetry which depend on it.
+function storeCountryCode(cc) {
+  // Set the country-code itself.
+  Services.prefs.setCharPref("browser.search.countryCode", cc);
+  // and update our "isUS" cache pref if it is US - that will prevent a
+  // fallback to the timezone check.
+  // However, only do this if the locale also matches.
+  if (getLocale() == "en-US") {
+    Services.prefs.setBoolPref("browser.search.isUS", (cc == "US"));
   }
-});
+  // and telemetry...
+  let isTimezoneUS = isUSTimezone();
+  if (cc == "US" && !isTimezoneUS) {
+    Services.telemetry.getHistogramById("SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE").add(1);
+  }
+  if (cc != "US" && isTimezoneUS) {
+    Services.telemetry.getHistogramById("SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY").add(1);
+  }
+}
 
 // Get the country we are in via a XHR geoip request.
 function fetchCountryCode() {
+  // values for the SEARCH_SERVICE_COUNTRY_FETCH_RESULT 'enum' telemetry probe.
+  const TELEMETRY_RESULT_ENUM = {
+    SUCCESS: 0,
+    SUCCESS_WITHOUT_DATA: 1,
+    XHRTIMEOUT: 2,
+    ERROR: 3,
+    // Note that we expect to add finer-grained error types here later (eg,
+    // dns error, network error, ssl error, etc) with .ERROR remaining as the
+    // generic catch-all that doesn't fit into other categories.
+  };
   let endpoint = Services.urlFormatter.formatURLPref("browser.search.geoip.url");
   // As an escape hatch, no endpoint means no geoip.
   if (!endpoint) {
-    return Promise.resolve(null);
+    return Promise.resolve();
   }
   let startTime = Date.now();
   return new Promise(resolve => {
+    // Instead of using a timeout on the xhr object itself, we simulate one
+    // using a timer and let the XHR request complete.  This allows us to
+    // capture reliable telemetry on what timeout value should actually be
+    // used to ensure most users don't see one while not making it so large
+    // that many users end up doing a sync init of the search service and thus
+    // would see the jank that implies.
+    // (Note we do actually use a timeout on the XHR, but that's set to be a
+    // large value just incase the request never completes - we don't want the
+    // XHR object to live forever)
+    let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
+    let timerId = setTimeout(() => {
+      LOG("_fetchCountryCode: timeout fetching country information");
+      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
+      timerId = null;
+      resolve();
+    }, timeoutMS);
+
+    let resolveAndReportSuccess = (result, reason) => {
+      // Even if we timed out, we want to save the country code and everything
+      // related so next startup sees the value and doesn't retry this dance.
+      if (result) {
+        storeCountryCode(result);
+      }
+      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT").add(reason);
+
+      // This notification is just for tests...
+      Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-complete");
+
+      // If we've already timed out then we've already resolved the promise,
+      // so there's nothing else to do.
+      if (timerId == null) {
+        return;
+      }
+      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
+      clearTimeout(timerId);
+      resolve();
+    };
+
     let request = new XMLHttpRequest();
-    request.timeout = Services.prefs.getIntPref("browser.search.geoip.timeout");
+    // This notification is just for tests...
+    Services.obs.notifyObservers(request, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-starting");
+    request.timeout = 100000; // 100 seconds as the last-chance fallback
     request.onload = function(event) {
       let took = Date.now() - startTime;
       let cc = event.target.response && event.target.response.country_code;
       LOG("_fetchCountryCode got success response in " + took + "ms: " + cc);
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_MS").add(took);
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS").add(cc ? 1 : 0);
-      if (!cc) {
-        Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS_WITHOUT_DATA").add(1);
-      }
-      resolve(cc);
+      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS").add(took);
+      let reason = cc ? TELEMETRY_RESULT_ENUM.SUCCESS : TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA;
+      resolveAndReportSuccess(cc, reason);
+    };
+    request.ontimeout = function(event) {
+      LOG("_fetchCountryCode: XHR finally timed-out fetching country information");
+      resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
     };
     request.onerror = function(event) {
       LOG("_fetchCountryCode: failed to retrieve country information");
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS").add(0);
-      resolve(null);
+      resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.ERROR);
     };
-    request.ontimeout = function(event) {
-      LOG("_fetchCountryCode: timeout fetching country information");
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIMEOUT").add(1);
-      Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS").add(0);
-      resolve(null);
-    }
     request.open("POST", endpoint, true);
     request.setRequestHeader("Content-Type", "application/json");
     request.responseType = "json";
     request.send("{}");
   });
 }
 
 /**
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -297,8 +297,56 @@ let addTestEngines = Task.async(function
       } else {
         Services.search.addEngineWithDetails(item.name, ...item.details);
       }
     });
   }
 
   return engines;
 });
+
+/**
+ * Returns a promise that is resolved when an observer notification from the
+ * search service fires with the specified data.
+ *
+ * @param aExpectedData
+ *        The value the observer notification sends that causes us to resolve
+ *        the promise.
+ */
+function waitForSearchNotification(aExpectedData) {
+  return new Promise(resolve => {
+    const SEARCH_SERVICE_TOPIC = "browser-search-service";
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+      if (aData != aExpectedData)
+        return;
+
+      Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
+      resolve(aSubject);
+    }, SEARCH_SERVICE_TOPIC, false);
+  });
+}
+
+// This "enum" from nsSearchService.js
+const TELEMETRY_RESULT_ENUM = {
+  SUCCESS: 0,
+  SUCCESS_WITHOUT_DATA: 1,
+  XHRTIMEOUT: 2,
+  ERROR: 3,
+};
+
+/**
+ * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe.
+ *
+ * @param aExpectedValue
+ *        If a value from TELEMETRY_RESULT_ENUM, we expect to see this value
+ *        recorded exactly once in the probe.  If |null|, we expect to see
+ *        nothing recorded in the probe at all.
+ */
+function checkCountryResultTelemetry(aExpectedValue) {
+  let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT");
+  let snapshot = histogram.snapshot();
+  // The probe is declared with 8 values, but we get 9 back from .counts
+  let expectedCounts = [0,0,0,0,0,0,0,0,0];
+  if (aExpectedValue != null) {
+    expectedCounts[aExpectedValue] = 1;
+  }
+  deepEqual(snapshot.counts, expectedCounts);
+}
--- a/toolkit/components/search/tests/xpcshell/test_location.js
+++ b/toolkit/components/search/tests/xpcshell/test_location.js
@@ -20,16 +20,21 @@ function run_test() {
     removeCacheFile();
   });
 
   Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
   Services.search.init(() => {
     equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU", "got the correct country code.");
     equal(Services.prefs.getBoolPref("browser.search.isUS"), false, "AU is not in the US.")
     // check we have "success" recorded in telemetry
-    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS");
-    let snapshot = histogram.snapshot();
-    equal(snapshot.sum, 1)
+    checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+    // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+    for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+                     "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+      let histogram = Services.telemetry.getHistogramById(hid);
+      let snapshot = histogram.snapshot();
+      deepEqual(snapshot.counts, [1,0,0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+    }
     do_test_finished();
     run_next_test();
   });
   do_test_pending();
 }
--- a/toolkit/components/search/tests/xpcshell/test_location_error.js
+++ b/toolkit/components/search/tests/xpcshell/test_location_error.js
@@ -15,31 +15,33 @@ function run_test() {
 
   do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
 
   do_register_cleanup(function() {
     removeMetadata();
     removeCacheFile();
   });
 
-  // from server-locations.txt, we choose a URL without a cert.
+  // this will cause an "unknown host" error, but not report an external
+  // network connection in the tests (note that the hosts listed in
+  // server-locations.txt are *not* loaded for xpcshell tests...)
   let url = "https://nocert.example.com:443";
   Services.prefs.setCharPref("browser.search.geoip.url", url);
   Services.search.init(() => {
     try {
       Services.prefs.getCharPref("browser.search.countryCode");
       ok(false, "not expecting countryCode to be set");
     } catch (ex) {}
-    // should be no success recorded.
-    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS");
-    let snapshot = histogram.snapshot();
-    equal(snapshot.sum, 0);
-
-    // should be no timeout either.
-    histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIMEOUT");
-    snapshot = histogram.snapshot();
-    equal(snapshot.sum, 0);
+    // should have an error recorded.
+    checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.ERROR);
+    // but false values for timeout and forced-sync-init.
+    for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+                     "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+      let histogram = Services.telemetry.getHistogramById(hid);
+      let snapshot = histogram.snapshot();
+      deepEqual(snapshot.counts, [1,0,0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+    }
 
     do_test_finished();
     run_next_test();
   });
   do_test_pending();
 }
--- a/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
+++ b/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
@@ -31,23 +31,23 @@ function run_test() {
       Services.prefs.getCharPref("browser.search.isUS");
       ok(false, "should be no isUS pref yet either");
     } catch (_) {}
     // fetch the engines - this should force the timezone check
     Services.search.getEngines();
     equal(Services.prefs.getBoolPref("browser.search.isUS"),
           isUSTimezone(),
           "should have set isUS based on current timezone.");
-    // should have a false value for success.
-    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS");
-    let snapshot = histogram.snapshot();
-    equal(snapshot.sum, 0);
-
-    // and a flag for SEARCH_SERVICE_COUNTRY_SUCCESS_WITHOUT_DATA
-    histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS_WITHOUT_DATA");
-    snapshot = histogram.snapshot();
-    equal(snapshot.sum, 1);
+    // should have recorded SUCCESS_WITHOUT_DATA
+    checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA);
+    // and false values for timeout and forced-sync-init.
+    for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+                     "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+      let histogram = Services.telemetry.getHistogramById(hid);
+      let snapshot = histogram.snapshot();
+      deepEqual(snapshot.counts, [1,0,0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+    }
 
     do_test_finished();
     run_next_test();
   });
   do_test_pending();
 }
--- a/toolkit/components/search/tests/xpcshell/test_location_sync.js
+++ b/toolkit/components/search/tests/xpcshell/test_location_sync.js
@@ -59,20 +59,37 @@ add_task(function* test_simple() {
   // a little wait to check we didn't do the xhr thang.
   yield new Promise(resolve => {
     do_timeout(500, resolve);
   });
 
   deepEqual(getCountryCodePref(), undefined, "didn't do the geoip xhr");
   // and no telemetry evidence of geoip.
   for (let hid of [
-    "SEARCH_SERVICE_COUNTRY_FETCH_MS",
-    "SEARCH_SERVICE_COUNTRY_SUCCESS",
-    "SEARCH_SERVICE_COUNTRY_SUCCESS_WITHOUT_DATA",
-    "SEARCH_SERVICE_COUNTRY_FETCH_TIMEOUT",
+    "SEARCH_SERVICE_COUNTRY_FETCH_RESULT",
+    "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS",
+    "SEARCH_SERVICE_COUNTRY_TIMEOUT",
     "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
     "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+    "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT",
     ]) {
       let histogram = Services.telemetry.getHistogramById(hid);
       let snapshot = histogram.snapshot();
-      equal(snapshot.sum, 0);
+      equal(snapshot.sum, 0, hid);
+      switch (snapshot.histogram_type) {
+        case Ci.nsITelemetry.HISTOGRAM_FLAG:
+          // flags are a special case in that they are initialized with a default
+          // of one |0|.
+          deepEqual(snapshot.counts, [1,0,0], hid);
+          break;
+        case Ci.nsITelemetry.HISTOGRAM_BOOLEAN:
+          // booleans aren't initialized at all, so should have all zeros.
+          deepEqual(snapshot.counts, [0,0,0], hid);
+          break;
+        case Ci.nsITelemetry.HISTOGRAM_EXPONENTIAL:
+        case Ci.nsITelemetry.HISTOGRAM_LINEAR:
+          equal(snapshot.counts.reduce((a, b) => a+b), 0, hid);
+          break;
+        default:
+          ok(false, "unknown histogram type " + snapshot.histogram_type + " for " + hid);
+      }
     }
 });
--- a/toolkit/components/search/tests/xpcshell/test_location_timeout.js
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout.js
@@ -1,28 +1,37 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function startServer() {
+// This is testing the "normal" timer-based timeout for the location search.
+
+function startServer(continuePromise) {
   let srv = new HttpServer();
   function lookupCountry(metadata, response) {
     response.processAsync();
-    // wait 200 ms before writing a valid response - the search service
-    // should timeout before the response is written so the response should
-    // be ignored.
-    do_timeout(200, () => {
+    // wait for our continuePromise to resolve before writing a valid
+    // response.
+    // This will be resolved after the timeout period, so we can check
+    // the behaviour in that case.
+    continuePromise.then(() => {
       response.setStatusLine("1.1", 200, "OK");
       response.write('{"country_code" : "AU"}');
+      response.finish();
     });
   }
   srv.registerPathHandler("/lookup_country", lookupCountry);
   srv.start(-1);
   return srv;
 }
 
+function getProbeSum(probe, sum) {
+  let histogram = Services.telemetry.getHistogramById(probe);
+  return histogram.snapshot().sum;
+}
+
 function run_test() {
   removeMetadata();
   removeCacheFile();
 
   do_check_false(Services.search.isInitialized);
 
   let engineDummyFile = gProfD.clone();
   engineDummyFile.append("searchplugins");
@@ -32,32 +41,55 @@ function run_test() {
 
   do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
 
   do_register_cleanup(function() {
     removeMetadata();
     removeCacheFile();
   });
 
-  let server = startServer();
+  let resolveContinuePromise;
+  let continuePromise = new Promise(resolve => {
+    resolveContinuePromise = resolve;
+  });
+
+  let server = startServer(continuePromise);
   let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
   Services.prefs.setCharPref("browser.search.geoip.url", url);
   Services.prefs.setIntPref("browser.search.geoip.timeout", 50);
   Services.search.init(() => {
     try {
       Services.prefs.getCharPref("browser.search.countryCode");
       ok(false, "not expecting countryCode to be set");
     } catch (ex) {}
-    // should be no success recorded.
-    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_SUCCESS");
+    // should be no result recorded at all.
+    checkCountryResultTelemetry(null);
+
+    // should have set the flag indicating we saw a timeout.
+    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
     let snapshot = histogram.snapshot();
-    equal(snapshot.sum, 0);
+    deepEqual(snapshot.counts, [0,1,0]);
+    // should not yet have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+    // test server is still blocked on our promise.
+    equal(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS"), 0);
 
-    // should be a timeout.
-    histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIMEOUT");
-    snapshot = histogram.snapshot();
-    equal(snapshot.sum, 1);
+    waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+      // now we *should* have a report of how long the response took even though
+      // it timed out.
+      // The telemetry "sum" will be the actual time in ms - just check it's non-zero.
+      ok(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS") != 0);
+      // should have reported the fetch ended up being successful
+      checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
 
-    do_test_finished();
-    server.stop(run_next_test);
+      // and should have the result of the response that finally came in, and
+      // everything dependent should also be updated.
+      equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU");
+      equal(Services.prefs.getBoolPref("browser.search.isUS"), false);
+
+      do_test_finished();
+      server.stop(run_next_test);
+    });
+    // now tell the server to send its response.  That will end up causing the
+    // search service to notify of that the response was received.
+    resolveContinuePromise();
   });
   do_test_pending();
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the long, last-resort XHR-based timeout for the location
+// search.
+
+function startServer(continuePromise) {
+  let srv = new HttpServer();
+  function lookupCountry(metadata, response) {
+    response.processAsync();
+    // wait for our continuePromise to resolve before writing a valid
+    // response.
+    // This will be resolved after the timeout period, so we can check
+    // the behaviour in that case.
+    continuePromise.then(() => {
+      response.setStatusLine("1.1", 200, "OK");
+      response.write('{"country_code" : "AU"}');
+      response.finish();
+    });
+  }
+  srv.registerPathHandler("/lookup_country", lookupCountry);
+  srv.start(-1);
+  return srv;
+}
+
+function verifyProbeSum(probe, sum) {
+  let histogram = Services.telemetry.getHistogramById(probe);
+  let snapshot = histogram.snapshot();
+  equal(snapshot.sum, sum, probe);
+}
+
+function run_test() {
+  removeMetadata();
+  removeCacheFile();
+
+  do_check_false(Services.search.isInitialized);
+
+  let engineDummyFile = gProfD.clone();
+  engineDummyFile.append("searchplugins");
+  engineDummyFile.append("test-search-engine.xml");
+  let engineDir = engineDummyFile.parent;
+  engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+  do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+  do_register_cleanup(function() {
+    removeMetadata();
+    removeCacheFile();
+  });
+
+  let resolveContinuePromise;
+  let continuePromise = new Promise(resolve => {
+    resolveContinuePromise = resolve;
+  });
+
+  let server = startServer(continuePromise);
+  let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+  Services.prefs.setCharPref("browser.search.geoip.url", url);
+  // The timeout for the timer.
+  Services.prefs.setIntPref("browser.search.geoip.timeout", 10);
+  let promiseXHRStarted = waitForSearchNotification("geoip-lookup-xhr-starting");
+  Services.search.init(() => {
+    try {
+      Services.prefs.getCharPref("browser.search.countryCode");
+      ok(false, "not expecting countryCode to be set");
+    } catch (ex) {}
+    // should be no result recorded at all.
+    checkCountryResultTelemetry(null);
+
+    // should have set the flag indicating we saw a timeout.
+    let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
+    let snapshot = histogram.snapshot();
+    deepEqual(snapshot.counts, [0,1,0]);
+
+    // should not have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+    // test server is still blocked on our promise.
+    verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+
+    promiseXHRStarted.then(xhr => {
+      // Set the timeout on the xhr object to an extremely low value, so it
+      // should timeout immediately.
+      xhr.timeout = 10;
+      // wait for the xhr timeout to fire.
+      waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+        // should have the XHR timeout recorded.
+        checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
+        // still should not have a report of how long the response took as we
+        // only record that on success responses.
+        verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+        // and we don't know the country code.
+        try {
+          Services.prefs.getCharPref("browser.search.countryCode");
+          ok(false, "not expecting countryCode to be set");
+        } catch (ex) {}
+
+        // unblock the server even though nothing is listening.
+        resolveContinuePromise();
+
+        do_test_finished();
+        server.stop(run_next_test);
+      });
+    });
+  });
+  do_test_pending();
+}
--- a/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
+++ b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
@@ -42,30 +42,18 @@ function getDefaultEngineName() {
   // Copy the logic from nsSearchService
   let pref = kDefaultenginenamePref;
   if (getIsUS()) {
     pref += ".US";
   }
   return Services.prefs.getComplexValue(pref, nsIPLS).data;
 }
 
-function waitForNotification(aExpectedData) {
-  let deferred = Promise.defer();
-
-  const SEARCH_SERVICE_TOPIC = "browser-search-service";
-  Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
-    if (aData != aExpectedData)
-      return;
-
-    Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
-    deferred.resolve();
-  }, SEARCH_SERVICE_TOPIC, false);
-
-  return deferred.promise;
-}
+// waitForSearchNotification is in head_search.js
+let waitForNotification = waitForSearchNotification;
 
 function asyncInit() {
   let deferred = Promise.defer();
 
   Services.search.init(function() {
     do_check_true(Services.search.isInitialized);
     deferred.resolve();
   });
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -30,16 +30,17 @@ support-files =
 [test_init_async_multiple.js]
 [test_init_async_multiple_then_sync.js]
 [test_json_cache.js]
 [test_location.js]
 [test_location_error.js]
 [test_location_malformed_json.js]
 [test_location_sync.js]
 [test_location_timeout.js]
+[test_location_timeout_xhr.js]
 [test_nodb.js]
 [test_nodb_pluschanges.js]
 [test_save_sorted_engines.js]
 [test_purpose.js]
 [test_defaultEngine.js]
 [test_notifications.js]
 [test_parseSubmissionURL.js]
 [test_SearchStaticData.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4572,41 +4572,42 @@
   "SEARCH_SERVICE_BUILD_CACHE_MS": {
     "expires_in_version": "40",
     "kind": "exponential",
     "high": "1000",
     "n_buckets": 15,
     "extended_statistics_ok": true,
     "description": "Time (ms) it takes to build the cache of the search service"
   },
-  "SEARCH_SERVICE_COUNTRY_FETCH_MS": {
+  "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS": {
     "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
     "expires_in_version": "never",
-    "kind": "linear",
-    "n_buckets": 20,
-    "high": 2000,
+    "kind": "exponential",
+    "n_buckets": 30,
+    "high": 100000,
     "description": "Time (ms) it takes to fetch the country code"
   },
-  "SEARCH_SERVICE_COUNTRY_FETCH_TIMEOUT": {
+  "SEARCH_SERVICE_COUNTRY_FETCH_RESULT": {
     "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
     "expires_in_version": "never",
-    "kind": "flag",
-    "description": "If we saw a timeout fetching the country-code"
-  },
-  "SEARCH_SERVICE_COUNTRY_SUCCESS": {
+    "kind": "enumerated",
+    "n_values": 8,
+    "description": "Result of XHR request fetching the country-code. 0=SUCCESS, 1=SUCCESS_WITHOUT_DATA, 2=XHRTIMEOUT, 3=ERROR (rest reserved for finer-grained error codes later)"
+  },
+  "SEARCH_SERVICE_COUNTRY_TIMEOUT": {
     "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
     "expires_in_version": "never",
     "kind": "boolean",
-    "description": "If we successfully fetched the country-code."
-  },
-  "SEARCH_SERVICE_COUNTRY_SUCCESS_WITHOUT_DATA": {
+    "description": "True if we stopped waiting for the XHR response before it completed"
+  },
+  "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT": {
     "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
     "expires_in_version": "never",
-    "kind": "flag",
-    "description": "If we got a success response but no country-code"
+    "kind": "boolean",
+    "description": "True if the search service was synchronously initialized while we were waiting for the XHR response"
   },
   "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE": {
     "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Set if the fetched country-code indicates US but the time-zone heuristic doesn't"
   },
   "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY": {
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -411,27 +411,45 @@ let Printing = {
 
   enterPrintPreview(printSettings, contentWindow) {
     // Bug 1088070 - we should instantiate nsIPrintSettings here in the
     // content script instead of passing it down as a CPOW.
     if (Cu.isCrossProcessWrapper(printSettings)) {
       printSettings = null;
     }
 
+    // We'll call this whenever we've finished reflowing the document, or if
+    // we errored out while attempting to print preview (in which case, we'll
+    // notify the parent that we've failed).
+    let notifyEntered = (error) => {
+      removeEventListener("printPreviewUpdate", onPrintPreviewReady);
+      sendAsyncMessage("Printing:Preview:Entered", {
+        failed: !!error,
+      });
+    };
+
+    let onPrintPreviewReady = () => {
+      notifyEntered();
+    };
+
     // We have to wait for the print engine to finish reflowing all of the
     // documents and subdocuments before we can tell the parent to flip to
     // the print preview UI - otherwise, the print preview UI might ask for
     // information (like the number of pages in the document) before we have
     // our PresShells set up.
-    addEventListener("printPreviewUpdate", function onPrintPreviewReady() {
-      removeEventListener("printPreviewUpdate", onPrintPreviewReady);
-      sendAsyncMessage("Printing:Preview:Entered");
-    });
+    addEventListener("printPreviewUpdate", onPrintPreviewReady);
 
-    docShell.printPreview.printPreview(printSettings, contentWindow, this);
+    try {
+      docShell.printPreview.printPreview(printSettings, contentWindow, this);
+    } catch(error) {
+      // This might fail if we, for example, attempt to print a XUL document.
+      // In that case, we inform the parent to bail out of print preview.
+      Components.utils.reportError(error);
+      notifyEntered(error);
+    }
   },
 
   exitPrintPreview() {
     docShell.printPreview.exitPrintPreview();
   },
 
   print(printSettings, contentWindow) {
     // Bug 1088070 - we should instantiate nsIPrintSettings here in the
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1591,16 +1591,17 @@ extends="chrome://global/content/binding
 
           let emphasiseTitle = true;
           let emphasiseUrl = true;
 
           // Hide the title's extra box by default, until we find out later if
           // we need extra stuff.
           this._extraBox.hidden = true;
           this._titleBox.flex = 1;
+          this._typeImage.hidden = false;
 
           this.removeAttribute("actiontype");
           this.classList.remove("overridable-action");
 
           // The ellipses are hidden via their visibility so that they always
           // take up space and don't pop in on top of text when shown.  For
           // keyword searches, however, the title ellipsis should not take up
           // space when hidden.  Setting the hidden property accomplishes that.
@@ -1676,17 +1677,17 @@ extends="chrome://global/content/binding
                                                 ]);
 
             types.delete("autofill");
           }
 
           type = [...types].join(" ");
 
           // If we have a tag match, show the tags and icon
-          if (type == "tag") {
+          if (type == "tag" || type == "bookmark-tag") {
             // Configure the extra box for tags display
             this._extraBox.hidden = false;
             this._extraBox.childNodes[0].hidden = false;
             this._extraBox.childNodes[1].hidden = true;
             this._extraBox.pack = "end";
             this._titleBox.flex = 1;
 
             // The title is separated from the tags by an endash
@@ -1694,18 +1695,23 @@ extends="chrome://global/content/binding
             [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
 
             // Each tag is split by a comma in an undefined order, so sort it
             let sortedTags = tags.split(",").sort().join(", ");
 
             // Emphasize the matching text in the tags
             this._setUpDescription(this._extra, sortedTags);
 
-            // Treat tagged matches as bookmarks for the star
-            type = "bookmark";
+            // If we're suggesting bookmarks, then treat tagged matches as
+            // bookmarks for the star.
+            if (type == "bookmark-tag") {
+              type = "bookmark";
+            } else {
+              this._typeImage.hidden = true;
+            }
           // keyword and favicon type results for search engines
           // have an extra magnifying glass icon after them
           } else if (type == "keyword" || (initialTypes.has("search") &&
               initialTypes.has("favicon"))) {
             // Configure the extra box for keyword display
             this._extraBox.hidden = false;
             this._extraBox.childNodes[0].hidden = true;
             // The second child node is ":" and it should be hidden for non keyword types
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -14,16 +14,21 @@
                     implements="nsIObserver, nsIDOMEventListener, nsIMessageListener, nsIRemoteBrowser">
 
       <field name="_securityUI">null</field>
 
       <property name="securityUI"
                 readonly="true">
         <getter><![CDATA[
           if (!this._securityUI) {
+            // Don't attempt to create the remote web progress if the
+            // messageManager has already gone away
+            if (!this.messageManager)
+              return null;
+
             let jsm = "resource://gre/modules/RemoteSecurityUI.jsm";
             let RemoteSecurityUI = Components.utils.import(jsm, {}).RemoteSecurityUI;
             this._securityUI = new RemoteSecurityUI();
           }
 
           // We want to double-wrap the JS implemented interface, so that QI and instanceof works.
           var ptr = Cc["@mozilla.org/supports-interface-pointer;1"].
                         createInstance(Ci.nsISupportsInterfacePointer);
@@ -35,45 +40,57 @@
       <method name="adjustPriority">
         <parameter name="adjustment"/>
         <body><![CDATA[
           this.messageManager.sendAsyncMessage("NetworkPrioritizer:AdjustPriority",
                                                {adjustment: adjustment});
         ]]></body>
       </method>
 
+      <field name="_controller">null</field>
+
       <field name="_remoteWebNavigation">null</field>
 
       <property name="webNavigation"
                 onget="return this._remoteWebNavigation;"
                 readonly="true"/>
 
       <field name="_remoteWebProgress">null</field>
 
       <property name="webProgress" readonly="true">
       	<getter>
       	  <![CDATA[
             if (!this._remoteWebProgress) {
+              // Don't attempt to create the remote web progress if the
+              // messageManager has already gone away
+              if (!this.messageManager)
+                return null;
+
               let jsm = "resource://gre/modules/RemoteWebProgress.jsm";
-              let RemoteWebProgressManager = Cu.import(jsm, {}).RemoteWebProgressManager;
+              let { RemoteWebProgressManager } = Cu.import(jsm, {});
               this._remoteWebProgressManager = new RemoteWebProgressManager(this);
               this._remoteWebProgress = this._remoteWebProgressManager.topLevelWebProgress;
             }
             return this._remoteWebProgress;
       	  ]]>
       	</getter>
       </property>
 
       <field name="_remoteFinder">null</field>
 
       <property name="finder" readonly="true">
         <getter><![CDATA[
           if (!this._remoteFinder) {
+            // Don't attempt to create the remote web progress if the
+            // messageManager has already gone away
+            if (!this.messageManager)
+              return null;
+
             let jsm = "resource://gre/modules/RemoteFinder.jsm";
-            let RemoteFinder = Cu.import(jsm, {}).RemoteFinder;
+            let { RemoteFinder } = Cu.import(jsm, {});
             this._remoteFinder = new RemoteFinder(this);
           }
           return this._remoteFinder;
         ]]></getter>
       </property>
 
       <field name="_documentURI">null</field>
 
@@ -201,16 +218,18 @@
           <![CDATA[
             let {frameLoader} = this.QueryInterface(Ci.nsIFrameLoaderOwner);
             frameLoader.tabParent.setIsDocShellActive(val);
             return val;
           ]]>
         </setter>
       </property>
 
+      <field name="mDestroyed">false</field>
+
       <constructor>
         <![CDATA[
           /*
            * Don't try to send messages from this function. The message manager for
            * the <browser> element may not be initialized yet.
            */
 
           let jsm = "resource://gre/modules/RemoteWebNavigation.jsm";
@@ -241,20 +260,34 @@
           this.controllers.appendController(this._controller);
 
           Services.obs.addObserver(this, "ask-children-to-exit-fullscreen", false);
         ]]>
       </constructor>
 
       <destructor>
         <![CDATA[
-          Services.obs.removeObserver(this, "ask-children-to-exit-fullscreen");
+          this.destroy();
         ]]>
       </destructor>
 
+      <!-- This is necessary because the destructor doesn't always get called when
+           we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
+      <method name="destroy">
+        <body><![CDATA[
+          if (this.mDestroyed)
+            return;
+          this.mDestroyed = true;
+
+          this.controllers.removeController(this._controller);
+
+          Services.obs.removeObserver(this, "ask-children-to-exit-fullscreen");
+        ]]></body>
+      </method>
+
       <method name="receiveMessage">
         <parameter name="aMessage"/>
         <body><![CDATA[
           let data = aMessage.data;
           switch (aMessage.name) {
             case "Browser:Init":
               let result = {};
               result.useGlobalHistory = !this.hasAttribute("disableglobalhistory");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser.ini
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser.ini
@@ -19,16 +19,17 @@ support-files =
   redirect.sjs
   restartless.xpi
   signed-no-cn.xpi
   signed-no-o.xpi
   signed-tampered.xpi
   signed-untrusted.xpi
   signed.xpi
   signed2.xpi
+  slowinstall.sjs
   startsoftwareupdate.html
   theme.xpi
   triggerredirect.html
   unsigned.xpi
 
 [browser_auth.js]
 [browser_auth2.js]
 [browser_auth3.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
@@ -0,0 +1,101 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const RELATIVE_PATH = "browser/toolkit/mozapps/extensions/test/xpinstall"
+const NOTIFICATION_TOPIC = "slowinstall-complete";
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param  aQueryString
+ *         The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ *         queryString.
+ */
+function parseQueryString(aQueryString) {
+  var paramArray = aQueryString.split("&");
+  var regex = /^([^=]+)=(.*)$/;
+  var params = {};
+  for (var i = 0, sz = paramArray.length; i < sz; i++) {
+    var match = regex.exec(paramArray[i]);
+    if (!match)
+      throw "Bad parameter in queryString!  '" + paramArray[i] + "'";
+    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+  }
+
+  return params;
+}
+
+function handleRequest(aRequest, aResponse) {
+  let id = +getState("ID");
+  setState("ID", "" + (id + 1));
+
+  function LOG(str) {
+    dump("slowinstall.sjs[" + id + "]: " + str + "\n");
+  }
+
+  aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+
+  var params = { };
+  if (aRequest.queryString)
+    params = parseQueryString(aRequest.queryString);
+
+  if (params.file) {
+    let xpiFile = "";
+
+    function complete_download() {
+      LOG("Completing download");
+      downloadPaused = false;
+
+      try {
+        // Doesn't seem to be a sane way to read using OS.File and write to an
+        // nsIOutputStream so here we are.
+        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        file.initWithPath(xpiFile);
+        let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+                     createInstance(Ci.nsIFileInputStream);
+        stream.init(file, -1, -1, stream.DEFER_OPEN + stream.CLOSE_ON_EOF);
+
+        NetUtil.asyncCopy(stream, aResponse.bodyOutputStream, () => {
+          LOG("Download complete");
+          aResponse.finish();
+        });
+      }
+      catch (e) {
+        LOG("Exception " + e);
+      }
+    }
+
+    let waitForComplete = new Promise(resolve => {
+      function complete() {
+        Services.obs.removeObserver(complete, NOTIFICATION_TOPIC);
+        resolve();
+      }
+
+      Services.obs.addObserver(complete, NOTIFICATION_TOPIC, false);
+    });
+
+    aResponse.processAsync();
+
+    OS.File.getCurrentDirectory().then(dir => {
+      xpiFile = OS.Path.join(dir, ...RELATIVE_PATH.split("/"), params.file);
+      LOG("Starting slow download of " + xpiFile);
+
+      OS.File.stat(xpiFile).then(info => {
+        aResponse.setHeader("Content-Type", "binary/octet-stream");
+        aResponse.setHeader("Content-Length", info.size.toString());
+
+        LOG("Download paused");
+        waitForComplete.then(complete_download);
+      });
+    });
+  }
+  else if (params.continue) {
+    dump("slowinstall.sjs: Received signal to complete all current downloads.\n");
+    Services.obs.notifyObservers(null, NOTIFICATION_TOPIC, null);
+  }
+}