Merge fx-team to m-c with bustage fix, a=merge
authorNigel Babu <nigelbabu@gmail.com>
Tue, 16 Sep 2014 08:43:45 +0530
changeset 228657 0e581e529fd699814ac2532c8cfee57862ea20ab
parent 228642 4e8c6c5c0961683e3d53ab16ce518a335243f116 (current diff)
parent 228656 6c5e6061b704333663bbcff2ff7a7a8a9d0958ca (diff)
child 228658 3b7921328fc126999fa908e6379cbd555d028c50
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c with bustage fix, a=merge
browser/base/content/browser-plugins.js
browser/base/content/nsContextMenu.js
browser/modules/PluginContent.jsm
toolkit/devtools/server/actors/script.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -253,16 +253,22 @@ XPCOMUtils.defineLazyGetter(this, "PageM
 });
 
 /**
 * We can avoid adding multiple load event listeners and save some time by adding
 * one listener that calls all real handlers.
 */
 function pageShowEventHandlers(persisted) {
   XULBrowserWindow.asyncUpdateUI();
+
+  // The PluginClickToPlay events are not fired when navigating using the
+  // BF cache. |persisted| is true when the page is loaded from the
+  // BF cache, so this code reshows the notification if necessary.
+  if (persisted)
+    gPluginHandler.reshowClickToPlayNotification();
 }
 
 function UpdateBackForwardCommands(aWebNavigation) {
   var backBroadcaster = document.getElementById("Browser:Back");
   var forwardBroadcaster = document.getElementById("Browser:Forward");
 
   // Avoid setting attributes on broadcasters if the value hasn't changed!
   // Remember, guys, setting attributes on elements is expensive!  They
@@ -769,16 +775,23 @@ function gKeywordURIFixup({ target: brow
 var gBrowserInit = {
   delayedStartupFinished: false,
 
   onLoad: function() {
     var mustLoadSidebar = false;
 
     gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
 
+    // Note that the XBL binding is untrusted
+    gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true);
+    gBrowser.addEventListener("PluginCrashed",         gPluginHandler, true);
+    gBrowser.addEventListener("PluginOutdated",        gPluginHandler, true);
+    gBrowser.addEventListener("PluginInstantiated",    gPluginHandler, true);
+    gBrowser.addEventListener("PluginRemoved",         gPluginHandler, true);
+
     gBrowser.addEventListener("NewPluginInstalled", gPluginHandler.newPluginInstalled, true);
 
     Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
 
     window.addEventListener("AppCommand", HandleAppCommandEvent, true);
 
     // These routines add message listeners. They must run before
     // loading the frame script to ensure that we don't miss any
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -11,18 +11,16 @@ Cu.import("resource://gre/modules/Servic
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
   "resource:///modules/ContentLinkHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
-  "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
   "resource:///modules/UITour.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
 
 // TabChildGlobal
@@ -490,19 +488,16 @@ let ClickEventHandler = {
     // Note: makeURI() will throw if aUri is not a valid URI.
     return [href ? BrowserUtils.makeURI(href, null, baseURI).spec : null, null];
   }
 };
 ClickEventHandler.init();
 
 ContentLinkHandler.init(this);
 
-// TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
-let pluginContent = new PluginContent(global);
-
 addEventListener("DOMWebNotificationClicked", function(event) {
   sendAsyncMessage("DOMWebNotificationClicked", {});
 }, false);
 
 let PageStyleHandler = {
   init: function() {
     addMessageListener("PageStyle:Switch", this);
     addMessageListener("PageStyle:Disable", this);
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1310,21 +1310,21 @@ nsContextMenu.prototype = {
         this.sendMedia();
   },
 
   sendMedia: function() {
     MailIntegration.sendMessage(this.mediaURL, "");
   },
 
   playPlugin: function() {
-    gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
+    gPluginHandler._showClickToPlayNotification(this.browser, this.target, true);
   },
 
   hidePlugin: function() {
-    gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
+    gPluginHandler.hideClickToPlayOverlay(this.target);
   },
 
   // Generate email address and put it on clipboard.
   copyEmail: function() {
     // Copy the comma-separated list of email addresses only.
     // There are other ways of embedding email addresses in a mailto:
     // link, but such complex parsing is beyond us.
     var url = this.linkURL;
--- a/browser/base/content/test/general/browser_datareporting_notification.js
+++ b/browser/base/content/test/general/browser_datareporting_notification.js
@@ -127,33 +127,33 @@ function test_multiple_windows() {
   let window2 = OpenBrowserWindow();
   whenDelayedStartupFinished(window2, function onWindow() {
     let notification1 = document.getElementById("global-notificationbox");
     let notification2 = window2.document.getElementById("global-notificationbox");
     ok(notification2, "2nd window has a global notification box.");
 
     let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
     let displayCount = 0;
-    let prefWindowClosed = false;
+    let prefWindowOpened = false;
     let mutationObserversRemoved = false;
 
     function onAlertDisplayed() {
       displayCount++;
 
       if (displayCount != 2) {
         return;
       }
 
       ok(true, "Data reporting info bar displayed on all open windows.");
 
       // We register two independent observers and we need both to clean up
       // properly. This handles gating for test completion.
       function maybeFinish() {
-        if (!prefWindowClosed) {
-          dump("Not finishing test yet because pref pane isn't closed.\n");
+        if (!prefWindowOpened) {
+          dump("Not finishing test yet because pref pane hasn't yet appeared.\n");
           return;
         }
 
         if (!mutationObserversRemoved) {
           dump("Not finishing test yet because mutation observers haven't been removed yet.\n");
           return;
         }
 
@@ -188,26 +188,24 @@ function test_multiple_windows() {
       waitForNotificationClose(notification2.currentNotification, onAlertClose);
 
       // While we're here, we dual purpose this test to check that pressing the
       // button does the right thing.
       let buttons = notification2.currentNotification.getElementsByTagName("button");
       is(buttons.length, 1, "There is 1 button in the data reporting notification.");
       let button = buttons[0];
 
-      // Automatically close preferences window when it is opened as part of
-      // button press.
+      // Add an observer to ensure the "advanced" pane opened (but don't bother
+      // closing it - we close the entire window when done.)
       Services.obs.addObserver(function observer(prefWin, topic, data) {
         Services.obs.removeObserver(observer, "advanced-pane-loaded");
 
         ok(true, "Advanced preferences opened on info bar button press.");
         executeSoon(function soon() {
-          dump("Closing preferences.\n");
-          prefWin.close();
-          prefWindowClosed = true;
+          prefWindowOpened = true;
           maybeFinish();
         });
       }, "advanced-pane-loaded", false);
 
       button.click();
     }
 
     notification1.addEventListener("AlertActive", function active1() {
--- a/browser/base/content/test/plugins/browser_CTP_crashreporting.js
+++ b/browser/base/content/test/plugins/browser_CTP_crashreporting.js
@@ -3,82 +3,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
 const gTestRoot = getRootDirectory(gTestPath);
 var gTestBrowser = null;
 
-/**
- * Frame script that will be injected into the test browser
- * to cause the crash, and then manipulate the crashed plugin
- * UI. Specifically, after the crash, we ensure that the
- * crashed plugin UI is using the right style rules and that
- * the submit URL opt-in defaults to checked. Then, we fill in
- * a comment with the crash report, uncheck the submit URL
- * opt-in, and send the crash reports.
- */
-function frameScript() {
-  function fail(reason) {
-    sendAsyncMessage("test:crash-plugin:fail", {
-      reason: `Failure from frameScript: ${reason}`,
-    });
-  }
-
-  addMessageListener("test:crash-plugin", () => {
-    let doc = content.document;
-
-    addEventListener("PluginCrashed", (event) => {
-      let plugin = doc.getElementById("test");
-      if (!plugin) {
-        fail("Could not find plugin element");
-        return;
-      }
-
-      let getUI = (anonid) => {
-        return doc.getAnonymousElementByAttribute(plugin, "anonid", anonid);
-      };
-
-      let style = content.getComputedStyle(getUI("pleaseSubmit"));
-      if (style.display != "block") {
-        fail("Submission UI visibility is not correct. Expected block, "
-             + " got " + style.display);
-        return;
-      }
-
-      getUI("submitComment").value = "a test comment";
-      if (!getUI("submitURLOptIn").checked) {
-        fail("URL opt-in should default to true.");
-        return;
-      }
-
-      getUI("submitURLOptIn").click();
-      getUI("submitButton").click();
-    });
-
-    let plugin = doc.getElementById("test");
-    try {
-      plugin.crash()
-    } catch(e) {
-    }
-  });
-
-  addMessageListener("test:plugin-submit-status", () => {
-    let doc = content.document;
-    let plugin = doc.getElementById("test");
-    let submitStatusEl =
-      doc.getAnonymousElementByAttribute(plugin, "anonid", "submitStatus");
-    let submitStatus = submitStatusEl.getAttribute("status");
-    sendAsyncMessage("test:plugin-submit-status:result", {
-      submitStatus: submitStatus,
-    });
-  });
-}
-
 // Test that plugin crash submissions still work properly after
 // click-to-play activation.
 
 function test() {
   // Crashing the plugin takes up a lot of time, so extend the test timeout.
   requestLongerTimeout(2);
   waitForExplicitFinish();
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
@@ -92,28 +26,24 @@ function test() {
             getService(Components.interfaces.nsIEnvironment);
   let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
   let serverURL = env.get("MOZ_CRASHREPORTER_URL");
   env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
   env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
 
   let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
   gTestBrowser = gBrowser.getBrowserForTab(tab);
-  let mm = gTestBrowser.messageManager;
-  mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false);
-  mm.addMessageListener("test:crash-plugin:fail", (message) => {
-    ok(false, message.data.reason);
-  });
-
+  gTestBrowser.addEventListener("PluginCrashed", onCrash, false);
   gTestBrowser.addEventListener("load", onPageLoad, true);
   Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
 
   registerCleanupFunction(function cleanUp() {
     env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
     env.set("MOZ_CRASHREPORTER_URL", serverURL);
+    gTestBrowser.removeEventListener("PluginCrashed", onCrash, false);
     gTestBrowser.removeEventListener("load", onPageLoad, true);
     Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
     gBrowser.removeCurrentTab();
   });
 
   gTestBrowser.contentWindow.location = gTestRoot + "plugin_big.html";
 }
 function onPageLoad() {
@@ -135,18 +65,41 @@ function afterBindingAttached() {
   popupNotification.reshow();
   PopupNotifications.panel.firstChild._primaryButton.click();
 
   let condition = function() objLoadingContent.activated;
   waitForCondition(condition, pluginActivated, "Waited too long for plugin to activate");
 }
 
 function pluginActivated() {
-  let mm = gTestBrowser.messageManager;
-  mm.sendAsyncMessage("test:crash-plugin");
+  let plugin = gTestBrowser.contentDocument.getElementById("test");
+  try {
+    plugin.crash();
+  } catch (e) {
+    // The plugin crashed in the above call, an exception is expected.
+  }
+}
+
+function onCrash() {
+  try {
+    let plugin = gBrowser.contentDocument.getElementById("test");
+    let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
+    let style =
+      gBrowser.contentWindow.getComputedStyle(elt("pleaseSubmit"));
+    is(style.display, "block", "Submission UI visibility should be correct");
+
+    elt("submitComment").value = "a test comment";
+    is(elt("submitURLOptIn").checked, true, "URL opt-in should default to true");
+    EventUtils.synthesizeMouseAtCenter(elt("submitURLOptIn"), {}, gTestBrowser.contentWindow);
+    EventUtils.synthesizeMouseAtCenter(elt("submitButton"), {}, gTestBrowser.contentWindow);
+    // And now wait for the submission status notification.
+  }
+  catch (err) {
+    failWithException(err);
+  }
 }
 
 function onSubmitStatus(subj, topic, data) {
   try {
     // Wait for success or failed, doesn't matter which.
     if (data != "success" && data != "failed")
       return;
 
@@ -170,33 +123,29 @@ function onSubmitStatus(subj, topic, dat
     let val = getPropertyBagValue(extra, "PluginUserComment");
     is(val, "a test comment",
        "Comment in extra data should match comment in textbox");
 
     val = getPropertyBagValue(extra, "PluginContentURL");
     ok(val === undefined,
        "URL should be absent from extra data when opt-in not checked");
 
-    let submitStatus = null;
-    let mm = gTestBrowser.messageManager;
-    mm.addMessageListener("test:plugin-submit-status:result", (message) => {
-      submitStatus = message.data.submitStatus;
+    // Execute this later in case the event to change submitStatus has not
+    // have been dispatched yet.
+    executeSoon(function () {
+      let plugin = gBrowser.contentDocument.getElementById("test");
+      let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
+      is(elt("submitStatus").getAttribute("status"), data,
+         "submitStatus data should match");
     });
-
-    mm.sendAsyncMessage("test:plugin-submit-status");
-
-    let condition = () => submitStatus;
-    waitForCondition(condition, () => {
-      is(submitStatus, data, "submitStatus data should match");
-      finish();
-    }, "Waiting for submitStatus to be reported from frame script");
   }
   catch (err) {
     failWithException(err);
   }
+  finish();
 }
 
 function getPropertyBagValue(bag, key) {
   try {
     var val = bag.getProperty(key);
   }
   catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
   return val;
--- a/browser/base/content/test/plugins/browser_CTP_drag_drop.js
+++ b/browser/base/content/test/plugins/browser_CTP_drag_drop.js
@@ -24,21 +24,21 @@ function test() {
 }
 
 function handleEvent() {
   gNextTest();
 }
 
 function part1() {
   gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent);
-  waitForNotificationPopup("click-to-play-plugins", gBrowser.selectedBrowser, () => {
-    gNextTest = part2;
-    gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-    gNewWindow.addEventListener("load", handleEvent, true);
-  });
+  ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab");
+
+  gNextTest = part2;
+  gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+  gNewWindow.addEventListener("load", handleEvent, true);
 }
 
 function part2() {
   gNewWindow.removeEventListener("load", handleEvent);
   let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser);
   waitForCondition(condition, part3, "Waited too long for click-to-play notification");
 }
 
@@ -57,20 +57,20 @@ function part4() {
 
   gBrowser.selectedBrowser.addEventListener("PluginBindingAttached", handleEvent, true, true);
   gNextTest = part5;
   gBrowser.selectedBrowser.contentDocument.location = gHttpTestRoot + "plugin_test.html";
 }
 
 function part5() {
   gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent);
-  waitForNotificationPopup("click-to-play-plugins", gBrowser.selectedBrowser, () => {
-    gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
-    waitForFocus(part6, gNewWindow);
-  });
+  ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab");
+
+  gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+  waitForFocus(part6, gNewWindow);
 }
 
 function part6() {
   let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser);
   waitForCondition(condition, part7, "Waited too long for click-to-play notification");
 }
 
 function part7() {
@@ -87,16 +87,14 @@ function part7() {
 }
 
 function part8() {
   // Click the activate button on doorhanger to make sure it works
   gNewWindow.PopupNotifications.panel.firstChild._primaryButton.click();
 
   let plugin = gNewWindow.gBrowser.selectedBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-  waitForCondition(() => objLoadingContent.activated, shutdown, "plugin should be activated now");
-}
+  ok(objLoadingContent.activated, "plugin should be activated now");
 
-function shutdown() {
   gNewWindow.close();
   gNewWindow = null;
   finish();
 }
--- a/browser/base/content/test/plugins/browser_CTP_iframe.js
+++ b/browser/base/content/test/plugins/browser_CTP_iframe.js
@@ -69,25 +69,23 @@ function runAfterPluginBindingAttached(f
 
 // Tests that the overlays are visible and actionable if the plugin is in an iframe.
 function test1() {
   let frame = gTestBrowser.contentDocument.getElementById("frame");
   let doc = frame.contentDocument;
   let plugin = doc.getElementById("test");
   ok(plugin, "Test 1, Found plugin in page");
 
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
-    let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
-    ok(overlay.classList.contains("visible"), "Test 1, Plugin overlay should exist, not be hidden");
-    let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon");
+  let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+  ok(overlay.classList.contains("visible"), "Test 1, Plugin overlay should exist, not be hidden");
+  let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon")
 
-    EventUtils.synthesizeMouseAtCenter(closeIcon, {}, frame.contentWindow);
-    let condition = () => !overlay.classList.contains("visible");
-    waitForCondition(condition, test2, "Test 1, Waited too long for the overlay to become invisible.");
-  });
+  EventUtils.synthesizeMouseAtCenter(closeIcon, {}, frame.contentWindow);
+  let condition = () => !overlay.classList.contains("visible");
+  waitForCondition(condition, test2, "Test 1, Waited too long for the overlay to become invisible.");
 }
 
 function test2() {
   prepareTest(delayTest(runAfterPluginBindingAttached(test3)), gHttpTestRoot + "plugin_iframe.html");
 }
 
 function test3() {
   let frame = gTestBrowser.contentDocument.getElementById("frame");
--- a/browser/base/content/test/plugins/browser_CTP_notificationBar.js
+++ b/browser/base/content/test/plugins/browser_CTP_notificationBar.js
@@ -58,47 +58,53 @@ function runAfterPluginBindingAttached(f
     elems[0].clientTop;
     executeSoon(func);
   };
 }
 
 // Tests for the notification bar for hidden plugins.
 
 function test1() {
-  info("Test 1 - expecting a notification bar for hidden plugins.");
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
-    waitForNotificationBar("plugin-hidden", gTestBrowser, () => {
+  let notification = PopupNotifications.getNotification("click-to-play-plugins");
+  ok(notification, "Test 1: There should be a plugin notification");
+
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
+    () => {
       // Don't use setTestPluginEnabledState here because we already saved the
       // prior value
       getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
       prepareTest(test2, gTestRoot + "plugin_small.html");
-    });
-  });
+    },
+    "Test 1, expected to have a plugin notification bar");
 }
 
 function test2() {
-  info("Test 2 - expecting no plugin notification bar on visible plugins.");
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
-    let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  let notification = PopupNotifications.getNotification("click-to-play-plugins");
+  ok(notification, "Test 2: There should be a plugin notification");
+
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
 
-    waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
-      () => {
-        getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
-        prepareTest(test3, gTestRoot + "plugin_overlayed.html");
-      },
-      "expected to not have a plugin notification bar"
-    );
-  });
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
+    () => {
+      getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+      prepareTest(test3, gTestRoot + "plugin_overlayed.html");
+    },
+    "Test 2, expected to not have a plugin notification bar");
 }
 
 function test3() {
-  info("Test 3 - expecting a plugin notification bar when plugins are overlaid");
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
-    waitForNotificationBar("plugin-hidden", gTestBrowser, test3b);
-  });
+  let notification = PopupNotifications.getNotification("click-to-play-plugins");
+  ok(notification, "Test 3: There should be a plugin notification");
+
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
+    test3b,
+    "Test 3, expected the plugin infobar to be triggered when plugin was overlayed");
 }
 
 function test3b()
 {
   let doc = gTestBrowser.contentDocument;
   let plugin = doc.getElementById("test");
   ok(plugin, "Test 3b, Found plugin in page");
   plugin.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -107,64 +113,66 @@ function test3b()
   ok(!plugin.activated, "Test 3b, Plugin should not be activated");
   let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
   ok(!overlay.classList.contains("visible"), "Test 3b, Plugin overlay should be hidden");
 
   prepareTest(test4, gTestRoot + "plugin_positioned.html");
 }
 
 function test4() {
-  info("Test 4 - expecting a plugin notification bar when plugins are overlaid offscreen")
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
-    waitForNotificationBar("plugin-hidden", gTestBrowser, test4b);
-  });
+  let notification = PopupNotifications.getNotification("click-to-play-plugins");
+  ok(notification, "Test 4: There should be a plugin notification");
+
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
+    test4b,
+    "Test 4, expected the plugin infobar to be triggered when plugin was overlayed");
 }
 
 function test4b() {
   let doc = gTestBrowser.contentDocument;
   let plugin = doc.getElementById("test");
   ok(plugin, "Test 4b, Found plugin in page");
   plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   is(plugin.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
      "Test 4b, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
   ok(!plugin.activated, "Test 4b, Plugin should not be activated");
   let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
   ok(!overlay.classList.contains("visible"), "Test 4b, Plugin overlay should be hidden");
 
   prepareTest(runAfterPluginBindingAttached(test5), gHttpTestRoot + "plugin_small.html");
 }
 
+// Test that the notification bar is getting dismissed when directly activating plugins
+// via the doorhanger.
+
 function test5() {
   let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
   waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
     test6,
     "Test 5, expected a notification bar for hidden plugins");
 }
 
-// Test that the notification bar is getting dismissed when directly activating plugins
-// via the doorhanger.
+function test6() {
+  let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+  ok(notification, "Test 6, Should have a click-to-play notification");
+  let plugin = gTestBrowser.contentDocument.getElementById("test");
+  ok(plugin, "Test 6, Found plugin in page");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+     "Test 6, Plugin should be click-to-play");
 
-function test6() {
-  info("Test 6 - expecting the doorhanger to be dismissed when directly activating plugins.");
-  waitForNotificationPopup("click-to-play-plugins", gTestBrowser, (notification) => {
-    let plugin = gTestBrowser.contentDocument.getElementById("test");
-    ok(plugin, "Test 6, Found plugin in page");
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
-       "Test 6, Plugin should be click-to-play");
+  // simulate "always allow"
+  notification.reshow();
+  PopupNotifications.panel.firstChild._primaryButton.click();
 
-    // simulate "always allow"
-    notification.reshow();
-    PopupNotifications.panel.firstChild._primaryButton.click();
-
-    let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
-    waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
-      test7,
-      "Test 6, expected the notification bar for hidden plugins to get dismissed");
-  });
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
+    test7,
+    "Test 6, expected the notification bar for hidden plugins to get dismissed");
 }
 
 function test7() {
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "Test 7, Found plugin in page");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   waitForCondition(() => objLoadingContent.activated, finishTest,
     "Test 7, Waited too long for plugin to activate");
--- a/browser/base/content/test/plugins/browser_bug743421.js
+++ b/browser/base/content/test/plugins/browser_bug743421.js
@@ -55,18 +55,20 @@ function test1b() {
   ok(popupNotification, "Test 1b, Should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0];
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated");
 
   // Click the activate button on doorhanger to make sure it works
   popupNotification.reshow();
   PopupNotifications.panel.firstChild._primaryButton.click();
-  var condition = function() objLoadingContent.activated;
-  waitForCondition(condition, test1c, "Test 1b, Waited too long for plugin activation");
+
+  ok(objLoadingContent.activated, "Test 1b, Doorhanger should activate plugin");
+
+  test1c();
 }
 
 function test1c() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 1c, Should still have a click-to-play notification");
   var plugin = new XPCNativeWrapper(XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addPlugin());
 
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
--- a/browser/base/content/test/plugins/browser_bug820497.js
+++ b/browser/base/content/test/plugins/browser_bug820497.js
@@ -31,31 +31,29 @@ function pluginBindingAttached() {
   gNumPluginBindingsAttached++;
 
   if (gNumPluginBindingsAttached == 1) {
     var doc = gTestBrowser.contentDocument;
     var testplugin = doc.getElementById("test");
     ok(testplugin, "should have test plugin");
     var secondtestplugin = doc.getElementById("secondtest");
     ok(!secondtestplugin, "should not yet have second test plugin");
-    var notification;
-    waitForNotificationPopup("click-to-play-plugins", gTestBrowser, (notification => {
-      ok(notification, "should have popup notification");
-      // We don't set up the action list until the notification is shown
-      notification.reshow();
-      is(notification.options.pluginData.size, 1, "should be 1 type of plugin in the popup notification");
-      XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addSecondPlugin();
-    }));
+    var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+    ok(notification, "should have popup notification");
+    // We don't set up the action list until the notification is shown
+    notification.reshow();
+    is(notification.options.pluginData.size, 1, "should be 1 type of plugin in the popup notification");
+    XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addSecondPlugin();
   } else if (gNumPluginBindingsAttached == 2) {
     var doc = gTestBrowser.contentDocument;
     var testplugin = doc.getElementById("test");
     ok(testplugin, "should have test plugin");
     var secondtestplugin = doc.getElementById("secondtest");
     ok(secondtestplugin, "should have second test plugin");
     var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
     ok(notification, "should have popup notification");
     notification.reshow();
-    let condition = () => (notification.options.pluginData.size == 2);
-    waitForCondition(condition, finish, "Waited too long for 2 types of plugins in popup notification");
+    is(notification.options.pluginData.size, 2, "should be 2 types of plugin in the popup notification");
+    finish();
   } else {
     ok(false, "if we've gotten here, something is quite wrong");
   }
 }
--- a/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
+++ b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
@@ -54,16 +54,16 @@ function onCrash(event) {
   for (let [name, val] of Iterator(propBagProperties)) {
     let type = typeof val;
     let propVal = type == "string"
                   ? propBag.getPropertyAsAString(name)
                   : propBag.getPropertyAsBool(name);
     is (propVal, val, "Correct property in detail propBag: " + name + ".");
   }
 
-  waitForNotificationBar("plugin-crashed", gTestBrowser, (notification) => {
-    let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
-    ok(notification, "Infobar was shown.");
-    is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM, "Correct priority.");
-    is(notification.getAttribute("label"), "The GlobalTestPlugin plugin has crashed.", "Correct message.");
-    finish();
-  });
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+  let notification = notificationBox.getNotificationWithValue("plugin-crashed");
+
+  ok(notification, "Infobar was shown.");
+  is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM, "Correct priority.");
+  is(notification.getAttribute("label"), "The GlobalTestPlugin plugin has crashed.", "Correct message.");
+  finish();
 }
--- a/browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js
+++ b/browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js
@@ -1,90 +1,19 @@
 /* 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/. */
 
 Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm");
 
 const CRASH_URL = "http://example.com/browser/browser/base/content/test/plugins/plugin_crashCommentAndURL.html";
+
 const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
 
-/**
- * Frame script that will be injected into the test browser
- * to cause plugin crashes, and then manipulate the crashed plugin
- * UI. The specific actions and checks that occur in the frame
- * script for the crashed plugin UI are set in the
- * test:crash-plugin message object sent from the parent. The actions
- * and checks that the parent can specify are:
- *
- * pleaseSubmitStyle: the display style that the pleaseSubmit anonymous element
- *                    should have - example "block", "none".
- * submitComment:     the comment that should be put into the crash report
- * urlOptIn:          true if the submitURLOptIn element should be checked.
- * sendCrashMessage:  if true, the frame script will send a
- *                    test:crash-plugin:crashed message when the plugin has
- *                    crashed. This is used for the last test case, and
- *                    causes the frame script to skip any of the crashed
- *                    plugin UI manipulation, since the last test shows
- *                    no crashed plugin UI.
- */
-function frameScript() {
-  function fail(reason) {
-    sendAsyncMessage("test:crash-plugin:fail", {
-      reason: `Failure from frameScript: ${reason}`,
-    });
-  }
-
-  addMessageListener("test:crash-plugin", (message) => {
-    addEventListener("PluginCrashed", function onPluginCrashed(event) {
-      removeEventListener("PluginCrashed", onPluginCrashed);
-
-      let doc = content.document;
-      let plugin = doc.getElementById("plugin");
-      if (!plugin) {
-        fail("Could not find plugin element");
-        return;
-      }
-
-      let getUI = (anonid) => {
-        return doc.getAnonymousElementByAttribute(plugin, "anonid", anonid);
-      };
-
-      let style = content.getComputedStyle(getUI("pleaseSubmit"));
-      if (style.display != message.data.pleaseSubmitStyle) {
-        fail("Submission UI visibility is not correct. Expected " +
-             `${message.data.pleaseSubmitStyle} and got ${style.display}`);
-        return;
-      }
-
-      if (message.data.sendCrashMessage) {
-        let propBag = event.detail.QueryInterface(Ci.nsIPropertyBag2);
-        let crashID = propBag.getPropertyAsAString("pluginDumpID");
-        sendAsyncMessage("test:crash-plugin:crashed", {
-          crashID: crashID,
-        });
-        return;
-      }
-
-      if (message.data.submitComment) {
-        getUI("submitComment").value = message.data.submitComment;
-      }
-      getUI("submitURLOptIn").checked = message.data.urlOptIn;
-      getUI("submitButton").click();
-    });
-
-    let plugin = content.document.getElementById("test");
-    try {
-      plugin.crash()
-    } catch(e) {
-    }
-  });
-}
-
 function test() {
   // Crashing the plugin takes up a lot of time, so extend the test timeout.
   requestLongerTimeout(runs.length);
   waitForExplicitFinish();
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
 
   // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
   // crash reports.  This test needs them enabled.  The test also needs a mock
@@ -95,28 +24,24 @@ function test() {
             getService(Components.interfaces.nsIEnvironment);
   let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
   let serverURL = env.get("MOZ_CRASHREPORTER_URL");
   env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
   env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
 
   let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
   let browser = gBrowser.getBrowserForTab(tab);
-  let mm = browser.messageManager;
-  mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
-
-  mm.addMessageListener("test:crash-plugin:fail", (message) => {
-    ok(false, message.data.reason);
-  });
-
+  browser.addEventListener("PluginCrashed", onCrash, false);
   Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
 
   registerCleanupFunction(function cleanUp() {
     env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
     env.set("MOZ_CRASHREPORTER_URL", serverURL);
+    gBrowser.selectedBrowser.removeEventListener("PluginCrashed", onCrash,
+                                                 false);
     Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
     gBrowser.removeCurrentTab();
   });
 
   doNextRun();
 }
 
 let runs = [
@@ -146,44 +71,57 @@ function doNextRun() {
       return;
     }
     currentRun = runs.shift();
     let args = ["width", "height"].reduce(function (memo, arg) {
       if (arg in currentRun)
         memo[arg] = currentRun[arg];
       return memo;
     }, {});
-    let mm = gBrowser.selectedBrowser.messageManager;
-
-    if (!currentRun.shouldSubmittionUIBeVisible) {
-      mm.addMessageListener("test:crash-plugin:crash", function onCrash(message) {
-        mm.removeMessageListener("test:crash-plugin:crash", onCrash);
-
-        ok(!!message.data.crashID, "pluginDumpID should be set");
-        CrashSubmit.delete(message.data.crashID);
-        doNextRun();
-      });
-    }
-
-    mm.sendAsyncMessage("test:crash-plugin", {
-      pleaseSubmitStyle: currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
-      submitComment: currentRun.comment,
-      urlOptIn: currentRun.urlOptIn,
-      sendOnCrashMessage: !currentRun.shouldSubmissionUIBeVisible,
-    });
     gBrowser.loadURI(CRASH_URL + "?" +
                      encodeURIComponent(JSON.stringify(args)));
     // And now wait for the crash.
   }
   catch (err) {
     failWithException(err);
     finish();
   }
 }
 
+function onCrash(event) {
+  try {
+    let plugin = gBrowser.contentDocument.getElementById("plugin");
+    let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
+    let style =
+      gBrowser.contentWindow.getComputedStyle(elt("pleaseSubmit"));
+    is(style.display,
+       currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
+       "Submission UI visibility should be correct");
+    if (!currentRun.shouldSubmissionUIBeVisible) {
+      // Done with this run. We don't submit the crash, so we will have to
+      // remove the dump manually.
+      let propBag = event.detail.QueryInterface(Ci.nsIPropertyBag2);
+      let crashID = propBag.getPropertyAsAString("pluginDumpID");
+      ok(!!crashID, "pluginDumpID should be set");
+      CrashSubmit.delete(crashID);
+
+      doNextRun();
+      return;
+    }
+    elt("submitComment").value = currentRun.comment;
+    elt("submitURLOptIn").checked = currentRun.urlOptIn;
+    elt("submitButton").click();
+    // And now wait for the submission status notification.
+  }
+  catch (err) {
+    failWithException(err);
+    doNextRun();
+  }
+}
+
 function onSubmitStatus(subj, topic, data) {
   try {
     // Wait for success or failed, doesn't matter which.
     if (data != "success" && data != "failed")
       return;
 
     let propBag = subj.QueryInterface(Ci.nsIPropertyBag);
     if (data == "success") {
--- a/browser/base/content/test/plugins/browser_pluginnotification.js
+++ b/browser/base/content/test/plugins/browser_pluginnotification.js
@@ -51,17 +51,16 @@ TabOpenListener.prototype = {
       executeSoon(this.closecallback);
       this.closecallback = null;
     }
   }
 };
 
 function test() {
   waitForExplicitFinish();
-  SimpleTest.requestCompleteLog();
   requestLongerTimeout(2);
   registerCleanupFunction(function() {
     clearAllPluginPermissions();
     Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
     Services.prefs.clearUserPref("plugins.hideMissingPluginsNotification");
   });
   Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
 
@@ -793,65 +792,63 @@ function test24a() {
   ok(plugin, "Test 24a, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 24a, Plugin should be click-to-play");
   ok(!objLoadingContent.activated, "Test 24a, plugin should not be activated");
 
   // simulate "always allow"
   notification.reshow();
   PopupNotifications.panel.firstChild._primaryButton.click();
-  waitForCondition(() => objLoadingContent.activated, () => {
-    prepareTest(test24b, gHttpTestRoot + "plugin_test.html");
-  }, "Test 24a, plugin should now be activated.");
-
+  prepareTest(test24b, gHttpTestRoot + "plugin_test.html");
 }
 
 // did the "always allow" work as intended?
 function test24b() {
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "Test 24b, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-  waitForCondition(() => objLoadingContent.activated, () => {
-    setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml", () => {
-      prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html");
-    });
-  }, "Test 24b, plugin should be activated");
+  ok(objLoadingContent.activated, "Test 24b, plugin should be activated");
+  setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml",
+  function() {
+    prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html");
+  });
 }
 
 // the plugin is now blocklisted, so it should not automatically load
 function test24c() {
   var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 24c, Should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "Test 24c, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 24c, Plugin should be vulnerable/updatable");
-  waitForCondition(() => !objLoadingContent.activated, () => {
-    // simulate "always allow"
-    notification.reshow();
-    PopupNotifications.panel.firstChild._primaryButton.click();
+  ok(!objLoadingContent.activated, "Test 24c, plugin should not be activated");
 
-    prepareTest(test24d, gHttpTestRoot + "plugin_test.html");
-  }, "Test 24c, plugin should not be activated");
+  // simulate "always allow"
+  notification.reshow();
+  PopupNotifications.panel.firstChild._primaryButton.click();
+
+  prepareTest(test24d, gHttpTestRoot + "plugin_test.html");
 }
 
 // We should still be able to always allow a plugin after we've seen that it's
 // blocklisted.
 function test24d() {
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "Test 24d, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-  waitForCondition(() => objLoadingContent.activated, () => {
-    // this resets the vulnerable plugin permission
-    setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", () => {
-      clearAllPluginPermissions();
-      resetBlocklist();
-      prepareTest(test25, gTestRoot + "plugin_syncRemoved.html");
-    });
-  }, "Test 24d, plugin should be activated");
+  ok(objLoadingContent.activated, "Test 24d, plugin should be activated");
+
+  // this resets the vulnerable plugin permission
+  setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml",
+  function() {
+    clearAllPluginPermissions();
+    resetBlocklist();
+    prepareTest(test25, gTestRoot + "plugin_syncRemoved.html");
+  });
 }
 
 function test25() {
   let notification = PopupNotifications.getNotification("click-to-play-plugins");
   ok(notification, "Test 25: There should be a plugin notification even if the plugin was immediately removed");
   ok(notification.dismissed, "Test 25: The notification should be dismissed by default");
 
   finishTest();
--- a/browser/base/content/test/plugins/head.js
+++ b/browser/base/content/test/plugins/head.js
@@ -103,33 +103,8 @@ function setAndUpdateBlocklist(aURL, aCa
     _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
   Services.prefs.setCharPref("extensions.blocklist.url", aURL);
   updateBlocklist(aCallback);
 }
 
 function resetBlocklist() {
   Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
 }
-
-function waitForNotificationPopup(notificationID, browser, callback) {
-  let notification;
-  waitForCondition(
-    () => (notification = PopupNotifications.getNotification(notificationID, browser)),
-    () => {
-      ok(notification, `Successfully got the ${notificationID} notification popup`);
-      callback(notification);
-    },
-    `Waited too long for the ${notificationID} notification popup`
-  );
-}
-
-function waitForNotificationBar(notificationID, browser, callback) {
-  let notification;
-  let notificationBox = gBrowser.getNotificationBox(browser);
-  waitForCondition(
-    () => (notification = notificationBox.getNotificationWithValue(notificationID)),
-    () => {
-      ok(notification, `Successfully got the ${notificationID} notification bar`);
-      callback(notification);
-    },
-    `Waited too long for the ${notificationID} notification bar`
-  );
-}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1971,17 +1971,17 @@
           var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box");
 
           if (this._states.SINGLE == state) {
             grid.hidden = true;
             this._setupSingleState();
             return;
           }
 
-          let host = this.notification.options.host;
+          let host = gPluginHandler._getHostFromPrincipal(this.notification.browser.contentWindow.document.nodePrincipal);
           this._setupDescription("pluginActivateMultiple.message", null, host);
 
           var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox");
 
           var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties");
           this._primaryButton.label = dialogStrings.GetStringFromName("button-accept");
           this._primaryButton.setAttribute("default", "true");
 
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -6,31 +6,24 @@
   <head>
     <meta charset="utf-8">
     <!-- Title is set in conversation.js -->
     <title></title>
     <link rel="stylesheet" type="text/css" href="loop/shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/conversation.css">
  </head>
-  <body class="fx-embedded" onload="loop.conversation.init();">
+  <body class="fx-embedded">
 
     <div id="messages"></div>
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
-    <script>
-      window.OTProperties = {
-        cdnURL: 'loop/',
-      };
-      window.OTProperties.assetURL = window.OTProperties.cdnURL + 'sdk-content/';
-      window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
-      window.OTProperties.cssURL = window.OTProperties.assetURL + 'css/ot.css';
-    </script>
+    <script type="text/javascript" src="loop/js/otconfig.js"></script>
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/react-0.11.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -426,8 +426,10 @@ loop.conversation = (function(OT, mozL10
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
     init: init
   };
 })(window.OT, document.mozL10n);
+
+document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -426,8 +426,10 @@ loop.conversation = (function(OT, mozL10
   }
 
   return {
     ConversationRouter: ConversationRouter,
     IncomingCallView: IncomingCallView,
     init: init
   };
 })(window.OT, document.mozL10n);
+
+document.addEventListener('DOMContentLoaded', loop.conversation.init);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/otconfig.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+window.OTProperties = {
+  cdnURL: 'loop/',
+};
+window.OTProperties.assetURL = window.OTProperties.cdnURL + 'sdk-content/';
+window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
+window.OTProperties.cssURL = window.OTProperties.assetURL + 'css/ot.css';
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -546,8 +546,10 @@ loop.panel = (function(_, mozL10n) {
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
+
+document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -546,8 +546,10 @@ loop.panel = (function(_, mozL10n) {
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
+
+document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -5,17 +5,17 @@
 <html>
   <head>
     <meta charset="utf-8">
     <title>Loop Panel</title>
     <link rel="stylesheet" type="text/css" href="loop/shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/panel.css">
   </head>
-  <body class="panel" onload="loop.panel.init();">
+  <body class="panel">
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/shared/libs/react-0.11.1.js"></script>
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -9,16 +9,17 @@ browser.jar:
 
   # Desktop libs (see bottom of this file for TokBox sdk assets)
   content/browser/loop/libs/l10n.js                 (content/libs/l10n.js)
 
   # Desktop script
   content/browser/loop/js/client.js                 (content/js/client.js)
   content/browser/loop/js/desktopRouter.js          (content/js/desktopRouter.js)
   content/browser/loop/js/conversation.js           (content/js/conversation.js)
+  content/browser/loop/js/otconfig.js               (content/js/otconfig.js)
   content/browser/loop/js/panel.js                  (content/js/panel.js)
 
   # Shared styles
   content/browser/loop/shared/css/reset.css         (content/shared/css/reset.css)
   content/browser/loop/shared/css/common.css        (content/shared/css/common.css)
   content/browser/loop/shared/css/panel.css         (content/shared/css/panel.css)
   content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
 
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -44,14 +44,17 @@
   <script src="../../content/js/conversation.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
   <script src="panel_test.js"></script>
   <script>
+    // Stop the default init functions running to avoid conflicts in tests
+    document.removeEventListener('DOMContentLoaded', loop.panel.init);
+    document.removeEventListener('DOMContentLoaded', loop.conversation.init);
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
 </html>
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -1547,16 +1547,24 @@ var Scratchpad = {
    *         the inner window ID
    */
   getInnerWindowId: function SP_getInnerWindowId(aWindow)
   {
     return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
            getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
   },
 
+  updateStatusBar: function SP_updateStatusBar(aEventType)
+  {
+    var statusBarField = document.getElementById("statusbar-line-col");
+    let { line, ch } = this.editor.getCursor();
+    statusBarField.textContent = this.strings.formatStringFromName(
+      "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2);
+  },
+
   /**
    * The Scratchpad window load event handler. This method
    * initializes the Scratchpad window and source editor.
    *
    * @param nsIDOMEvent aEvent
    */
   onLoad: function SP_onLoad(aEvent)
   {
@@ -1608,16 +1616,19 @@ var Scratchpad = {
     };
 
     this.editor = new Editor(config);
     let editorElement = document.querySelector("#scratchpad-editor");
     this.editor.appendTo(editorElement).then(() => {
       var lines = initialText.split("\n");
 
       this.editor.on("change", this._onChanged);
+      // Keep a reference to the bound version for use in onUnload.
+      this.updateStatusBar = Scratchpad.updateStatusBar.bind(this);
+      this.editor.on("cursorActivity", this.updateStatusBar);
       let okstring = this.strings.GetStringFromName("selfxss.okstring");
       let msg = this.strings.formatStringFromName("selfxss.msg", [okstring], 1);
       this._onPaste = WebConsoleUtils.pasteHandlerGen(this.editor.container.contentDocument.body,
                                                       document.querySelector('#scratchpad-notificationbox'),
                                                       msg, okstring);
       editorElement.addEventListener("paste", this._onPaste);
       editorElement.addEventListener("drop", this._onPaste);
       this.editor.on("save", () => this.saveFile());
@@ -1693,16 +1704,17 @@ var Scratchpad = {
     CloseObserver.uninit();
     if (this._onPaste) {
       let editorElement = document.querySelector("#scratchpad-editor");
       editorElement.removeEventListener("paste", this._onPaste);
       editorElement.removeEventListener("drop", this._onPaste);
       this._onPaste = null;
     }
     this.editor.off("change", this._onChanged);
+    this.editor.off("cursorActivity", this.updateStatusBar);
     this.editor.destroy();
     this.editor = null;
 
     if (this._sidebar) {
       this._sidebar.destroy();
       this._sidebar = null;
     }
 
--- a/browser/devtools/scratchpad/scratchpad.xul
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -398,11 +398,12 @@
     <splitter class="devtools-side-splitter"/>
     <tabbox id="scratchpad-sidebar" class="devtools-sidebar-tabs"
                                     width="300"
                                     hidden="true">
       <tabs/>
       <tabpanels flex="1"/>
     </tabbox>
   </hbox>
+  <toolbar id="statusbar-line-col" class="chromeclass-toolbar"/>
 </notificationbox>
 
 </window>
--- a/browser/devtools/scratchpad/test/browser_scratchpad_run_error_goto_line.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_run_error_goto_line.js
@@ -39,11 +39,22 @@ function runTests(sw)
     let inputLine = lineInput.value;
     is(inputLine, errorLine, "jumpToLine input field is set from editor selection");
     EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView);
     // CodeMirror lines and columns are 0-based, Scratchpad UI and error
     // stack are 1-based.
     let cursor = sp.editor.getCursor();
     is(cursor.line + 1, inputLine, "jumpToLine goto error location (line)");
     is(cursor.ch + 1, 1, "jumpToLine goto error location (column)");
+  }, error => {
+    ok(false, error);
+    finish();
+  }).then(() => {
+    var statusBarField = sp.editor.container.ownerDocument.querySelector("#statusbar-line-col");
+    let { line, ch } = sp.editor.getCursor();
+    is(statusBarField.textContent, sp.strings.formatStringFromName(
+      "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2), "statusbar text is correct (" + statusBarField.textContent + ")");
+    finish();
+  }, error => {
+    ok(false, error);
     finish();
   });
 }
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -361,16 +361,18 @@
 @BINPATH@/browser/components/nsSetDefaultBrowser.manifest
 @BINPATH@/browser/components/nsSetDefaultBrowser.js
 @BINPATH@/browser/components/BrowserDownloads.manifest
 @BINPATH@/browser/components/DownloadsStartup.js
 @BINPATH@/browser/components/DownloadsUI.js
 @BINPATH@/browser/components/BrowserPlaces.manifest
 @BINPATH@/browser/components/devtools-clhandler.manifest
 @BINPATH@/browser/components/devtools-clhandler.js
+@BINPATH@/browser/components/webideCli.js
+@BINPATH@/browser/components/webideComponents.manifest
 @BINPATH@/browser/components/Experiments.manifest
 @BINPATH@/browser/components/ExperimentsService.js
 @BINPATH@/browser/components/translation.manifest
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/BrowserPageThumbs.manifest
 @BINPATH@/components/crashmonitor.manifest
 @BINPATH@/components/nsCrashMonitor.js
--- a/browser/locales/en-US/chrome/browser/devtools/scratchpad.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/scratchpad.properties
@@ -73,16 +73,21 @@ scratchpad.noargs=Scratchpad was created
 # LOCALIZATION NOTE  (notification.browserContext): This is the message displayed
 # over the top of the editor when the user has switched to browser context.
 browserContext.notification=This scratchpad executes in the Browser context.
 
 # LOCALIZATION NOTE (help.openDocumentationPage): This returns a localized link with
 # documentation for Scratchpad on MDN.
 help.openDocumentationPage=https://developer.mozilla.org/en/Tools/Scratchpad
 
+# LOCALIZATION NOTE (scratchpad.statusBarLineCol): Line, Column
+# information displayed in statusbar when selection is made in
+# Scratchpad.
+scratchpad.statusBarLineCol  = Line %1$S, Col %2$S
+
 # LOCALIZATION NOTE (fileExists.notification): This is the message displayed
 # over the top of the the editor when a file does not exist.
 fileNoLongerExists.notification=This file no longer exists.
 
 # LOCALIZATION NOTE (propertiesFilterPlaceholder): this is the text that
 # appears in the filter text box for the properties view container.
 propertiesFilterPlaceholder=Filter properties
 
deleted file mode 100644
--- a/browser/modules/PluginContent.jsm
+++ /dev/null
@@ -1,992 +0,0 @@
-# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-# 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/.
-
-"use strict";
-
-let Cc = Components.classes;
-let Ci = Components.interfaces;
-let Cu = Components.utils;
-
-this.EXPORTED_SYMBOLS = [ "PluginContent" ];
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
-  const url = "chrome://browser/locale/browser.properties";
-  return Services.strings.createBundle(url);
-});
-
-this.PluginContent = function (global) {
-  this.init(global);
-}
-
-PluginContent.prototype = {
-  init: function (global) {
-    this.global = global;
-    // Need to hold onto the content window or else it'll get destroyed
-    this.content = this.global.content;
-    // Cache of plugin actions for the current page.
-    this.pluginData = new Map();
-
-    // Note that the XBL binding is untrusted
-    global.addEventListener("PluginBindingAttached", this, true, true);
-    global.addEventListener("PluginCrashed",         this, true);
-    global.addEventListener("PluginOutdated",        this, true);
-    global.addEventListener("PluginInstantiated",    this, true);
-    global.addEventListener("PluginRemoved",         this, true);
-    global.addEventListener("unload",                this);
-
-    global.addEventListener("pageshow", (event) => this.onPageShow(event), true);
-
-    global.addMessageListener("BrowserPlugins:ActivatePlugins", this);
-    global.addMessageListener("BrowserPlugins:NotificationRemoved", this);
-    global.addMessageListener("BrowserPlugins:NotificationShown", this);
-    global.addMessageListener("BrowserPlugins:ContextMenuCommand", this);
-  },
-
-  uninit: function() {
-    delete this.global;
-    delete this.content;
-  },
-
-  receiveMessage: function (msg) {
-    switch (msg.name) {
-      case "BrowserPlugins:ActivatePlugins":
-        this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
-        break;
-      case "BrowserPlugins:NotificationRemoved":
-        this.clearPluginDataCache();
-        break;
-      case "BrowserPlugins:NotificationShown":
-        setTimeout(() => this.updateNotificationUI(), 0);
-        break;
-      case "BrowserPlugins:ContextMenuCommand":
-        switch (msg.data.command) {
-          case "play":
-            this._showClickToPlayNotification(msg.objects.plugin, true);
-            break;
-          case "hide":
-            this.hideClickToPlayOverlay(msg.objects.plugin);
-            break;
-        }
-        break;
-    }
-  },
-
-  onPageShow: function (event) {
-    // Ignore events that aren't from the main document.
-    if (this.global.content && event.target != this.global.content.document) {
-      return;
-    }
-
-    // The PluginClickToPlay events are not fired when navigating using the
-    // BF cache. |persisted| is true when the page is loaded from the
-    // BF cache, so this code reshows the notification if necessary.
-    if (event.persisted) {
-      this.reshowClickToPlayNotification();
-    }
-  },
-
-  getPluginUI: function (plugin, anonid) {
-    return plugin.ownerDocument.
-           getAnonymousElementByAttribute(plugin, "anonid", anonid);
-  },
-
-  _getPluginInfo: function (pluginElement) {
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-    pluginElement.QueryInterface(Ci.nsIObjectLoadingContent);
-
-    let tagMimetype;
-    let pluginName = gNavigatorBundle.GetStringFromName("pluginInfo.unknownPlugin");
-    let pluginTag = null;
-    let permissionString = null;
-    let fallbackType = null;
-    let blocklistState = null;
-
-    tagMimetype = pluginElement.actualType;
-    if (tagMimetype == "") {
-      tagMimetype = pluginElement.type;
-    }
-
-    if (this.isKnownPlugin(pluginElement)) {
-      pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType);
-      pluginName = this.makeNicePluginName(pluginTag.name);
-
-      permissionString = pluginHost.getPermissionStringForType(pluginElement.actualType);
-      fallbackType = pluginElement.defaultFallbackType;
-      blocklistState = pluginHost.getBlocklistStateForType(pluginElement.actualType);
-      // Make state-softblocked == state-notblocked for our purposes,
-      // they have the same UI. STATE_OUTDATED should not exist for plugin
-      // items, but let's alias it anyway, just in case.
-      if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED ||
-          blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) {
-        blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
-      }
-    }
-
-    return { mimetype: tagMimetype,
-             pluginName: pluginName,
-             pluginTag: pluginTag,
-             permissionString: permissionString,
-             fallbackType: fallbackType,
-             blocklistState: blocklistState,
-           };
-  },
-
-  // Map the plugin's name to a filtered version more suitable for user UI.
-  makeNicePluginName : function (aName) {
-    if (aName == "Shockwave Flash")
-      return "Adobe Flash";
-
-    // Clean up the plugin name by stripping off parenthetical clauses,
-    // trailing version numbers or "plugin".
-    // EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar"
-    // Do this by first stripping the numbers, etc. off the end, and then
-    // removing "Plugin" (and then trimming to get rid of any whitespace).
-    // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled)
-    let newName = aName.replace(/\(.*?\)/g, "").
-                        replace(/[\s\d\.\-\_\(\)]+$/, "").
-                        replace(/\bplug-?in\b/i, "").trim();
-    return newName;
-  },
-
-  /**
-   * Update the visibility of the plugin overlay.
-   */
-  setVisibility : function (plugin, overlay, shouldShow) {
-    overlay.classList.toggle("visible", shouldShow);
-  },
-
-  /**
-   * Check whether the plugin should be visible on the page. A plugin should
-   * not be visible if the overlay is too big, or if any other page content
-   * overlays it.
-   *
-   * This function will handle showing or hiding the overlay.
-   * @returns true if the plugin is invisible.
-   */
-  shouldShowOverlay : function (plugin, overlay) {
-    // If the overlay size is 0, we haven't done layout yet. Presume that
-    // plugins are visible until we know otherwise.
-    if (overlay.scrollWidth == 0) {
-      return true;
-    }
-
-    // Is the <object>'s size too small to hold what we want to show?
-    let pluginRect = plugin.getBoundingClientRect();
-    // XXX bug 446693. The text-shadow on the submitted-report text at
-    //     the bottom causes scrollHeight to be larger than it should be.
-    let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) ||
-                    (overlay.scrollHeight - 5 > Math.ceil(pluginRect.height));
-    if (overflows) {
-      return false;
-    }
-
-    // Is the plugin covered up by other content so that it is not clickable?
-    // Floating point can confuse .elementFromPoint, so inset just a bit
-    let left = pluginRect.left + 2;
-    let right = pluginRect.right - 2;
-    let top = pluginRect.top + 2;
-    let bottom = pluginRect.bottom - 2;
-    let centerX = left + (right - left) / 2;
-    let centerY = top + (bottom - top) / 2;
-    let points = [[left, top],
-                   [left, bottom],
-                   [right, top],
-                   [right, bottom],
-                   [centerX, centerY]];
-
-    if (right <= 0 || top <= 0) {
-      return false;
-    }
-
-    let contentWindow = plugin.ownerDocument.defaultView;
-    let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-
-    for (let [x, y] of points) {
-      let el = cwu.elementFromPoint(x, y, true, true);
-      if (el !== plugin) {
-        return false;
-      }
-    }
-
-    return true;
-  },
-
-  addLinkClickCallback: function (linkNode, callbackName /*callbackArgs...*/) {
-    // XXX just doing (callback)(arg) was giving a same-origin error. bug?
-    let self = this;
-    let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
-    linkNode.addEventListener("click",
-                              function(evt) {
-                                if (!evt.isTrusted)
-                                  return;
-                                evt.preventDefault();
-                                if (callbackArgs.length == 0)
-                                  callbackArgs = [ evt ];
-                                (self[callbackName]).apply(self, callbackArgs);
-                              },
-                              true);
-
-    linkNode.addEventListener("keydown",
-                              function(evt) {
-                                if (!evt.isTrusted)
-                                  return;
-                                if (evt.keyCode == evt.DOM_VK_RETURN) {
-                                  evt.preventDefault();
-                                  if (callbackArgs.length == 0)
-                                    callbackArgs = [ evt ];
-                                  evt.preventDefault();
-                                  (self[callbackName]).apply(self, callbackArgs);
-                                }
-                              },
-                              true);
-  },
-
-  // Helper to get the binding handler type from a plugin object
-  _getBindingType : function(plugin) {
-    if (!(plugin instanceof Ci.nsIObjectLoadingContent))
-      return null;
-
-    switch (plugin.pluginFallbackType) {
-      case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
-        return "PluginNotFound";
-      case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED:
-        return "PluginDisabled";
-      case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED:
-        return "PluginBlocklisted";
-      case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED:
-        return "PluginOutdated";
-      case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
-        return "PluginClickToPlay";
-      case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
-        return "PluginVulnerableUpdatable";
-      case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
-        return "PluginVulnerableNoUpdate";
-      case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW:
-        return "PluginPlayPreview";
-      default:
-        // Not all states map to a handler
-        return null;
-    }
-  },
-
-  handleEvent: function (event) {
-    let eventType = event.type;
-
-    if (eventType == "unload") {
-      this.uninit();
-      return;
-    }
-
-    if (eventType == "PluginRemoved") {
-      this.updateNotificationUI();
-      return;
-    }
-
-    if (eventType == "click") {
-      this.onOverlayClick(event);
-      return;
-    }
-
-    if (eventType == "PluginCrashed" &&
-        !(event.target instanceof Ci.nsIObjectLoadingContent)) {
-      // If the event target is not a plugin object (i.e., an <object> or
-      // <embed> element), this call is for a window-global plugin.
-      this.pluginInstanceCrashed(event.target, event);
-      return;
-    }
-
-    let plugin = event.target;
-    let doc = plugin.ownerDocument;
-
-    if (!(plugin instanceof Ci.nsIObjectLoadingContent))
-      return;
-
-    if (eventType == "PluginBindingAttached") {
-      // The plugin binding fires this event when it is created.
-      // As an untrusted event, ensure that this object actually has a binding
-      // and make sure we don't handle it twice
-      let overlay = this.getPluginUI(plugin, "main");
-      if (!overlay || overlay._bindingHandled) {
-        return;
-      }
-      overlay._bindingHandled = true;
-
-      // Lookup the handler for this binding
-      eventType = this._getBindingType(plugin);
-      if (!eventType) {
-        // Not all bindings have handlers
-        return;
-      }
-    }
-
-    let shouldShowNotification = false;
-    switch (eventType) {
-      case "PluginCrashed":
-        this.pluginInstanceCrashed(plugin, event);
-        break;
-
-      case "PluginNotFound": {
-        let installable = this.showInstallNotification(plugin, eventType);
-        let contentWindow = plugin.ownerDocument.defaultView;
-        // For non-object plugin tags, register a click handler to install the
-        // plugin. Object tags can, and often do, deal with that themselves,
-        // so don't stomp on the page developers toes.
-        if (installable && !(plugin instanceof contentWindow.HTMLObjectElement)) {
-          let installStatus = this.getPluginUI(plugin, "installStatus");
-          installStatus.setAttribute("installable", "true");
-          let iconStatus = this.getPluginUI(plugin, "icon");
-          iconStatus.setAttribute("installable", "true");
-
-          let installLink = this.getPluginUI(plugin, "installPluginLink");
-          this.addLinkClickCallback(installLink, "installSinglePlugin", plugin);
-        }
-        break;
-      }
-
-      case "PluginBlocklisted":
-      case "PluginOutdated":
-        shouldShowNotification = true;
-        break;
-
-      case "PluginVulnerableUpdatable":
-        let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink");
-        this.addLinkClickCallback(updateLink, "forwardCallback", "openPluginUpdatePage");
-        /* FALLTHRU */
-
-      case "PluginVulnerableNoUpdate":
-      case "PluginClickToPlay":
-        this._handleClickToPlayEvent(plugin);
-        let overlay = this.getPluginUI(plugin, "main");
-        let pluginName = this._getPluginInfo(plugin).pluginName;
-        let messageString = gNavigatorBundle.formatStringFromName("PluginClickToActivate", [pluginName], 1);
-        let overlayText = this.getPluginUI(plugin, "clickToPlay");
-        overlayText.textContent = messageString;
-        if (eventType == "PluginVulnerableUpdatable" ||
-            eventType == "PluginVulnerableNoUpdate") {
-          let vulnerabilityString = gNavigatorBundle.GetStringFromName(eventType);
-          let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus");
-          vulnerabilityText.textContent = vulnerabilityString;
-        }
-        shouldShowNotification = true;
-        break;
-
-      case "PluginPlayPreview":
-        this._handlePlayPreviewEvent(plugin);
-        break;
-
-      case "PluginDisabled":
-        let manageLink = this.getPluginUI(plugin, "managePluginsLink");
-        this.addLinkClickCallback(manageLink, "forwardCallback", "managePlugins");
-        shouldShowNotification = true;
-        break;
-
-      case "PluginInstantiated":
-        shouldShowNotification = true;
-        break;
-    }
-
-    // Show the in-content UI if it's not too big. The crashed plugin handler already did this.
-    if (eventType != "PluginCrashed") {
-      let overlay = this.getPluginUI(plugin, "main");
-      if (overlay != null) {
-        this.setVisibility(plugin, overlay,
-                           this.shouldShowOverlay(plugin, overlay));
-        let resizeListener = (event) => {
-          this.setVisibility(plugin, overlay,
-            this.shouldShowOverlay(plugin, overlay));
-          this.updateNotificationUI();
-        };
-        plugin.addEventListener("overflow", resizeListener);
-        plugin.addEventListener("underflow", resizeListener);
-      }
-    }
-
-    let closeIcon = this.getPluginUI(plugin, "closeIcon");
-    if (closeIcon) {
-      closeIcon.addEventListener("click", event => {
-        if (event.button == 0 && event.isTrusted)
-          this.hideClickToPlayOverlay(plugin);
-      }, true);
-    }
-
-    if (shouldShowNotification) {
-      this._showClickToPlayNotification(plugin, false);
-    }
-  },
-
-  isKnownPlugin: function (objLoadingContent) {
-    return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) ==
-            Ci.nsIObjectLoadingContent.TYPE_PLUGIN);
-  },
-
-  canActivatePlugin: function (objLoadingContent) {
-    // if this isn't a known plugin, we can't activate it
-    // (this also guards pluginHost.getPermissionStringForType against
-    // unexpected input)
-    if (!this.isKnownPlugin(objLoadingContent))
-      return false;
-
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-    let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
-    let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal;
-    let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
-
-    let isFallbackTypeValid =
-      objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
-      objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
-
-    if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) {
-      // checking if play preview is subject to CTP rules
-      let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType);
-      isFallbackTypeValid = !playPreviewInfo.ignoreCTP;
-    }
-
-    return !objLoadingContent.activated &&
-           pluginPermission != Ci.nsIPermissionManager.DENY_ACTION &&
-           isFallbackTypeValid;
-  },
-
-  hideClickToPlayOverlay: function (plugin) {
-    let overlay = this.getPluginUI(plugin, "main");
-    if (overlay) {
-      overlay.classList.remove("visible");
-    }
-  },
-
-  stopPlayPreview: function (plugin, playPlugin) {
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    if (objLoadingContent.activated)
-      return;
-
-    if (playPlugin)
-      objLoadingContent.playPlugin();
-    else
-      objLoadingContent.cancelPlayPreview();
-  },
-
-  // Callback for user clicking on a missing (unsupported) plugin.
-  installSinglePlugin: function (plugin) {
-    this.global.sendAsyncMessage("PluginContent:InstallSinglePlugin", {
-      pluginInfo: this._getPluginInfo(plugin),
-    });
-  },
-
-  // Forward a link click callback to the chrome process.
-  forwardCallback: function (name) {
-    this.global.sendAsyncMessage("PluginContent:LinkClickCallback", { name: name });
-  },
-
-#ifdef MOZ_CRASHREPORTER
-  submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) {
-    let keyVals = {};
-    if (plugin) {
-      let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
-      if (userComment)
-        keyVals.PluginUserComment = userComment;
-      if (this.getPluginUI(plugin, "submitURLOptIn").checked)
-        keyVals.PluginContentURL = plugin.ownerDocument.URL;
-    }
-
-    this.global.sendAsyncMessage("PluginContent:SubmitReport", {
-      pluginDumpID: pluginDumpID,
-      browserDumpID: browserDumpID,
-      keyVals: keyVals,
-    });
-  },
-#endif
-
-  reloadPage: function () {
-    this.global.content.location.reload();
-  },
-
-  showInstallNotification: function (plugin) {
-    let [shown] = this.global.sendSyncMessage("PluginContent:ShowInstallNotification", {
-      pluginInfo: this._getPluginInfo(plugin),
-    });
-    return shown;
-  },
-
-  // Event listener for click-to-play plugins.
-  _handleClickToPlayEvent: function (plugin) {
-    let doc = plugin.ownerDocument;
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    // guard against giving pluginHost.getPermissionStringForType a type
-    // not associated with any known plugin
-    if (!this.isKnownPlugin(objLoadingContent))
-      return;
-    let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
-    let principal = doc.defaultView.top.document.nodePrincipal;
-    let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
-
-    let overlay = this.getPluginUI(plugin, "main");
-
-    if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) {
-      if (overlay) {
-        overlay.classList.remove("visible");
-      }
-      return;
-    }
-
-    if (overlay) {
-      overlay.addEventListener("click", this, true);
-    }
-  },
-
-  onOverlayClick: function (event) {
-    let document = event.target.ownerDocument;
-    let plugin = document.getBindingParent(event.target);
-    let contentWindow = plugin.ownerDocument.defaultView.top;
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    // Have to check that the target is not the link to update the plugin
-    if (!(event.originalTarget instanceof contentWindow.HTMLAnchorElement) &&
-        (event.originalTarget.getAttribute('anonid') != 'closeIcon') &&
-          event.button == 0 && event.isTrusted) {
-      this._showClickToPlayNotification(plugin, true);
-    event.stopPropagation();
-    event.preventDefault();
-    }
-  },
-
-  _handlePlayPreviewEvent: function (plugin) {
-    let doc = plugin.ownerDocument;
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-    let pluginInfo = this._getPluginInfo(plugin);
-    let playPreviewInfo = pluginHost.getPlayPreviewInfo(pluginInfo.mimetype);
-
-    let previewContent = this.getPluginUI(plugin, "previewPluginContent");
-    let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
-    if (!iframe) {
-      // lazy initialization of the iframe
-      iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
-      iframe.className = "previewPluginContentFrame";
-      previewContent.appendChild(iframe);
-
-      // Force a style flush, so that we ensure our binding is attached.
-      plugin.clientTop;
-    }
-    iframe.src = playPreviewInfo.redirectURL;
-
-    // MozPlayPlugin event can be dispatched from the extension chrome
-    // code to replace the preview content with the native plugin
-    let playPluginHandler = (event) => {
-      if (!event.isTrusted)
-        return;
-
-      previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true);
-
-      let playPlugin = !event.detail;
-      this.stopPlayPreview(plugin, playPlugin);
-
-      // cleaning up: removes overlay iframe from the DOM
-      let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
-      if (iframe)
-        previewContent.removeChild(iframe);
-    };
-
-    previewContent.addEventListener("MozPlayPlugin", playPluginHandler, true);
-
-    if (!playPreviewInfo.ignoreCTP) {
-      this._showClickToPlayNotification(plugin, false);
-    }
-  },
-
-  reshowClickToPlayNotification: function () {
-    let contentWindow = this.global.content;
-    let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-    let plugins = cwu.plugins;
-    for (let plugin of plugins) {
-      let overlay = this.getPluginUI(plugin, "main");
-      if (overlay)
-        overlay.removeEventListener("click", this, true);
-      let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-      if (this.canActivatePlugin(objLoadingContent))
-        this._handleClickToPlayEvent(plugin);
-    }
-    this._showClickToPlayNotification(null, false);
-  },
-
-  // Match the behaviour of nsPermissionManager
-  _getHostFromPrincipal: function (principal) {
-    if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) {
-      return "(null)";
-    }
-
-    try {
-      if (principal.URI.host)
-        return principal.URI.host;
-    } catch (e) {}
-
-    return principal.origin;
-  },
-
-  /**
-   * Activate the plugins that the user has specified.
-   */
-  activatePlugins: function (pluginInfo, newState) {
-    let contentWindow = this.global.content;
-    let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-    let plugins = cwu.plugins;
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-
-    let pluginFound = false;
-    for (let plugin of plugins) {
-      plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-      if (!this.isKnownPlugin(plugin)) {
-        continue;
-      }
-      if (pluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) {
-        pluginFound = true;
-        if (newState == "block") {
-          plugin.reload(true);
-        } else {
-          if (this.canActivatePlugin(plugin)) {
-            let overlay = this.getPluginUI(plugin, "main");
-            if (overlay) {
-              overlay.removeEventListener("click", this, true);
-            }
-            plugin.playPlugin();
-          }
-        }
-      }
-    }
-
-    // If there are no instances of the plugin on the page any more, what the
-    // user probably needs is for us to allow and then refresh.
-    if (newState != "block" && !pluginFound) {
-      this.reloadPage();
-    }
-    this.updateNotificationUI();
-  },
-
-  _showClickToPlayNotification: function (plugin, showNow) {
-    let plugins = [];
-
-    // If plugin is null, that means the user has navigated back to a page with
-    // plugins, and we need to collect all the plugins.
-    if (plugin === null) {
-      let contentWindow = this.global.content;
-      let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDOMWindowUtils);
-      // cwu.plugins may contain non-plugin <object>s, filter them out
-      plugins = cwu.plugins.filter((plugin) =>
-        plugin.getContentTypeForMIMEType(plugin.actualType) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN);
-
-      if (plugins.length == 0) {
-        this.removeNotification("click-to-play-plugins");
-        return;
-      }
-    } else {
-      plugins = [plugin];
-    }
-
-    let pluginData = this.pluginData;
-
-    let principal = this.global.content.document.nodePrincipal;
-    let principalHost = this._getHostFromPrincipal(principal);
-
-    for (let p of plugins) {
-      let pluginInfo = this._getPluginInfo(p);
-      if (pluginInfo.permissionString === null) {
-        Cu.reportError("No permission string for active plugin.");
-        continue;
-      }
-      if (pluginData.has(pluginInfo.permissionString)) {
-        continue;
-      }
-
-      let permissionObj = Services.perms.
-        getPermissionObject(principal, pluginInfo.permissionString, false);
-      if (permissionObj) {
-        pluginInfo.pluginPermissionHost = permissionObj.host;
-        pluginInfo.pluginPermissionType = permissionObj.expireType;
-      }
-      else {
-        pluginInfo.pluginPermissionHost = principalHost;
-        pluginInfo.pluginPermissionType = undefined;
-      }
-
-      this.pluginData.set(pluginInfo.permissionString, pluginInfo);
-    }
-
-    this.global.sendAsyncMessage("PluginContent:ShowClickToPlayNotification", {
-      plugins: [... this.pluginData.values()],
-      showNow: showNow,
-      host: principalHost,
-    }, null, principal);
-  },
-
-  updateNotificationUI: function () {
-    // Make a copy of the actions from the last popup notification.
-    let haveInsecure = false;
-    let actions = new Map();
-    for (let action of this.pluginData.values()) {
-      switch (action.fallbackType) {
-        // haveInsecure will trigger the red flashing icon and the infobar
-        // styling below
-        case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
-        case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
-          haveInsecure = true;
-          // fall through
-
-        case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
-          actions.set(action.permissionString, action);
-          continue;
-      }
-    }
-
-    // Remove plugins that are already active, or large enough to show an overlay.
-    let contentWindow = this.global.content;
-    let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-    for (let plugin of cwu.plugins) {
-      let info = this._getPluginInfo(plugin);
-      if (!actions.has(info.permissionString)) {
-        continue;
-      }
-      let fallbackType = info.fallbackType;
-      if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
-        actions.delete(info.permissionString);
-        if (actions.size == 0) {
-          break;
-        }
-        continue;
-      }
-      if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
-          fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE &&
-          fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE) {
-        continue;
-      }
-      let overlay = this.getPluginUI(plugin, "main");
-      if (!overlay) {
-        continue;
-      }
-      let shouldShow = this.shouldShowOverlay(plugin, overlay);
-      this.setVisibility(plugin, overlay, shouldShow);
-      if (shouldShow) {
-        actions.delete(info.permissionString);
-        if (actions.size == 0) {
-          break;
-        }
-      }
-    }
-
-    // If there are any items remaining in `actions` now, they are hidden
-    // plugins that need a notification bar.
-    let principal = contentWindow.document.nodePrincipal;
-    this.global.sendAsyncMessage("PluginContent:UpdateHiddenPluginUI", {
-      haveInsecure: haveInsecure,
-      actions: [... actions.values()],
-      host: this._getHostFromPrincipal(principal),
-    }, null, principal);
-  },
-
-  removeNotification: function (name) {
-    this.global.sendAsyncMessage("PluginContent:RemoveNotification", { name: name });
-  },
-
-  clearPluginDataCache: function () {
-    this.pluginData.clear();
-  },
-
-  hideNotificationBar: function (name) {
-    this.global.sendAsyncMessage("PluginContent:HideNotificationBar", { name: name });
-  },
-
-  // Crashed-plugin event listener. Called for every instance of a
-  // plugin in content.
-  pluginInstanceCrashed: function (target, aEvent) {
-    // Ensure the plugin and event are of the right type.
-    if (!(aEvent instanceof Ci.nsIDOMCustomEvent))
-      return;
-
-    let propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
-    let submittedReport = propBag.getPropertyAsBool("submittedCrashReport");
-    let doPrompt        = true; // XXX followup for .getPropertyAsBool("doPrompt");
-    let submitReports   = true; // XXX followup for .getPropertyAsBool("submitReports");
-    let pluginName      = propBag.getPropertyAsAString("pluginName");
-    let pluginDumpID    = propBag.getPropertyAsAString("pluginDumpID");
-    let browserDumpID   = null;
-    let gmpPlugin       = false;
-
-    try {
-      browserDumpID = propBag.getPropertyAsAString("browserDumpID");
-    } catch (e) {
-      // For GMP crashes we don't get a browser dump.
-    }
-
-    try {
-      gmpPlugin = propBag.getPropertyAsBool("gmpPlugin");
-    } catch (e) {
-      // This property is only set for GMP plugins.
-    }
-
-    // For non-GMP plugins, remap the plugin name to a more user-presentable form.
-    if (!gmpPlugin) {
-      pluginName = this.makeNicePluginName(pluginName);
-    }
-
-    let messageString = gNavigatorBundle.formatStringFromName("crashedpluginsMessage.title", [pluginName], 1);
-
-    let plugin = null, doc;
-    if (target instanceof Ci.nsIObjectLoadingContent) {
-      plugin = target;
-      doc = plugin.ownerDocument;
-    } else {
-      doc = target.document;
-      if (!doc) {
-        return;
-      }
-      // doPrompt is specific to the crashed plugin overlay, and
-      // therefore is not applicable for window-global plugins.
-      doPrompt = false;
-    }
-
-    let status;
-#ifdef MOZ_CRASHREPORTER
-    // Determine which message to show regarding crash reports.
-    if (submittedReport) { // submitReports && !doPrompt, handled in observer
-      status = "submitted";
-    }
-    else if (!submitReports && !doPrompt) {
-      status = "noSubmit";
-    }
-    else if (!pluginDumpID) {
-      // If we don't have a minidumpID, we can't (or didn't) submit anything.
-      // This can happen if the plugin is killed from the task manager.
-      status = "noReport";
-    }
-    else {
-      status = "please";
-    }
-
-    // If we don't have a minidumpID, we can't (or didn't) submit anything.
-    // This can happen if the plugin is killed from the task manager.
-    if (!pluginDumpID) {
-        status = "noReport";
-    }
-
-    // If we're showing the link to manually trigger report submission, we'll
-    // want to be able to update all the instances of the UI for this crash to
-    // show an updated message when a report is submitted.
-    if (doPrompt) {
-      let observer = {
-        QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                               Ci.nsISupportsWeakReference]),
-        observe : (subject, topic, data) => {
-          let propertyBag = subject;
-          if (!(propertyBag instanceof Ci.nsIPropertyBag2))
-            return;
-          // Ignore notifications for other crashes.
-          if (propertyBag.get("minidumpID") != pluginDumpID)
-            return;
-          let statusDiv = this.getPluginUI(plugin, "submitStatus");
-          statusDiv.setAttribute("status", data);
-        },
-
-        handleEvent : function(event) {
-            // Not expected to be called, just here for the closure.
-        }
-      }
-
-      // Use a weak reference, so we don't have to remove it...
-      Services.obs.addObserver(observer, "crash-report-status", true);
-      // ...alas, now we need something to hold a strong reference to prevent
-      // it from being GC. But I don't want to manually manage the reference's
-      // lifetime (which should be no greater than the page).
-      // Clever solution? Use a closue with an event listener on the document.
-      // When the doc goes away, so do the listener references and the closure.
-      doc.addEventListener("mozCleverClosureHack", observer, false);
-    }
-#endif
-
-    let isShowing = false;
-
-    if (plugin) {
-      // If there's no plugin (an <object> or <embed> element), this call is
-      // for a window-global plugin. In this case, there's no overlay to show.
-      isShowing = _setUpPluginOverlay.call(this, plugin, doPrompt);
-    }
-
-    if (isShowing) {
-      // If a previous plugin on the page was too small and resulted in adding a
-      // notification bar, then remove it because this plugin instance it big
-      // enough to serve as in-content notification.
-      this.hideNotificationBar("plugin-crashed");
-      doc.mozNoPluginCrashedNotification = true;
-    } else {
-      // If another plugin on the page was large enough to show our UI, we don't
-      // want to show a notification bar.
-      if (!doc.mozNoPluginCrashedNotification) {
-        this.global.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
-          messageString: messageString,
-          pluginDumpID: pluginDumpID,
-          browserDumpID: browserDumpID,
-        });
-        // Remove the notification when the page is reloaded.
-        doc.defaultView.top.addEventListener("unload", event => {
-          this.hideNotificationBar("plugin-crashed");
-        }, false);
-      }
-    }
-
-    // Configure the crashed-plugin placeholder.
-    // Returns true if the plugin overlay is visible.
-    function _setUpPluginOverlay(plugin, doPromptSubmit) {
-      if (!plugin) {
-        return false;
-      }
-
-      // Force a layout flush so the binding is attached.
-      plugin.clientTop;
-      let overlay = this.getPluginUI(plugin, "main");
-      let statusDiv = this.getPluginUI(plugin, "submitStatus");
-
-      if (doPromptSubmit) {
-        this.getPluginUI(plugin, "submitButton").addEventListener("click",
-        function (event) {
-          if (event.button != 0 || !event.isTrusted)
-            return;
-          this.submitReport(pluginDumpID, browserDumpID, plugin);
-          pref.setBoolPref("", optInCB.checked);
-        }.bind(this));
-        let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
-        let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL");
-        optInCB.checked = pref.getBoolPref("");
-      }
-
-      statusDiv.setAttribute("status", status);
-
-      let helpIcon = this.getPluginUI(plugin, "helpIcon");
-      this.addLinkClickCallback(helpIcon, "openHelpPage");
-
-      let crashText = this.getPluginUI(plugin, "crashedText");
-      crashText.textContent = messageString;
-
-      let link = this.getPluginUI(plugin, "reloadLink");
-      this.addLinkClickCallback(link, "reloadPage");
-
-      let isShowing = this.shouldShowOverlay(plugin, overlay);
-
-      // Is the <object>'s size too small to hold what we want to show?
-      if (!isShowing) {
-        // First try hiding the crash report submission UI.
-        statusDiv.removeAttribute("status");
-
-        isShowing = this.shouldShowOverlay(plugin, overlay);
-      }
-      this.setVisibility(plugin, overlay, isShowing);
-
-      return isShowing;
-    }
-  }
-};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -39,16 +39,15 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'wind
 
 if CONFIG['NIGHTLY_BUILD']:
     EXTRA_JS_MODULES += [
         'SignInToWebsite.jsm',
     ]
 
 EXTRA_PP_JS_MODULES += [
     'AboutHome.jsm',
-    'PluginContent.jsm',
     'RecentWindow.jsm',
     'UITour.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZILLA_OFFICIAL']:
     DEFINES['MOZILLA_OFFICIAL'] = 1
--- a/docshell/test/browser/browser.ini
+++ b/docshell/test/browser/browser.ini
@@ -97,9 +97,8 @@ skip-if = e10s
 [browser_loadURI.js]
 skip-if = e10s # Bug ?????? - event handler checks event.target is the content document and test e10s-utils doesn't do that.
 [browser_onbeforeunload_navigation.js]
 skip-if = e10s
 [browser_search_notification.js]
 skip-if = e10s
 [browser_timelineMarkers-01.js]
 [browser_timelineMarkers-02.js]
-skip-if = e10s # Bug 1064848
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -960,16 +960,20 @@ public abstract class GeckoApp
                 while((byteRead = is.read(buf)) != -1) {
                     os.write(buf, 0, byteRead);
                 }
                 byte[] imgBuffer = os.toByteArray();
                 image = BitmapUtils.decodeByteArray(imgBuffer);
             }
             if (image != null) {
                 String path = Media.insertImage(getContentResolver(),image, null, null);
+                if (path == null) {
+                    Toast.makeText((Context) this, R.string.set_image_path_fail, Toast.LENGTH_SHORT).show();
+                    return;
+                }
                 final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
                 intent.addCategory(Intent.CATEGORY_DEFAULT);
                 intent.setData(Uri.parse(path));
 
                 // Removes the image from storage once the chooser activity ends.
                 Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title));
                 ActivityResultHandler handler = new ActivityResultHandler() {
                     @Override
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -289,16 +289,17 @@ size. -->
 <!ENTITY new_tab "New Tab">
 <!ENTITY new_private_tab "New Private Tab">
 <!ENTITY close_all_tabs "Close All Tabs">
 <!ENTITY close_private_tabs "Close Private Tabs">
 <!ENTITY tabs_normal "Tabs">
 <!ENTITY tabs_private "Private">
 <!ENTITY tabs_synced "Synced">
 <!ENTITY set_image_fail "Unable to set image">
+<!ENTITY set_image_path_fail "Unable to save image">
 <!ENTITY set_image_chooser_title "Set Image As">
 
 <!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
 <!ENTITY find_text "Find in Page">
 <!ENTITY find_prev "Previous">
 <!ENTITY find_next "Next">
 <!ENTITY find_close "Close">
--- a/mobile/android/base/resources/values-land/styles.xml
+++ b/mobile/android/base/resources/values-land/styles.xml
@@ -67,13 +67,15 @@
 
     <style name="TabsPanelItem.TextAppearance.Linkified.Resend">
         <item name="android:layout_height">match_parent</item>
         <item name="android:gravity">center</item>
         <item name="android:layout_gravity">center</item>
     </style>
 
     <style name="Widget.Home.HistoryListView">
-        <item name="topDivider">true</item>
+        <item name="android:paddingLeft">50dp</item>
+        <item name="android:paddingRight">100dp</item>
+        <item name="android:paddingTop">0dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -19,17 +19,21 @@
     </style>
 
     <style name="Widget.BookmarkItemView" parent="Widget.TwoLinePageRow">
         <item name="android:paddingLeft">50dp</item>
         <item name="android:paddingRight">50dp</item>
     </style>
 
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
+        <item name="android:paddingTop">30dp</item>
+        <item name="android:paddingLeft">120dp</item>
+        <item name="android:paddingRight">120dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
+        <item name="topDivider">true</item>
     </style>
 
     <style name="Widget.TopSitesGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">55dp</item>
         <item name="android:paddingRight">55dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">20dp</item>
         <item name="android:verticalSpacing">20dp</item>
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -57,31 +57,32 @@
         <item name="android:layout_width">@dimen/browser_toolbar_height</item>
         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
         <item name="android:padding">@dimen/browser_toolbar_button_padding</item>
         <item name="android:background">@drawable/action_bar_button</item>
         <item name="android:scaleType">fitCenter</item>
     </style>
 
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
+        <item name="android:paddingTop">30dp</item>
+        <item name="android:paddingLeft">32dp</item>
+        <item name="android:paddingRight">32dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
+        <item name="topDivider">true</item>
     </style>
 
     <style name="Widget.TopSitesGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">5dp</item>
         <item name="android:paddingRight">5dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">10dp</item>
         <item name="android:verticalSpacing">10dp</item>
     </style>
 
     <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
-        <item name="android:paddingTop">30dp</item>
-        <item name="android:paddingLeft">32dp</item>
-        <item name="android:paddingRight">32dp</item>
         <item name="topDivider">false</item>
     </style>
 
     <style name="Widget.ReadingListView" parent="Widget.BookmarksListView"/>
 
     <style name="Widget.HomeBanner">
         <item name="android:paddingLeft">32dp</item>
         <item name="android:paddingRight">32dp</item>
--- a/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
@@ -1,30 +1,27 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <resources>
-    <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
-        <item name="android:paddingTop">30dp</item>
-        <item name="android:paddingLeft">120dp</item>
-        <item name="android:paddingRight">120dp</item>
-        <item name="topDivider">false</item>
-    </style>
 
     <style name="Widget.TopSitesGridView" parent="Widget.GridView">
         <item name="android:paddingLeft">55dp</item>
         <item name="android:paddingRight">55dp</item>
         <item name="android:paddingBottom">30dp</item>
         <item name="android:horizontalSpacing">56dp</item>
         <item name="android:verticalSpacing">20dp</item>
     </style>
 
     <style name="Widget.Home.HistoryListView">
+        <item name="android:paddingLeft">50dp</item>
+        <item name="android:paddingRight">100dp</item>
+        <item name="android:paddingTop">30dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
     <!-- Tabs panel -->
     <style name="TabsPanelFrame.RemoteTabs" parent="TabsPanelFrameBase">
         <item name="android:paddingLeft">0dp</item>
         <item name="android:paddingRight">0dp</item>
--- a/mobile/android/base/resources/values-xlarge-v11/styles.xml
+++ b/mobile/android/base/resources/values-xlarge-v11/styles.xml
@@ -12,26 +12,22 @@
 
     <!-- TabWidget --> 
     <style name="TabWidget">
         <item name="android:layout_width">300dip</item>
         <item name="android:layout_height">48dip</item>
     </style>
 
     <style name="Widget.Home.HistoryListView">
+        <item name="android:paddingLeft">32dp</item>
+        <item name="android:paddingRight">32dp</item>
+        <item name="android:paddingTop">30dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
         <item name="topDivider">true</item>
     </style>
 
     <!-- Tabs panel -->
     <style name="TabsPanelFrame.RemoteTabs" parent="TabsPanelFrameBase">
         <item name="android:paddingLeft">212dp</item>
         <item name="android:paddingRight">212dp</item>
     </style>
 
-    <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
-        <item name="android:paddingTop">30dp</item>
-        <item name="android:paddingLeft">32dp</item>
-        <item name="android:paddingRight">32dp</item>
-        <item name="topDivider">false</item>
-    </style>
-
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -58,20 +58,18 @@
     <style name="Widget.GridView" parent="Widget.BaseGridView">
         <item name="android:verticalSpacing">0dip</item>
         <item name="android:horizontalSpacing">0dip</item>
         <item name="android:cacheColorHint">@android:color/transparent</item>
         <item name="android:listSelector">@drawable/action_bar_button</item>
     </style>
 
     <style name="Widget.Home.HistoryListView">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingRight">0dip</item>
-        <item name="android:paddingLeft">0dip</item>
-        <item name="topDivider">true</item>
+        <item name="android:paddingLeft">32dp</item>
+        <item name="android:paddingRight">32dp</item>
         <item name="android:scrollbarStyle">outsideOverlay</item>
     </style>
 
     <style name="Widget.ListItem">
         <item name="android:minHeight">?android:attr/listPreferredItemHeight</item>
         <item name="android:textAppearance">?android:attr/textAppearanceLargeInverse</item>
         <item name="android:gravity">center_vertical</item>
         <item name="android:paddingLeft">12dip</item>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -431,16 +431,17 @@
 
   <!-- Search suggestions opt-in -->
   <string name="suggestions_prompt">&suggestions_prompt3;</string>
 
   <string name="suggestion_for_engine">&suggestion_for_engine;</string>
 
   <!-- Set Image Notifications -->
   <string name="set_image_fail">&set_image_fail;</string>
+  <string name="set_image_path_fail">&set_image_path_fail;</string>
   <string name="set_image_chooser_title">&set_image_chooser_title;</string>
 
   <!-- Contacts API -->
   <string name="contacts_account_chooser_dialog_title2">Share contacts from…</string>
 
 
   <!-- Guest mode -->
   <string name="new_guest_session">&new_guest_session;</string>
--- a/mobile/android/base/tests/roboextender/testSelectionHandler.html
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -4,31 +4,33 @@
     <meta name="viewport" content="initial-scale=1.0"/>
     <script type="application/javascript"
       src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
     <script type="application/javascript">
 
 const DIV_POINT_TEXT = "Under";
 const INPUT_TEXT = "Text for select all in an <input>";
 const TEXTAREA_TEXT = "Text for select all in a <textarea>";
+const READONLY_INPUT_TEXT = "readOnly text";
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 /* =================================================================================
  *
  * Start of all text selection tests, check initialization state.
  *
  */
 function startTests() {
   testSelectAllDivs().
     then(testSelectDivAtPoint).
     then(testSelectInput).
     then(testSelectTextarea).
+    then(testReadonlyInput).
     then(testCloseSelection).
     then(finishTests, function(err) {
       ok(false, "Error in selection test " + err);
       finishTests();
     });
 }
 
 /* =================================================================================
@@ -152,16 +154,37 @@ function testSelectTextarea() {
     return Promise.all([
       ok(!sh.isSelectionActive(), "Selection should not be active"),
     ]);
   });
 }
 
 /* =================================================================================
  *
+ * Ensure that readonly inputs aren't editable, and not subject to
+ * SelectionHandler actions such as "cut".
+ *
+ */
+function testReadonlyInput() {
+  var sh = getSelectionHandler();
+  var readOnlyNode = document.getElementById("readOnlyTextInput");
+  readOnlyNode.value = READONLY_INPUT_TEXT;
+
+  return Promise.all([
+    ok(!sh.isSelectionActive(), "Selection should not be active at start of testReadonlyInput"),
+
+  ]).then(function() {
+    return Promise.all([
+      ok(!sh.isElementEditableText(readOnlyNode), "Selected element should be readOnly (not editable)"),
+    ]);
+  });
+}
+
+/* =================================================================================
+ *
  * Various text selection tests to end active selections, including:
  *   1.) Clicking outside the selection.
  *   2.) SelectionEnd or Tab:Selected messages from java.
  *
  */
 function testCloseSelection() {
   var sh = getSelectionHandler();
   var inputNode = document.getElementById("inputNode");
@@ -282,10 +305,12 @@ function is(one, two, msg) {
       ornare lorem. Pellentesque nec dictum ante. Nam quis ligula ultricies, auctor
       nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus
       ligula interdum enim, vel varius libero sem ut ligula.</div><br>
 
     <input id="inputNode" type="text"><br>
 
     <textarea id="textareaNode"></textarea><br>
 
+    <input id="readOnlyTextInput" type="text" readonly><br>
+
   </body>
 </html>
--- a/mobile/android/search/java/org/mozilla/search/MainActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/MainActivity.java
@@ -4,41 +4,44 @@
 
 package org.mozilla.search;
 
 import android.content.AsyncQueryHandler;
 import android.content.ContentValues;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.widget.TextView;
 
 import com.nineoldandroids.animation.Animator;
 import com.nineoldandroids.animation.AnimatorSet;
 import com.nineoldandroids.animation.ObjectAnimator;
 
 import org.mozilla.gecko.LocaleAware;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.search.autocomplete.ClearableEditText;
 import org.mozilla.search.autocomplete.SuggestionsFragment;
+import org.mozilla.search.providers.SearchEngine;
+import org.mozilla.search.providers.SearchEngineManager;
+import org.mozilla.search.providers.SearchEngineManager.SearchEngineCallback;
 
 /**
  * The main entrance for the Android search intent.
  * <p/>
  * State management is delegated to child fragments. Fragments communicate
  * with each other by passing messages through this activity.
  */
-public class MainActivity extends LocaleAware.LocaleAwareFragmentActivity implements AcceptsSearchQuery {
+public class MainActivity extends LocaleAware.LocaleAwareFragmentActivity
+        implements AcceptsSearchQuery, SearchEngineCallback {
 
     private static final String KEY_SEARCH_STATE = "search_state";
     private static final String KEY_EDIT_STATE = "edit_state";
     private static final String KEY_QUERY = "query";
 
     static enum SearchState {
         PRESEARCH,
         POSTSEARCH
@@ -48,27 +51,34 @@ public class MainActivity extends Locale
         WAITING,
         EDITING
     }
 
     // Default states when activity is created.
     private SearchState searchState = SearchState.PRESEARCH;
     private EditState editState = EditState.WAITING;
 
+    private SearchEngineManager searchEngineManager;
+
+    // Only accessed on the main thread.
+    private SearchEngine engine;
+
+    private SuggestionsFragment suggestionsFragment;
+    private PostSearchFragment postSearchFragment;
+
     private AsyncQueryHandler queryHandler;
 
     // Main views in layout.
     private ClearableEditText editText;
     private View preSearch;
     private View postSearch;
 
     private View settingsButton;
 
     private View suggestions;
-    private SuggestionsFragment suggestionsFragment;
 
     private static final int SUGGESTION_TRANSITION_DURATION = 300;
     private static final Interpolator SUGGESTION_TRANSITION_INTERPOLATOR =
             new AccelerateDecelerateInterpolator();
 
     // Views used for suggestion animation.
     private TextView animationText;
     private View animationCard;
@@ -80,16 +90,25 @@ public class MainActivity extends Locale
     // Vertical translation of suggestion animation text to align with the search bar.
     private int textEndY;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.search_activity_main);
 
+        suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);
+        postSearchFragment = (PostSearchFragment)  getSupportFragmentManager().findFragmentById(R.id.postsearch);
+
+        searchEngineManager = new SearchEngineManager(this);
+        searchEngineManager.setChangeCallback(this);
+
+        // Initialize the fragments with the selected search engine.
+        searchEngineManager.getEngine(this);
+
         queryHandler = new AsyncQueryHandler(getContentResolver()) {};
 
         editText = (ClearableEditText) findViewById(R.id.search_edit_text);
         editText.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 setEditState(EditState.EDITING);
             }
@@ -128,17 +147,16 @@ public class MainActivity extends Locale
         settingsButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 startActivity(new Intent(MainActivity.this, SearchPreferenceActivity.class));
             }
         });
 
         suggestions = findViewById(R.id.suggestions);
-        suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);
 
         animationText = (TextView) findViewById(R.id.animation_text);
         animationCard = findViewById(R.id.animation_card);
 
         cardPaddingX = getResources().getDimensionPixelSize(R.dimen.card_background_padding_x);
         cardPaddingY = getResources().getDimensionPixelSize(R.dimen.card_background_padding_y);
         textEndY = getResources().getDimensionPixelSize(R.dimen.animation_text_translation_y);
 
@@ -146,35 +164,38 @@ public class MainActivity extends Locale
             setSearchState(SearchState.valueOf(savedInstanceState.getString(KEY_SEARCH_STATE)));
             setEditState(EditState.valueOf(savedInstanceState.getString(KEY_EDIT_STATE)));
 
             final String query = savedInstanceState.getString(KEY_QUERY);
             editText.setText(query);
 
             // If we're in the postsearch state, we need to re-do the query.
             if (searchState == SearchState.POSTSEARCH) {
-                ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch))
-                        .startSearch(query);
+                startSearch(query);
             }
         } else {
             // If there isn't a state to restore, the activity will start in the presearch state,
             // and we should enter editing mode to bring up the keyboard.
             setEditState(EditState.EDITING);
         }
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        searchEngineManager.destroy();
+        searchEngineManager = null;
+        engine = null;
+        suggestionsFragment = null;
+        postSearchFragment = null;
         queryHandler = null;
         editText = null;
         preSearch = null;
         postSearch = null;
         settingsButton = null;
-        suggestionsFragment = null;
         suggestions = null;
         animationText = null;
         animationCard = null;
     }
 
     @Override
     protected void onStart() {
         super.onStart();
@@ -216,29 +237,57 @@ public class MainActivity extends Locale
     public void onSearch(String query) {
         onSearch(query, null);
     }
 
     @Override
     public void onSearch(String query, SuggestionAnimation suggestionAnimation) {
         storeQuery(query);
 
-        ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch))
-                .startSearch(query);
+        startSearch(query);
 
         if (suggestionAnimation != null) {
             // Animate the suggestion card if start bounds are specified.
             animateSuggestion(query, suggestionAnimation);
         } else {
             // Otherwise immediately switch to the results view.
             setEditState(EditState.WAITING);
             setSearchState(SearchState.POSTSEARCH);
         }
     }
 
+    private void startSearch(final String query) {
+        if (engine != null) {
+            postSearchFragment.startSearch(engine, query);
+            return;
+        }
+
+        // engine will only be null if startSearch is called before the getEngine
+        // call in onCreate is completed.
+        searchEngineManager.getEngine(new SearchEngineCallback() {
+            @Override
+            public void execute(SearchEngine engine) {
+                postSearchFragment.startSearch(engine, query);
+            }
+        });
+    }
+
+    /**
+     * This method is called when we fetch the current engine in onCreate,
+     * as well as whenever the current engine changes. This method will only
+     * ever be called on the main thread.
+     *
+     * @param engine The current search engine.
+     */
+    @Override
+    public void execute(SearchEngine engine) {
+        this.engine = engine;
+        suggestionsFragment.setEngine(engine);
+    }
+
     /**
      * Animates search suggestion to search bar. This animation has 2 main parts:
      *
      * 1) Vertically translate query text from suggestion card to search bar.
      * 2) Expand suggestion card to fill the results view area.
      *
      * @param query
      * @param suggestionAnimation
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -1,15 +1,14 @@
 /* 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 android.app.Activity;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.support.v4.app.Fragment;
 import android.text.TextUtils;
 import android.util.Log;
@@ -23,25 +22,24 @@ import android.webkit.WebViewClient;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.search.providers.SearchEngine;
-import org.mozilla.search.providers.SearchEngineManager;
 
 public class PostSearchFragment extends Fragment {
 
     private static final String LOG_TAG = "PostSearchFragment";
 
-    private ProgressBar progressBar;
+    private SearchEngine engine;
 
-    private SearchEngineManager searchEngineManager;
+    private ProgressBar progressBar;
     private WebView webview;
     private View errorView;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false);
 
@@ -61,80 +59,59 @@ public class PostSearchFragment extends 
     public void onDestroyView() {
         super.onDestroyView();
         webview.setWebChromeClient(null);
         webview.setWebViewClient(null);
         webview = null;
         progressBar = null;
     }
 
-    @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        searchEngineManager = new SearchEngineManager(activity);
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-        searchEngineManager.destroy();
-        searchEngineManager = null;
-    }
+    public void startSearch(SearchEngine engine, String query) {
+        this.engine = engine;
 
-    public void startSearch(final String query) {
-        searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
-            @Override
-            public void execute(SearchEngine engine) {
-                final String url = engine.resultsUriForQuery(query);
-
-                // Load about:blank to avoid flashing old results.
-                webview.loadUrl(Constants.ABOUT_BLANK);
-                webview.loadUrl(url);
-            }
-        });
+        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)) {
+            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
      * as an intent. It also handles network errors.
      */
     private class ResultsWebViewClient extends WebViewClient {
 
         // Whether or not there is a network error.
         private boolean networkError;
 
         @Override
         public void onPageStarted(WebView view, final String url, Bitmap favicon) {
             // Reset the error state.
             networkError = false;
 
-            searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
-                @Override
-                public void execute(SearchEngine engine) {
-                    // We keep URLs in the webview that are either about:blank or a search engine result page.
-                    if (TextUtils.equals(url, Constants.ABOUT_BLANK) || engine.isSearchResultsPage(url)) {
-                        // Keeping the URL in the webview is a noop.
-                        return;
-                    }
+            // We keep URLs in the webview that are either about:blank or a search engine result page.
+            if (TextUtils.equals(url, Constants.ABOUT_BLANK) || engine.isSearchResultsPage(url)) {
+                // Keeping the URL in the webview is a noop.
+                return;
+            }
 
-                    webview.stopLoading();
+            webview.stopLoading();
 
-                    Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
-                            TelemetryContract.Method.CONTENT, "search-result");
+            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
+                    TelemetryContract.Method.CONTENT, "search-result");
 
-                    final Intent i = new Intent(Intent.ACTION_VIEW);
+            final Intent i = new Intent(Intent.ACTION_VIEW);
 
-                    // This sends the URL directly to fennec, rather than to Android.
-                    i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
-                    i.setData(Uri.parse(url));
-                    startActivity(i);
-                }
-            });
+            // This sends the URL directly to fennec, rather than to Android.
+            i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
+            i.setData(Uri.parse(url));
+            startActivity(i);
         }
 
         @Override
         public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
             Log.e(LOG_TAG, "Error loading search results: " + description);
 
             networkError = true;
 
@@ -174,23 +151,17 @@ public class PostSearchFragment extends 
      * We use the WebChromeClient because it provides a hook to the titleReceived
      * event. Once the title is available, the page will have started parsing the
      * head element. The script injects its CSS into the head element.
      */
     private class ChromeClient extends WebChromeClient {
 
         @Override
         public void onReceivedTitle(final WebView view, String title) {
-
-            searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
-                @Override
-                public void execute(SearchEngine engine) {
-                    view.loadUrl(engine.getInjectableJs());
-                }
-            });
+            view.loadUrl(engine.getInjectableJs());
         }
 
         @Override
         public void onProgressChanged(WebView view, int newProgress) {
             if (newProgress < 100) {
                 if (progressBar.getVisibility() == View.INVISIBLE) {
                     progressBar.setVisibility(View.VISIBLE);
                 }
--- a/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
@@ -24,43 +24,40 @@ import android.widget.ListView;
 import org.mozilla.gecko.SuggestClient;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.search.AcceptsSearchQuery;
 import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
 import org.mozilla.search.Constants;
 import org.mozilla.search.R;
 import org.mozilla.search.providers.SearchEngine;
-import org.mozilla.search.providers.SearchEngineManager;
 
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  * A fragment to show search suggestions.
  */
-public class SuggestionsFragment extends Fragment implements SearchEngineManager.SearchEngineCallback {
+public class SuggestionsFragment extends Fragment {
 
     private static final String LOG_TAG = "SuggestionsFragment";
 
     private static final int LOADER_ID_SUGGESTION = 0;
     private static final String KEY_SEARCH_TERM = "search_term";
 
     // Timeout for the suggestion client to respond
     private static final int SUGGESTION_TIMEOUT = 3000;
 
     // Color of search term match in search suggestion
     private static final int SUGGESTION_HIGHLIGHT_COLOR = 0xFF999999;
 
     public static final String GECKO_SEARCH_TERMS_URL_PARAM = "__searchTerms__";
 
     private AcceptsSearchQuery searchListener;
 
-    private SearchEngineManager searchEngineManager;
-
     // Suggest client gets setup outside of the normal fragment lifecycle, therefore
     // clients should ensure that this isn't null before using it.
     private SuggestClient suggestClient;
     private SuggestionLoaderCallbacks suggestionLoaderCallbacks;
 
     private AutoCompleteAdapter autoCompleteAdapter;
 
     // Holds the list of search suggestions.
@@ -77,39 +74,25 @@ public class SuggestionsFragment extends
         if (activity instanceof AcceptsSearchQuery) {
             searchListener = (AcceptsSearchQuery) activity;
         } else {
             throw new ClassCastException(activity.toString() + " must implement AcceptsSearchQuery.");
         }
 
         suggestionLoaderCallbacks = new SuggestionLoaderCallbacks();
         autoCompleteAdapter = new AutoCompleteAdapter(activity);
-        searchEngineManager = new SearchEngineManager(activity);
-        searchEngineManager.setChangeCallback(this);
-
-        // Initialize the suggest client. This may happen asynchronously, so any clients should
-        // still perform a null check.
-        searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
-            @Override
-            public void execute(SearchEngine engine) {
-                suggestClient = new SuggestClient(getActivity(), engine.getSuggestionTemplate(GECKO_SEARCH_TERMS_URL_PARAM),
-                        SUGGESTION_TIMEOUT, Constants.SUGGESTION_MAX);
-            }
-        });
     }
 
     @Override
     public void onDetach() {
         super.onDetach();
 
         searchListener = null;
         suggestionLoaderCallbacks = null;
         autoCompleteAdapter = null;
-        searchEngineManager.destroy();
-        searchEngineManager = null;
         suggestClient = null;
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         suggestionsList = (ListView) inflater.inflate(R.layout.search_sugestions, container, false);
         suggestionsList.setAdapter(autoCompleteAdapter);
@@ -144,18 +127,17 @@ public class SuggestionsFragment extends
 
         if (null != suggestionsList) {
             suggestionsList.setOnItemClickListener(null);
             suggestionsList.setAdapter(null);
             suggestionsList = null;
         }
     }
 
-    @Override
-    public void execute(SearchEngine engine) {
+    public void setEngine(SearchEngine engine) {
         suggestClient = new SuggestClient(getActivity(), engine.getSuggestionTemplate(GECKO_SEARCH_TERMS_URL_PARAM),
                 SUGGESTION_TIMEOUT, Constants.SUGGESTION_MAX);
     }
 
     public void loadSuggestions(String query) {
         final Bundle args = new Bundle();
         args.putString(KEY_SEARCH_TERM, query);
         final LoaderManager loaderManager = getLoaderManager();
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -132,17 +132,18 @@ DataReportingService.prototype = Object.
                          CommonUtils.exceptionStr(ex));
         }
         break;
 
       case "sessionstore-windows-restored":
         this._os.removeObserver(this, "sessionstore-windows-restored");
         this._os.addObserver(this, "quit-application", false);
 
-        this.policy.startPolling();
+        let policy = this.policy;
+        policy.startPolling();
 
         // Don't initialize Firefox Health Reporter collection and submission
         // service unless it is enabled.
         if (!this._prefs.get("service.enabled", true)) {
           return;
         }
 
         let haveFirstRun = this._prefs.get("service.firstRun", false);
@@ -170,17 +171,17 @@ DataReportingService.prototype = Object.
             }
 
             // Side effect: instantiates the reporter instance if not already
             // accessed.
             //
             // The instance installs its own shutdown observers. So, we just
             // fire and forget: it will clean itself up.
             let reporter = this.healthReporter;
-            this.policy.ensureUserNotified();
+            policy.ensureUserNotified();
           }.bind(this),
         }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
         break;
 
       case "quit-application":
         this._os.removeObserver(this, "quit-application");
         this._quitting = true;
--- a/toolkit/devtools/apps/app-actor-front.js
+++ b/toolkit/devtools/apps/app-actor-front.js
@@ -46,17 +46,17 @@ function addDirToZip(writer, dir, basePa
     }
   }
 }
 
 /**
  * Convert an XPConnect result code to its name and message.
  * We have to extract them from an exception per bug 637307 comment 5.
  */
-function getResultTest(code) {
+function getResultText(code) {
   let regexp =
     /^\[Exception... "(.*)"  nsresult: "0x[0-9a-fA-F]* \((.*)\)"  location: ".*"  data: .*\]$/;
   let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
            createInstance(Ci.nsIXPCException);
   ex.initialize(null, code, null, null, null, null);
   let [, message, name] = regexp.exec(ex.toString());
   return { name: name, message: message };
 }
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -5072,24 +5072,27 @@ ThreadSources.prototype = {
    * source mapped, otherwise a promise of the same location.
    */
   getOriginalLocation: function ({ url, line, column }) {
     if (url in this._sourceMapsByGeneratedSource) {
       column = column || 0;
 
       return this._sourceMapsByGeneratedSource[url]
         .then((aSourceMap) => {
-          let { source: aSourceURL, line: aLine, column: aColumn } = aSourceMap.originalPositionFor({
-            line: line,
-            column: column
-          });
+          let {
+            source: aSourceURL,
+            line: aLine,
+            column: aColumn,
+            name: aName
+          } = aSourceMap.originalPositionFor({ line, column });
           return {
             url: aSourceURL,
             line: aLine,
-            column: aColumn
+            column: aColumn,
+            name: aName
           };
         })
         .then(null, error => {
           if (!DevToolsUtils.reportingDisabled) {
             DevToolsUtils.reportException("ThreadSources.prototype.getOriginalLocation", error);
           }
           return { url: null, line: null, column: null };
         });
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -4,16 +4,19 @@
 
 "use strict";
 
 const { Cu } = require("chrome");
 const { DebuggerServer } = require("devtools/server/main");
 const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 const Debugger = require("Debugger");
 const { getOffsetColumn } = require("devtools/server/actors/common");
+const promise = require("promise");
+
+Cu.import("resource://gre/modules/Task.jsm");
 
 // TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth
 // once it is implemented.
 if (!Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "depth")) {
   Debugger.Frame.prototype._depth = null;
   Object.defineProperty(Debugger.Frame.prototype, "depth", {
     get: function () {
       if (this._depth === null) {
@@ -79,16 +82,17 @@ function TracerActor(aConn, aParent)
   this._attached = false;
   this._activeTraces = new MapStack();
   this._totalTraces = 0;
   this._startTime = 0;
   this._sequence = 0;
   this._bufferSendTimer = null;
   this._buffer = [];
   this._hitCounts = new WeakMap();
+  this._packetScheduler = new JobScheduler();
 
   // Keep track of how many different trace requests have requested what kind of
   // tracing info. This way we can minimize the amount of data we are collecting
   // at any given time.
   this._requestsForTraceType = Object.create(null);
   for (let type of TRACE_TYPES) {
     this._requestsForTraceType[type] = 0;
   }
@@ -260,111 +264,133 @@ TracerActor.prototype = {
   /**
    * Called by the engine when a frame is entered. Sends an unsolicited packet
    * to the client carrying requested trace information.
    *
    * @param aFrame Debugger.Frame
    *        The stack frame that was entered.
    */
   onEnterFrame: function(aFrame) {
-    if (aFrame.script && aFrame.script.url == "self-hosted") {
-      return;
-    }
+    Task.spawn(function*() {
+      if (aFrame.script && aFrame.script.url == "self-hosted") {
+        return;
+      }
 
-    let packet = {
-      type: "enteredFrame",
-      sequence: this._sequence++
-    };
+      // This function might request original (i.e. source-mapped) location,
+      // which is asynchronous. We need to ensure that packets are sent out
+      // in the correct order.
+      let runInOrder = this._packetScheduler.schedule();
+
+      let packet = {
+        type: "enteredFrame",
+        sequence: this._sequence++
+      };
 
-    if (this._requestsForTraceType.name) {
-      packet.name = aFrame.callee
-        ? aFrame.callee.displayName || "(anonymous function)"
-        : "(" + aFrame.type + ")";
-    }
+      let sourceMappedLocation;
+      if (this._requestsForTraceType.name || this._requestsForTraceType.location) {
+        if (aFrame.script) {
+          sourceMappedLocation = yield this._parent.threadActor.sources.getOriginalLocation({
+            url: aFrame.script.url,
+            line: aFrame.script.startLine,
+            // We should return the location of the start of the script, but
+            // Debugger.Script does not provide complete start locations (bug
+            // 901138). Instead, return the current offset (the location of the
+            // first statement in the function).
+            column: getOffsetColumn(aFrame.offset, aFrame.script)
+          });
+        }
+      }
 
-    if (aFrame.script) {
-      if (this._requestsForTraceType.hitCount) {
-        // Increment hit count.
-        let previousHitCount = this._hitCounts.get(aFrame.script) || 0;
-        this._hitCounts.set(aFrame.script, previousHitCount + 1);
-
-        packet.hitCount = this._hitCounts.get(aFrame.script);
+      if (this._requestsForTraceType.name) {
+        if (sourceMappedLocation && sourceMappedLocation.name) {
+          packet.name = sourceMappedLocation.name;
+        } else {
+          packet.name = aFrame.callee
+            ? aFrame.callee.displayName || "(anonymous function)"
+            : "(" + aFrame.type + ")";
+        }
       }
 
       if (this._requestsForTraceType.location) {
-        // We should return the location of the start of the script, but
-        // Debugger.Script does not provide complete start locations (bug
-        // 901138). Instead, return the current offset (the location of the first
-        // statement in the function).
-        packet.location = {
-          url: aFrame.script.url,
-          line: aFrame.script.startLine,
-          column: getOffsetColumn(aFrame.offset, aFrame.script)
-        };
+        if (sourceMappedLocation && sourceMappedLocation.url) {
+          packet.location = sourceMappedLocation;
+        }
       }
-    }
 
-    if (this._parent.threadActor && aFrame.script) {
-      packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
-    } else {
-      packet.blackBoxed = false;
-    }
+      if (this._requestsForTraceType.hitCount) {
+        if (aFrame.script) {
+          // Increment hit count.
+          let previousHitCount = this._hitCounts.get(aFrame.script) || 0;
+          this._hitCounts.set(aFrame.script, previousHitCount + 1);
 
-    if (this._requestsForTraceType.callsite
-        && aFrame.older
-        && aFrame.older.script) {
-      let older = aFrame.older;
-      packet.callsite = {
-        url: older.script.url,
-        line: older.script.getOffsetLine(older.offset),
-        column: getOffsetColumn(older.offset, older.script)
-      };
-    }
+          packet.hitCount = this._hitCounts.get(aFrame.script);
+        }
+      }
+
+      if (this._parent.threadActor && aFrame.script) {
+        packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
+      } else {
+        packet.blackBoxed = false;
+      }
 
-    if (this._requestsForTraceType.time) {
-      packet.time = Date.now() - this._startTime;
-    }
-
-    if (this._requestsForTraceType.parameterNames && aFrame.callee) {
-      packet.parameterNames = aFrame.callee.parameterNames;
-    }
-
-    if (this._requestsForTraceType.arguments && aFrame.arguments) {
-      packet.arguments = [];
-      let i = 0;
-      for (let arg of aFrame.arguments) {
-        if (i++ > MAX_ARGUMENTS) {
-          break;
+      if (this._requestsForTraceType.callsite) {
+        if (aFrame.older && aFrame.older.script) {
+          let older = aFrame.older;
+          packet.callsite = {
+            url: older.script.url,
+            line: older.script.getOffsetLine(older.offset),
+            column: getOffsetColumn(older.offset, older.script)
+          };
         }
-        packet.arguments.push(createValueSnapshot(arg, true));
+      }
+
+      if (this._requestsForTraceType.time) {
+        packet.time = Date.now() - this._startTime;
       }
-    }
+
+      if (this._requestsForTraceType.parameterNames && aFrame.callee) {
+        packet.parameterNames = aFrame.callee.parameterNames;
+      }
 
-    if (this._requestsForTraceType.depth) {
-      packet.depth = aFrame.depth;
-    }
+      if (this._requestsForTraceType.arguments && aFrame.arguments) {
+        packet.arguments = [];
+        let i = 0;
+        for (let arg of aFrame.arguments) {
+          if (i++ > MAX_ARGUMENTS) {
+            break;
+          }
+          packet.arguments.push(createValueSnapshot(arg, true));
+        }
+      }
 
-    const onExitFrame = this.onExitFrame;
-    aFrame.onPop = function (aCompletion) {
-      onExitFrame(this, aCompletion);
-    };
+      if (this._requestsForTraceType.depth) {
+        packet.depth = aFrame.depth;
+      }
 
-    this._send(packet);
+      const onExitFrame = this.onExitFrame;
+      aFrame.onPop = function (aCompletion) {
+        onExitFrame(this, aCompletion);
+      };
+
+      runInOrder(() => this._send(packet));
+    }.bind(this));
   },
 
   /**
    * Called by the engine when a frame is exited. Sends an unsolicited packet to
    * the client carrying requested trace information.
    *
    * @param Debugger.Frame aFrame
    *        The Debugger.Frame that was just exited.
    * @param aCompletion object
    *        The debugger completion value for the frame.
    */
   onExitFrame: function(aFrame, aCompletion) {
+    let runInOrder = this._packetScheduler.schedule();
+
     let packet = {
       type: "exitedFrame",
       sequence: this._sequence++,
     };
 
     if (!aCompletion) {
       packet.why = "terminated";
     } else if (aCompletion.hasOwnProperty("return")) {
@@ -392,17 +418,17 @@ TracerActor.prototype = {
         packet.throw = createValueSnapshot(aCompletion.throw, true);
       }
 
       else if (this._requestsForTraceType.yield && "yield" in aCompletion) {
         packet.yield = createValueSnapshot(aCompletion.yield, true);
       }
     }
 
-    this._send(packet);
+    runInOrder(() => this._send(packet));
   }
 };
 
 /**
  * The request types this actor can handle.
  */
 TracerActor.prototype.requestTypes = {
   "attach": TracerActor.prototype.onAttach,
@@ -650,8 +676,39 @@ function propertySnapshot(aName, aObject
 
   return {
     configurable: desc.configurable,
     enumerable: desc.enumerable,
     writable: desc.writable,
     value: createValueSnapshot(desc.value)
   };
 }
+
+/**
+ * Scheduler for jobs to be run in the same order as in which they were
+ * scheduled.
+ */
+function JobScheduler()
+{
+  this._lastScheduledJob = promise.resolve();
+}
+
+JobScheduler.prototype = {
+  /**
+   * Schedule a new job.
+   *
+   * @return A function that can be called anytime with a job as a parameter.
+   *         Job won't be run until all previously scheduled jobs were run.
+   */
+  schedule: function() {
+    let deferred = promise.defer();
+    let previousJob = this._lastScheduledJob;
+    this._lastScheduledJob = deferred.promise;
+    return function runInOrder(aJob) {
+      previousJob.then(() => {
+        aJob();
+        deferred.resolve();
+      });
+    };
+  }
+};
+
+exports.JobScheduler = JobScheduler;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-14.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that tracer links to a correct (source-mapped) location.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test() {
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-source-map");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestThread(gClient, "test-source-map",
+      function(aResponse, aTabClient, aThreadClient, aTabResponse) {
+      gThreadClient = aThreadClient;
+      gThreadClient.resume(function (aResponse) {
+        gClient.attachTracer(aTabResponse.traceActor, function(aResponse, aTraceClient) {
+          gTraceClient = aTraceClient;
+          testSourceMaps();
+        });
+      });
+    });
+  });
+  do_test_pending();
+}
+
+const testSourceMaps = Task.async(function* () {
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      checkTrace(t);
+    }
+    tracesStopped.resolve();
+  });
+
+  yield startTrace();
+
+  yield executeOnNextTickAndWaitForPause(evalCode, gClient);
+  yield resume(gThreadClient);
+
+  yield tracesStopped.promise;
+  yield stopTrace();
+
+  finishClient(gClient);
+});
+
+function startTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["depth", "name", "location"], null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function evalCode()
+{
+  let { code, map } = (new SourceNode(null, null, null, [
+    new SourceNode(10, 0, "a.js", "function fnOuter() {\n"),
+    new SourceNode(20, 0, "a.js", "  fnInner();\n"),
+    new SourceNode(30, 0, "a.js", "}\n"),
+    new SourceNode(10, 0, "b.js", "function fnInner() {\n"),
+    new SourceNode(20, 0, "b.js", "  [1].forEach(function noop() {\n"),
+    new SourceNode(30, 0, "b.js", "    debugger;\n"),
+    new SourceNode(40, 0, "b.js", "  });\n"),
+    new SourceNode(50, 0, "b.js", "}\n"),
+    new SourceNode(60, 0, "b.js", "fnOuter();\n"),
+  ])).toStringWithSourceMap({
+    file: "abc.js",
+    sourceRoot: "http://example.com/"
+  });
+
+  code += "//# sourceMappingURL=data:text/json," + map.toString();
+
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8", "http://example.com/abc.js", 1);
+}
+
+
+function checkTrace({ type, sequence, depth, name, location, blackBoxed })
+{
+  switch(sequence) {
+  // First packet comes from evalInSandbox in evalCode.
+  case 0:
+    do_check_eq(name, "(global)");
+    break;
+
+  case 1:
+    do_check_eq(name, "fnOuter");
+    do_check_eq(location.url, "http://example.com/a.js");
+    do_check_eq(location.line, 10);
+    break;
+
+  case 2:
+    do_check_eq(name, "fnInner");
+    do_check_eq(location.url, "http://example.com/b.js");
+    do_check_eq(location.line, 10);
+    break;
+
+  case 3:
+    do_check_eq(name, "noop");
+    do_check_eq(location.url, "http://example.com/b.js");
+    do_check_eq(location.line, 20);
+    break;
+
+  case 4:
+  case 5:
+  case 6:
+  case 7:
+    do_check_eq(type, "exitedFrame");
+    break;
+
+  default:
+    // Should have covered all sequences.
+    do_check_true(false);
+  }
+}
+
+function stopTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-15.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Create 2 sources, A and B, B is source-mapped. When calling functions A->B->A,
+ * verify that traces are in a proper order.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestThread(gClient, "test-tracer-actor",
+      function(aResponse, aTabClient, aThreadClient, aTabResponse) {
+      gThreadClient = aThreadClient;
+      gThreadClient.resume(function (aResponse) {
+        gClient.attachTracer(aTabResponse.traceActor,
+          function(aResponse, aTraceClient) {
+          gTraceClient = aTraceClient;
+          testTraces();
+        });
+      });
+    });
+  });
+  do_test_pending();
+}
+
+const testTraces = Task.async(function* () {
+  // Read traces
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      check_trace(t);
+    }
+    tracesStopped.resolve();
+  });
+
+  yield startTrace();
+
+  yield executeOnNextTickAndWaitForPause(evalSourceMapSetup, gClient);
+  yield resume(gThreadClient);
+
+  evalSetup();
+
+  evalTestCode();
+
+  yield tracesStopped.promise;
+  yield stopTrace();
+
+  finishClient(gClient);
+});
+
+function startTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["depth", "name", "location"], null,
+    function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function evalSourceMapSetup() {
+  let { code, map } = (new SourceNode(null, null, null, [
+    new SourceNode(1, 0, "b_original.js", "" + function fnSourceMapped() {
+      fnInner();
+    } + "\n debugger;")
+  ])).toStringWithSourceMap({
+    file: "b.js",
+    sourceRoot: "http://example.com/"
+  });
+  code += "//# sourceMappingURL=data:text/json," + map.toString();
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8", "http://example.com/b.js");
+}
+
+function evalSetup()
+{
+  Components.utils.evalInSandbox(
+    "" + function fnOuter() {
+      fnSourceMapped();
+    } + "\n" +
+    "" + function fnInner() {
+      [1].forEach(function noop() {});
+    },
+    gDebuggee,
+    "1.8",
+    "http://example.com/a.js",
+    1
+  );
+}
+
+function evalTestCode()
+{
+  Components.utils.evalInSandbox(
+    "fnOuter();",
+    gDebuggee,
+    "1.8",
+    "http://example.com/a.js",
+    1
+  );
+}
+
+function stopTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function check_trace({ type, sequence, depth, name, location, blackBoxed })
+{
+  switch(sequence) {
+  // First two packets come from evalInSandbox in evalSetup
+  // The third packet comes from evalInSandbox in evalTestCode
+  case 0:
+  case 2:
+  case 4:
+    do_check_eq(name, "(global)");
+    do_check_eq(type, "enteredFrame");
+    break;
+
+  case 5:
+    do_check_eq(name, "fnOuter");
+    break;
+
+  case 6:
+    do_check_eq(name, "fnSourceMapped");
+    break;
+
+  case 7:
+    do_check_eq(name, "fnInner");
+    break;
+
+  case 8:
+    do_check_eq(name, "noop");
+    break;
+
+  case 1: // evalInSandbox
+  case 3: // evalInSandbox
+  case 9: // noop
+  case 10: // fnInner
+  case 11: // fnSourceMapped
+  case 12: // fnOuter
+  case 13: // evalInSandbox
+    do_check_eq(type, "exitedFrame");
+    break;
+
+  default:
+    // Should have covered all sequences.
+    do_check_true(false);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-13.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that job scheduler orders the jobs correctly.
+ */
+
+let { JobScheduler } = devtools.require("devtools/server/actors/tracer");
+
+function run_test()
+{
+  do_test_pending();
+
+  test_in_order();
+  test_shuffled();
+
+  do_timeout(0, do_test_finished);
+}
+
+function test_in_order() {
+  let jobScheduler = new JobScheduler();
+  let testArray = [0];
+
+  let first = jobScheduler.schedule();
+  let second = jobScheduler.schedule();
+  let third = jobScheduler.schedule();
+
+  first(() => testArray.push(1));
+  second(() => testArray.push(2));
+  third(() => testArray.push(3));
+
+  do_timeout(0, () => {
+    do_check_eq(testArray[0], 0);
+    do_check_eq(testArray[1], 1);
+    do_check_eq(testArray[2], 2);
+    do_check_eq(testArray[3], 3);
+  });
+}
+
+function test_shuffled() {
+  let jobScheduler = new JobScheduler();
+  let testArray = [0];
+
+  let first = jobScheduler.schedule();
+  let second = jobScheduler.schedule();
+  let third = jobScheduler.schedule();
+
+  third(() => testArray.push(3));
+  first(() => testArray.push(1));
+  second(() => testArray.push(2));
+
+  do_timeout(0, () => {
+    do_check_eq(testArray[0], 0);
+    do_check_eq(testArray[1], 1);
+    do_check_eq(testArray[2], 2);
+    do_check_eq(testArray[3], 3);
+  });
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -134,16 +134,18 @@ reason = bug 820380
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-08.js]
 [test_sourcemaps-09.js]
 [test_sourcemaps-10.js]
 [test_sourcemaps-11.js]
 [test_sourcemaps-12.js]
 [test_sourcemaps-13.js]
+[test_sourcemaps-14.js]
+[test_sourcemaps-15.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_objectgrips-08.js]
@@ -194,16 +196,17 @@ reason = bug 820380
 [test_trace_actor-05.js]
 [test_trace_actor-06.js]
 [test_trace_actor-07.js]
 [test_trace_actor-08.js]
 [test_trace_actor-09.js]
 [test_trace_actor-10.js]
 [test_trace_actor-11.js]
 [test_trace_actor-12.js]
+[test_trace_actor-13.js]
 [test_ignore_caught_exceptions.js]
 [test_requestTypes.js]
 reason = bug 937197
 [test_layout-reflows-observer.js]
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_monitor_actor.js]
--- a/toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js
@@ -70,31 +70,29 @@ function part3(aTestPlugin) {
 
   gBrowser.selectedTab = gBrowser.addTab();
   gPluginBrowser = gBrowser.selectedBrowser;
   gPluginBrowser.addEventListener("PluginBindingAttached", part4, true, true);
   gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
 }
 
 function part4() {
-  let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
-  waitForCondition(condition, () => {
-    gPluginBrowser.removeEventListener("PluginBindingAttached", part4);
-    gBrowser.removeCurrentTab();
+  ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part4: should have a click-to-play notification");
+  gPluginBrowser.removeEventListener("PluginBindingAttached", part4);
+  gBrowser.removeCurrentTab();
 
-    let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
-    let menu = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
-    let alwaysActivateItem = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "always-activate-menuitem");
-    menu.selectedItem = alwaysActivateItem;
-    alwaysActivateItem.doCommand();
-    gBrowser.selectedTab = gBrowser.addTab();
-    gPluginBrowser = gBrowser.selectedBrowser;
-    gPluginBrowser.addEventListener("load", part5, true);
-    gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
-  }, "part4: should have a click-to-play notification");
+  let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
+  let menu = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
+  let alwaysActivateItem = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "always-activate-menuitem");
+  menu.selectedItem = alwaysActivateItem;
+  alwaysActivateItem.doCommand();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gPluginBrowser = gBrowser.selectedBrowser;
+  gPluginBrowser.addEventListener("load", part5, true);
+  gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
 }
 
 function part5() {
   let testPlugin = gPluginBrowser.contentDocument.getElementById("test");
   ok(testPlugin, "part5: should have a plugin element in the page");
   let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
   let condition = function() objLoadingContent.activated;
   waitForCondition(condition, part6, "part5: waited too long for plugin to activate");
@@ -115,32 +113,30 @@ function part6() {
   neverActivateItem.doCommand();
   gBrowser.selectedTab = gBrowser.addTab();
   gPluginBrowser = gBrowser.selectedBrowser;
   gPluginBrowser.addEventListener("PluginBindingAttached", part7, true, true);
   gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
 }
 
 function part7() {
-  let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
-  waitForCondition(condition, () => {
-    let testPlugin = gPluginBrowser.contentDocument.getElementById("test");
-    ok(testPlugin, "part7: should have a plugin element in the page");
-    let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    ok(!objLoadingContent.activated, "part7: plugin should not be activated");
+  ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part7: disabled plugins still show a notification");
+  let testPlugin = gPluginBrowser.contentDocument.getElementById("test");
+  ok(testPlugin, "part7: should have a plugin element in the page");
+  let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  ok(!objLoadingContent.activated, "part7: plugin should not be activated");
 
-    gPluginBrowser.removeEventListener("PluginBindingAttached", part7);
-    gBrowser.removeCurrentTab();
+  gPluginBrowser.removeEventListener("PluginBindingAttached", part7);
+  gBrowser.removeCurrentTab();
 
-    let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
-    let details = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
-    is_element_visible(details, "part7: details link should be visible");
-    EventUtils.synthesizeMouseAtCenter(details, {}, gManagerWindow);
-    wait_for_view_load(gManagerWindow, part8);
-  }, "part7: disabled plugins still show a notification");
+  let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
+  let details = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
+  is_element_visible(details, "part7: details link should be visible");
+  EventUtils.synthesizeMouseAtCenter(details, {}, gManagerWindow);
+  wait_for_view_load(gManagerWindow, part8);
 }
 
 function part8() {
   let enableButton = gManagerWindow.document.getElementById("detail-enable-btn");
   is_element_hidden(enableButton, "part8: detail enable button should be hidden");
   let disableButton = gManagerWindow.document.getElementById("detail-disable-btn");
   is_element_hidden(disableButton, "part8: detail disable button should be hidden");
   let menu = gManagerWindow.document.getElementById("detail-state-menulist");
@@ -179,31 +175,29 @@ function part10() {
   askToActivateItem.doCommand();
   gBrowser.selectedTab = gBrowser.addTab();
   gPluginBrowser = gBrowser.selectedBrowser;
   gPluginBrowser.addEventListener("PluginBindingAttached", part11, true, true);
   gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
 }
 
 function part11() {
-  let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
-  waitForCondition(condition, () => {
-    gPluginBrowser.removeEventListener("PluginBindingAttached", part11);
-    gBrowser.removeCurrentTab();
+  ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part11: should have a click-to-play notification");
+  gPluginBrowser.removeEventListener("PluginBindingAttached", part11);
+  gBrowser.removeCurrentTab();
 
-    let pluginTag = getTestPluginTag();
+  let pluginTag = getTestPluginTag();
 
-  // causes appDisabled to be set
-    setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
-      function() {
-        close_manager(gManagerWindow, function() {
-        open_manager("addons://list/plugin", part12);
-      });
+// causes appDisabled to be set
+  setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
+    function() {
+      close_manager(gManagerWindow, function() {
+      open_manager("addons://list/plugin", part12);
     });
-  }, "part11: should have a click-to-play notification");
+  });
 }
 
 function part12(aWindow) {
   gManagerWindow = aWindow;
   let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
   pluginEl.parentNode.ensureElementIsVisible(pluginEl);
   let menu = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
   is(menu.disabled, true, "part12: state menu should be disabled");