Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 29 Mar 2016 14:28:13 +0200
changeset 290922 8898c7f8ea957a2a1527ab37a48b01b37a4bcb3e
parent 290921 296179ddbd84f1c893edb3a3b71f508b619a5f54 (current diff)
parent 290783 d5d53a3b4e50b94cdf85d20690526e5a00d5b63e (diff)
child 290923 e196794aa71a12b6d6a20f99cb5b8e0d95f035e2
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone48.0a1
Merge mozilla-central to mozilla-inbound
toolkit/components/telemetry/Histograms.json
--- a/browser/base/content/test/general/browser_bug596687.js
+++ b/browser/base/content/test/general/browser_bug596687.js
@@ -1,26 +1,25 @@
-function test() {
-  var tab = gBrowser.addTab(null, {skipAnimation: true});
-  gBrowser.selectedTab = tab;
+add_task(function* test() {
+  var tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
 
   var gotTabAttrModified = false;
   var gotTabClose = false;
 
   function onTabClose() {
     gotTabClose = true;
     tab.addEventListener("TabAttrModified", onTabAttrModified, false);
   }
 
   function onTabAttrModified() {
     gotTabAttrModified = true;
   }
 
   tab.addEventListener("TabClose", onTabClose, false);
 
-  gBrowser.removeTab(tab);
+  yield BrowserTestUtils.removeTab(tab);
 
   ok(gotTabClose, "should have got the TabClose event");
   ok(!gotTabAttrModified, "shouldn't have got the TabAttrModified event after TabClose");
 
   tab.removeEventListener("TabClose", onTabClose, false);
   tab.removeEventListener("TabAttrModified", onTabAttrModified, false);
-}
+});
--- a/browser/components/extensions/.eslintrc
+++ b/browser/components/extensions/.eslintrc
@@ -2,16 +2,17 @@
   "extends": "../../../toolkit/components/extensions/.eslintrc",
 
   "globals": {
     "AllWindowEvents": true,
     "currentWindow": true,
     "EventEmitter": true,
     "IconDetails": true,
     "makeWidgetId": true,
+    "pageActionFor": true,
     "PanelPopup": true,
     "TabContext": true,
     "ViewPopup": true,
     "WindowEventManager": true,
     "WindowListManager": true,
     "WindowManager": true,
   },
 }
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -12,20 +12,16 @@ var {
   EventManager,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> BrowserAction]
 var browserActionMap = new WeakMap();
 
-function browserActionOf(extension) {
-  return browserActionMap.get(extension);
-}
-
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension) {
   this.extension = extension;
 
   let widgetId = makeWidgetId(extension.id);
   this.id = `${widgetId}-browser-action`;
   this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
@@ -198,16 +194,22 @@ BrowserAction.prototype = {
   },
 
   shutdown() {
     this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
   },
 };
 
+BrowserAction.for = (extension) => {
+  return browserActionMap.get(extension);
+};
+
+global.browserActionFor = BrowserAction.for;
+
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
   let browserAction = new BrowserAction(manifest.browser_action, extension);
   browserAction.build();
   browserActionMap.set(extension, browserAction);
 });
 
 extensions.on("shutdown", (type, extension) => {
@@ -221,89 +223,89 @@ extensions.on("shutdown", (type, extensi
 extensions.registerSchemaAPI("browserAction", null, (extension, context) => {
   return {
     browserAction: {
       onClicked: new EventManager(context, "browserAction.onClicked", fire => {
         let listener = () => {
           let tab = TabManager.activeTab;
           fire(TabManager.convert(extension, tab));
         };
-        browserActionOf(extension).on("click", listener);
+        BrowserAction.for(extension).on("click", listener);
         return () => {
-          browserActionOf(extension).off("click", listener);
+          BrowserAction.for(extension).off("click", listener);
         };
       }).api(),
 
       enable: function(tabId) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : null;
-        browserActionOf(extension).setProperty(tab, "enabled", true);
+        BrowserAction.for(extension).setProperty(tab, "enabled", true);
       },
 
       disable: function(tabId) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : null;
-        browserActionOf(extension).setProperty(tab, "enabled", false);
+        BrowserAction.for(extension).setProperty(tab, "enabled", false);
       },
 
       setTitle: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
 
         let title = details.title;
         // Clear the tab-specific title when given a null string.
         if (tab && title == "") {
           title = null;
         }
-        browserActionOf(extension).setProperty(tab, "title", title);
+        BrowserAction.for(extension).setProperty(tab, "title", title);
       },
 
       getTitle: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let title = browserActionOf(extension).getProperty(tab, "title");
+        let title = BrowserAction.for(extension).getProperty(tab, "title");
         return Promise.resolve(title);
       },
 
       setIcon: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
         let icon = IconDetails.normalize(details, extension, context);
-        browserActionOf(extension).setProperty(tab, "icon", icon);
+        BrowserAction.for(extension).setProperty(tab, "icon", icon);
         return Promise.resolve();
       },
 
       setBadgeText: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "badgeText", details.text);
+        BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
       },
 
       getBadgeText: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let text = browserActionOf(extension).getProperty(tab, "badgeText");
+        let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
         return Promise.resolve(text);
       },
 
       setPopup: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
         // Note: Chrome resolves arguments to setIcon relative to the calling
         // context, but resolves arguments to setPopup relative to the extension
         // root.
         // For internal consistency, we currently resolve both relative to the
         // calling context.
         let url = details.popup && context.uri.resolve(details.popup);
-        browserActionOf(extension).setProperty(tab, "popup", url);
+        BrowserAction.for(extension).setProperty(tab, "popup", url);
       },
 
       getPopup: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let popup = browserActionOf(extension).getProperty(tab, "popup");
+        let popup = BrowserAction.for(extension).getProperty(tab, "popup");
         return Promise.resolve(popup);
       },
 
       setBadgeBackgroundColor: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
+        BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", details.color);
       },
 
       getBadgeBackgroundColor: function(details, callback) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
+        let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
         return Promise.resolve(color);
       },
     },
   };
 });
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -10,87 +10,97 @@ var {
   PlatformInfo,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> CommandList]
 var commandsMap = new WeakMap();
 
-function CommandList(commandsObj, extensionID) {
-  this.commands = this.loadCommandsFromManifest(commandsObj);
-  this.keysetID = `ext-keyset-id-${makeWidgetId(extensionID)}`;
+function CommandList(manifest, extension) {
+  this.extension = extension;
+  this.id = makeWidgetId(extension.id);
   this.windowOpenListener = null;
+
+  // Map[{String} commandName -> {Object} commandProperties]
+  this.commands = this.loadCommandsFromManifest(manifest);
+
+  // WeakMap[Window -> <xul:keyset>]
+  this.keysetsMap = new WeakMap();
+
   this.register();
   EventEmitter.decorate(this);
 }
 
 CommandList.prototype = {
   /**
    * Registers the commands to all open windows and to any which
    * are later created.
    */
   register() {
     for (let window of WindowListManager.browserWindows()) {
-      this.registerKeysToDocument(window.document);
+      this.registerKeysToDocument(window);
     }
 
     this.windowOpenListener = (window) => {
-      this.registerKeysToDocument(window.document);
+      if (!this.keysetsMap.has(window)) {
+        this.registerKeysToDocument(window);
+      }
     };
 
     WindowListManager.addOpenListener(this.windowOpenListener);
   },
 
   /**
    * Unregisters the commands from all open windows and stops commands
    * from being registered to windows which are later created.
    */
   unregister() {
     for (let window of WindowListManager.browserWindows()) {
-      let keyset = window.document.getElementById(this.keysetID);
-      if (keyset) {
-        keyset.remove();
+      if (this.keysetsMap.has(window)) {
+        this.keysetsMap.get(window).remove();
       }
     }
 
     WindowListManager.removeOpenListener(this.windowOpenListener);
   },
 
   /**
    * Creates a Map from commands for each command in the manifest.commands object.
-   * @param {Object} commandsObj The manifest.commands JSON object.
+   * @param {Object} manifest The manifest JSON object.
    */
-  loadCommandsFromManifest(commandsObj) {
+  loadCommandsFromManifest(manifest) {
     let commands = new Map();
     // For Windows, chrome.runtime expects 'win' while chrome.commands
     // expects 'windows'.  We can special case this for now.
     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
-    for (let name of Object.keys(commandsObj)) {
-      let command = commandsObj[name];
+    for (let name of Object.keys(manifest.commands)) {
+      let command = manifest.commands[name];
       commands.set(name, {
         description: command.description,
         shortcut: command.suggested_key[os] || command.suggested_key.default,
       });
     }
     return commands;
   },
 
   /**
    * Registers the commands to a document.
-   * @param {Document} doc The XUL document to insert the Keyset.
+   * @param {ChromeWindow} window The XUL window to insert the Keyset.
    */
-  registerKeysToDocument(doc) {
+  registerKeysToDocument(window) {
+    let doc = window.document;
     let keyset = doc.createElementNS(XUL_NS, "keyset");
-    keyset.id = this.keysetID;
+    keyset.id = `ext-keyset-id-${this.id}`;
     this.commands.forEach((command, name) => {
       let keyElement = this.buildKey(doc, name, command.shortcut);
       keyset.appendChild(keyElement);
     });
     doc.documentElement.appendChild(keyset);
+    this.keysetsMap.set(window, keyset);
   },
 
   /**
    * Builds a XUL Key element and attaches an onCommand listener which
    * emits a command event with the provided name when fired.
    *
    * @param {Document} doc The XUL document.
    * @param {String} name The name of the command.
@@ -105,17 +115,22 @@ CommandList.prototype = {
     // We need to have the attribute "oncommand" for the "command" listener to fire,
     // and it is currently ignored when set to the empty string.
     keyElement.setAttribute("oncommand", "//");
 
     /* eslint-disable mozilla/balanced-listeners */
     // We remove all references to the key elements when the extension is shutdown,
     // therefore the listeners for these elements will be garbage collected.
     keyElement.addEventListener("command", (event) => {
-      this.emit("command", name);
+      if (name == "_execute_page_action") {
+        let win = event.target.ownerDocument.defaultView;
+        pageActionFor(this.extension).triggerAction(win);
+      } else {
+        this.emit("command", name);
+      }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     return keyElement;
   },
 
   /**
    * Builds a XUL Key element from the provided shortcut.
@@ -190,17 +205,17 @@ CommandList.prototype = {
       return modifiersMap[modifier];
     }).join(" ");
   },
 };
 
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_commands", (type, directive, extension, manifest) => {
-  commandsMap.set(extension, new CommandList(manifest.commands, extension.id));
+  commandsMap.set(extension, new CommandList(manifest, extension));
 });
 
 extensions.on("shutdown", (type, extension) => {
   let commandsList = commandsMap.get(extension);
   if (commandsList) {
     commandsList.unregister();
     commandsMap.delete(extension);
   }
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,21 +1,21 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
-
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension) {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-page-action";
 
   this.tabManager = TabManager.for(extension);
 
@@ -118,16 +118,29 @@ PageAction.prototype = {
     if (!this.buttons.has(window)) {
       let button = this.addButton(window);
       this.buttons.set(window, button);
     }
 
     return this.buttons.get(window);
   },
 
+  /**
+   * Triggers this page action for the given window, with the same effects as
+   * if it were clicked by a user.
+   *
+   * This has no effect if the page action is hidden for the selected tab.
+   */
+  triggerAction(window) {
+    let pageAction = pageActionMap.get(this.extension);
+    if (pageAction.getProperty(window.gBrowser.selectedTab, "show")) {
+      pageAction.handleClick(window);
+    }
+  },
+
   // Handles a click event on the page action button for the given
   // window.
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   handleClick(window) {
     let tab = window.gBrowser.selectedTab;
     let popupURL = this.tabContext.get(tab).popup;
@@ -158,35 +171,35 @@ PageAction.prototype = {
     for (let window of WindowListManager.browserWindows()) {
       if (this.buttons.has(window)) {
         this.buttons.get(window).remove();
       }
     }
   },
 };
 
-PageAction.for = extension => {
-  return pageActionMap.get(extension);
-};
-
-
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
   let pageAction = new PageAction(manifest.page_action, extension);
   pageActionMap.set(extension, pageAction);
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (pageActionMap.has(extension)) {
     pageActionMap.get(extension).shutdown();
     pageActionMap.delete(extension);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
+PageAction.for = extension => {
+  return pageActionMap.get(extension);
+};
+
+global.pageActionFor = PageAction.for;
 
 extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
   return {
     pageAction: {
       onClicked: new EventManager(context, "pageAction.onClicked", fire => {
         let listener = (evt, tab) => {
           fire(TabManager.convert(extension, tab));
         };
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -19,16 +19,17 @@ support-files =
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_contextMenus.js]
+[browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_getViews.js]
 [browser_ext_lastError.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
@@ -0,0 +1,133 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_execute_page_action_without_popup() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "commands": {
+        "_execute_page_action": {
+          "suggested_key": {
+            "default": "Alt+Shift+J",
+          },
+        },
+        "send-keys-command": {
+          "suggested_key": {
+            "default": "Alt+Shift+3",
+          },
+        },
+      },
+      "page_action": {},
+    },
+
+    background: function() {
+      let isShown = false;
+
+      browser.commands.onCommand.addListener((commandName) => {
+        if (commandName == "_execute_page_action") {
+          browser.test.fail(`The onCommand listener should never fire for ${commandName}.`);
+        } else if (commandName == "send-keys-command") {
+          if (!isShown) {
+            isShown = true;
+            browser.tabs.query({currentWindow: true, active: true}, tabs => {
+              tabs.forEach(tab => {
+                browser.pageAction.show(tab.id);
+              });
+              browser.test.sendMessage("send-keys");
+            });
+          }
+        }
+      });
+
+      browser.pageAction.onClicked.addListener(() => {
+        browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown.");
+        browser.test.notifyPass("page-action-without-popup");
+      });
+
+      browser.test.sendMessage("send-keys");
+    },
+  });
+
+  extension.onMessage("send-keys", () => {
+    EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true});
+    EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("page-action-without-popup");
+  yield extension.unload();
+});
+
+add_task(function* test_execute_page_action_with_popup() {
+  let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>Test Popup</body></html>`;
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "commands": {
+        "_execute_page_action": {
+          "suggested_key": {
+            "default": "Alt+Shift+J",
+          },
+        },
+        "send-keys-command": {
+          "suggested_key": {
+            "default": "Alt+Shift+3",
+          },
+        },
+      },
+      "page_action": {
+        "default_popup": "popup.html",
+      },
+    },
+
+    files: {
+      "popup.html": scriptPage("popup.js"),
+      "popup.js": function() {
+        browser.runtime.sendMessage("popup-opened");
+      },
+    },
+
+    background: function() {
+      let isShown = false;
+
+      browser.commands.onCommand.addListener((message) => {
+        if (message == "_execute_page_action") {
+          browser.test.fail(`The onCommand listener should never fire for ${message}.`);
+        }
+
+        if (message == "send-keys-command") {
+          if (!isShown) {
+            isShown = true;
+            browser.tabs.query({currentWindow: true, active: true}, tabs => {
+              tabs.forEach(tab => {
+                browser.pageAction.show(tab.id);
+              });
+              browser.test.sendMessage("send-keys");
+            });
+          }
+        }
+      });
+
+      browser.pageAction.onClicked.addListener(() => {
+        browser.test.fail(`The onClicked listener should never fire when the pageAction has a popup.`);
+      });
+
+      browser.runtime.onMessage.addListener(msg => {
+        browser.test.assertEq(msg, "popup-opened", "expected popup opened");
+        browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown.");
+        browser.test.notifyPass("page-action-with-popup");
+      });
+
+      browser.test.sendMessage("send-keys");
+    },
+  });
+
+  extension.onMessage("send-keys", () => {
+    EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true});
+    EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("page-action-with-popup");
+  yield extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -1,39 +1,38 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-add_task(function* () {
+add_task(function* test_user_defined_commands() {
   // Create a window before the extension is loaded.
   let win1 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots");
   yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "name": "Commands Extension",
       "commands": {
         "toggle-feature-using-alt-shift-3": {
           "suggested_key": {
             "default": "Alt+Shift+3",
           },
         },
         "toggle-feature-using-alt-shift-comma": {
           "suggested_key": {
             "default": "Alt+Shift+Comma",
           },
           "unrecognized_property": "with-a-random-value",
         },
       },
     },
 
     background: function() {
-      browser.commands.onCommand.addListener((message) => {
-        browser.test.sendMessage("oncommand", message);
+      browser.commands.onCommand.addListener((commandName) => {
+        browser.test.sendMessage("oncommand", commandName);
       });
       browser.test.sendMessage("ready");
     },
   });
 
 
   SimpleTest.waitForExplicitFinish();
   let waitForConsole = new Promise(resolve => {
@@ -48,20 +47,22 @@ add_task(function* () {
   // Create another window after the extension is loaded.
   let win2 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:config");
   yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser);
 
   // Confirm the keysets have been added to both windows.
   let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
   let keyset = win1.document.getElementById(keysetID);
-  is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children");
+  ok(keyset != null, "Expected keyset to exist");
+  is(keyset.childNodes.length, 2, "Expected keyset to have 2 children");
 
   keyset = win2.document.getElementById(keysetID);
-  is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children");
+  ok(keyset != null, "Expected keyset to exist");
+  is(keyset.childNodes.length, 2, "Expected keyset to have 2 children");
 
   // Confirm that the commands are registered to both windows.
   yield focusWindow(win1);
   EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
   let message = yield extension.awaitMessage("oncommand");
   is(message, "toggle-feature-using-alt-shift-3", "Expected onCommand listener to fire with correct message");
 
   yield focusWindow(win2);
@@ -79,8 +80,10 @@ add_task(function* () {
   is(keyset, null, "Expected keyset to be removed from the window");
 
   yield BrowserTestUtils.closeWindow(win1);
   yield BrowserTestUtils.closeWindow(win2);
 
   SimpleTest.endMonitorConsole();
   yield waitForConsole;
 });
+
+
--- a/devtools/client/debugger/content/actions/breakpoints.js
+++ b/devtools/client/debugger/content/actions/breakpoints.js
@@ -111,18 +111,17 @@ function _removeOrDisableBreakpoint(loca
     // If the breakpoint is already disabled, we don't need to remove
     // it from the server. We just need to dispatch an action
     // simulating a successful server request to remove it, and it
     // will be removed completely from the state.
     if(!bp.disabled) {
       return dispatch(Object.assign({}, action, {
         [PROMISE]: bpClient.remove()
       }));
-    }
-    else {
+    } else {
       return dispatch(Object.assign({}, action, { status: "done" }));
     }
   }
 }
 
 function removeAllBreakpoints() {
   return (dispatch, getState) => {
     const breakpoints = getBreakpoints(getState());
@@ -149,31 +148,40 @@ function setBreakpointCondition(location
     }
     if (bp.loading){
       // TODO(jwl): when this function is called, make sure the action
       // creator waits for the breakpoint to exist
       throw new Error("breakpoint must be saved");
     }
 
     const bpClient = getBreakpointClient(bp.actor);
-
-    return dispatch({
+    const action = {
       type: constants.SET_BREAKPOINT_CONDITION,
       breakpoint: bp,
-      condition: condition,
-      [PROMISE]: Task.spawn(function*() {
-        const newClient = yield bpClient.setCondition(gThreadClient, condition);
+      condition: condition
+    };
+
+    // If it's not disabled, we need to update the condition on the
+    // server. Otherwise, just dispatch a non-remote action that
+    // updates the condition locally.
+    if(!bp.disabled) {
+      return dispatch(Object.assign({}, action, {
+        [PROMISE]: Task.spawn(function*() {
+          const newClient = yield bpClient.setCondition(gThreadClient, condition);
 
-        // Remove the old instance and save the new one
-        setBreakpointClient(bpClient.actor, null);
-        setBreakpointClient(newClient.actor, newClient);
+          // Remove the old instance and save the new one
+          setBreakpointClient(bpClient.actor, null);
+          setBreakpointClient(newClient.actor, newClient);
 
-        return { actor: newClient.actor };
-      })
-    });
+          return { actor: newClient.actor };
+        })
+      }));
+    } else {
+      return dispatch(action);
+    }
   };
 }
 
 module.exports = {
   enableBreakpoint,
   addBreakpoint,
   disableBreakpoint,
   removeBreakpoint,
--- a/devtools/client/debugger/content/reducers/breakpoints.js
+++ b/devtools/client/debugger/content/reducers/breakpoints.js
@@ -112,25 +112,33 @@ function update(state = initialState, ac
     break;
   }
 
   case constants.SET_BREAKPOINT_CONDITION: {
     const id = makeLocationId(action.breakpoint.location);
     const bp = state.breakpoints[id];
     emitChange("breakpoint-condition-updated", bp);
 
-    if (action.status === 'start') {
+    if (!action.status) {
+      // No status means that it wasn't a remote request. Just update
+      // the condition locally.
+      return mergeIn(state, ['breakpoints', id], {
+        condition: action.condition
+      });
+    }
+    else if (action.status === 'start') {
       return mergeIn(state, ['breakpoints', id], {
         loading: true,
         condition: action.condition
       });
     }
     else if (action.status === 'done') {
       return mergeIn(state, ['breakpoints', id], {
         loading: false,
+        condition: action.condition,
         // Setting a condition creates a new breakpoint client as of
         // now, so we need to update the actor
         actor: action.value.actor
       });
     }
     else if (action.status === 'error') {
       emitChange("breakpoint-removed", bp);
       return deleteIn(state, ['breakpoints', id]);
--- a/devtools/client/debugger/content/reducers/sources.js
+++ b/devtools/client/debugger/content/reducers/sources.js
@@ -62,18 +62,25 @@ function update(state = initialState, ac
 
   case constants.TOGGLE_PRETTY_PRINT:
     let s = state;
     if (action.status === "error") {
       s = mergeIn(state, ['sourcesText', action.source.actor], {
         loading: false
       });
 
-      // If it errored, just display the source as it way before.
-      emitChange('prettyprinted', s.sources[action.source.actor]);
+      // If it errored, just display the source as it was before, but
+      // only if there is existing text already. If auto-prettifying
+      // is on, the original text may still be coming in and we don't
+      // have it yet. If we try to set empty text we confuse the
+      // editor because it thinks it's already displaying the source's
+      // text and won't load the text when it actually comes in.
+      if(s.sourcesText[action.source.actor].text != null) {
+        emitChange('prettyprinted', s.sources[action.source.actor]);
+      }
     }
     else {
       s = _updateText(state, action);
       // Don't do this yet, the progress bar is still imperatively shown
       // from the source view. We will fix in the next iteration.
       // emitChange('source-text-loaded', s.sources[action.source.actor]);
 
       if (action.status === 'done') {
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -77,16 +77,17 @@ var DebuggerView = {
     this.Sources.initialize();
     this.VariableBubble.initialize();
     this.WatchExpressions.initialize();
     this.EventListeners.initialize();
     this.GlobalSearch.initialize();
     this._initializeVariablesView();
 
     this._editorSource = {};
+    this._editorDocuments = {};
 
     document.title = L10N.getStr("DebuggerWindowTitle");
 
     this.editor.on("cursorActivity", this.Sources._onEditorCursorActivity);
 
     this.controller = DebuggerController;
     const getState = this.controller.getState;
 
@@ -369,24 +370,25 @@ var DebuggerView = {
   },
 
   removeEditorBreakpoint: function (breakpoint) {
     const { location } = breakpoint;
     const source = queries.getSelectedSource(this.controller.getState());
 
     if (source && source.actor === location.actor) {
       this.editor.removeBreakpoint(location.line - 1);
+      this.editor.removeBreakpointCondition(location.line - 1);
     }
   },
 
   renderEditorBreakpointCondition: function (breakpoint) {
-    const { location, condition } = breakpoint;
+    const { location, condition, disabled } = breakpoint;
     const source = queries.getSelectedSource(this.controller.getState());
 
-    if (source && source.actor === location.actor) {
+    if (source && source.actor === location.actor && !disabled) {
       if (condition) {
         this.editor.setBreakpointCondition(location.line - 1);
       } else {
         this.editor.removeBreakpointCondition(location.line - 1);
       }
     }
   },
 
@@ -410,24 +412,38 @@ var DebuggerView = {
   showProgressBar: function() {
     this._editorDeck.selectedIndex = 2;
   },
 
   /**
    * Sets the currently displayed text contents in the source editor.
    * This resets the mode and undo stack.
    *
+   * @param string documentKey
+   *        Key to get the correct editor document
+   *
    * @param string aTextContent
    *        The source text content.
+   *
+   * @param boolean shouldUpdateText
+            Forces a text and mode reset
    */
-  _setEditorText: function(aTextContent = "") {
-    this.editor.setMode(Editor.modes.text);
-    this.editor.setText(aTextContent);
+  _setEditorText: function(documentKey, aTextContent = "", shouldUpdateText = false) {
+    const isNew = this._setEditorDocument(documentKey);
+
     this.editor.clearDebugLocation();
     this.editor.clearHistory();
+    this.editor.setCursor({ line: 0, ch: 0});
+    this.editor.removeBreakpoints();
+
+    // Only set editor's text and mode if it is a new document
+    if (isNew || shouldUpdateText) {
+      this.editor.setMode(Editor.modes.text);
+      this.editor.setText(aTextContent);
+    }
   },
 
   /**
    * Sets the proper editor mode (JS or HTML) according to the specified
    * content type, or by determining the type from the url or text content.
    *
    * @param string aUrl
    *        The source url.
@@ -447,16 +463,39 @@ var DebuggerView = {
     if (aTextContent.match(/^\s*</)) {
       return void this.editor.setMode(Editor.modes.html);
     }
 
     // Unknown language, use text.
     this.editor.setMode(Editor.modes.text);
   },
 
+  /**
+   * Sets the editor's displayed document.
+   * If there isn't a document for the source, create one
+   *
+   * @param string key - key used to access the editor document cache
+   *
+   * @return boolean isNew - was the document just created
+   */
+  _setEditorDocument: function(key) {
+    let isNew;
+
+    if (!this._editorDocuments[key]) {
+      isNew = true;
+      this._editorDocuments[key] = this.editor.createDocument();
+    } else {
+      isNew = false;
+    }
+
+    const doc = this._editorDocuments[key];
+    this.editor.replaceDocument(doc);
+    return isNew;
+  },
+
   renderBlackBoxed: function(source) {
     this._renderSourceText(
       source,
       queries.getSourceText(this.controller.getState(), source.actor)
     );
   },
 
   renderPrettyPrinted: function(source) {
@@ -472,16 +511,17 @@ var DebuggerView = {
       queries.getSourceText(this.controller.getState(), source.actor),
       queries.getSelectedSourceOpts(this.controller.getState())
     );
   },
 
   _renderSourceText: function(source, textInfo, opts = {}) {
     const selectedSource = queries.getSelectedSource(this.controller.getState());
 
+    // Exit early if we're attempting to render an unselected source
     if (!selectedSource || selectedSource.actor !== source.actor) {
       return;
     }
 
     if (source.isBlackBoxed) {
       this.showBlackBoxMessage();
       setTimeout(() => {
         window.emit(EVENTS.SOURCE_SHOWN, source);
@@ -491,22 +531,22 @@ var DebuggerView = {
     else {
       this.showEditor();
     }
 
     if (textInfo.loading) {
       // TODO: bug 1228866, we need to update `_editorSource` here but
       // still make the editor be updated when the full text comes
       // through somehow.
-      this._setEditorText(L10N.getStr("loadingText"));
+      this._setEditorText('loading', L10N.getStr("loadingText"));
       return;
     }
     else if (textInfo.error) {
       let msg = L10N.getFormatStr("errorLoadingText2", textInfo.error);
-      this._setEditorText(msg);
+      this._setEditorText('error', msg);
       Cu.reportError(msg);
       dumpn(msg);
 
       this.showEditor();
       window.emit(EVENTS.SOURCE_ERROR_SHOWN, source);
       return;
     }
 
@@ -523,24 +563,28 @@ var DebuggerView = {
 
     if (this._editorSource.actor === source.actor &&
         this._editorSource.prettyPrinted === source.isPrettyPrinted &&
         this._editorSource.blackboxed === source.isBlackBoxed) {
       this.updateEditorPosition(opts);
       return;
     }
 
+    let { text, contentType } = textInfo;
+    let shouldUpdateText = this._editorSource.prettyPrinted != source.isPrettyPrinted;
+    this._setEditorText(source.actor, text, shouldUpdateText);
+
     this._editorSource.actor = source.actor;
     this._editorSource.prettyPrinted = source.isPrettyPrinted;
     this._editorSource.blackboxed = source.isBlackBoxed;
+    this._editorSource.prettyPrinted = source.isPrettyPrinted;
 
-    let { text, contentType } = textInfo;
-    this._setEditorText(text);
     this._setEditorMode(source.url, contentType, text);
     this.updateEditorBreakpoints(source);
+
     setTimeout(() => {
       window.emit(EVENTS.SOURCE_SHOWN, source);
     }, 0);
 
     this.updateEditorPosition(opts);
   },
 
   updateEditorPosition: function(opts) {
@@ -782,29 +826,29 @@ var DebuggerView = {
     this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   handleTabNavigation: function() {
     dumpn("Handling tab navigation in the DebuggerView");
-
     this.Filtering.clearSearch();
     this.GlobalSearch.clearView();
     this.StackFrames.empty();
     this.Sources.empty();
     this.Variables.empty();
     this.EventListeners.empty();
 
     if (this.editor) {
       this.editor.setMode(Editor.modes.text);
       this.editor.setText("");
       this.editor.clearHistory();
       this._editorSource = {};
+      this._editorDocuments = {};
     }
   },
 
   Toolbar: null,
   Options: null,
   Filtering: null,
   GlobalSearch: null,
   StackFrames: null,
--- a/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_conditional-breakpoints-03.js
@@ -20,36 +20,55 @@ function test() {
     const actions = bindActionCreators(gPanel);
     const getState = gDebugger.DebuggerController.getState;
 
     // This test forces conditional breakpoints to be evaluated on the
     // client-side
     var client = gPanel.target.client;
     client.mainRoot.traits.conditionalBreakpoints = false;
 
+    function waitForConditionUpdate() {
+      // This will close the popup and send another request to update
+      // the condition
+      gSources._hideConditionalPopup();
+      return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+    }
+
     Task.spawn(function*() {
       yield waitForSourceAndCaretAndScopes(gPanel, ".html", 17);
       const location = { actor: gSources.selectedValue, line: 18 };
 
       yield actions.addBreakpoint(location, "hello");
       yield actions.disableBreakpoint(location);
       yield actions.addBreakpoint(location);
 
       const bp = queries.getBreakpoint(getState(), location);
       is(bp.condition, "hello", "The conditional expression is correct.");
 
-      const finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
+      let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
       EventUtils.sendMouseEvent({ type: "click" },
                                 gDebugger.document.querySelector(".dbg-breakpoint"),
                                 gDebugger);
       yield finished;
 
       const textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox");
       is(textbox.value, "hello", "The expression is correct (2).")
 
+      yield waitForConditionUpdate();
+      yield actions.disableBreakpoint(location);
+      yield actions.setBreakpointCondition(location, "foo");
+      yield actions.addBreakpoint(location);
+
+      finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
+      EventUtils.sendMouseEvent({ type: "click" },
+                                gDebugger.document.querySelector(".dbg-breakpoint"),
+                                gDebugger);
+      yield finished;
+      is(textbox.value, "foo", "The expression is correct (3).")
+
       // Reset traits back to default value
       client.mainRoot.traits.conditionalBreakpoints = true;
       resumeDebuggerThenCloseAndFinish(gPanel);
     });
 
     callInTab(gTab, "ermahgerd");
   });
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_server-conditional-bp-03.js
@@ -16,34 +16,53 @@ function test() {
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const queries = gDebugger.require('./content/queries');
     const constants = gDebugger.require('./content/constants');
     const actions = bindActionCreators(gPanel);
     const getState = gDebugger.DebuggerController.getState;
 
+    function waitForConditionUpdate() {
+      // This will close the popup and send another request to update
+      // the condition
+      gSources._hideConditionalPopup();
+      return waitForDispatch(gPanel, constants.SET_BREAKPOINT_CONDITION);
+    }
+
     Task.spawn(function*() {
       yield waitForSourceAndCaretAndScopes(gPanel, ".html", 17);
       const location = { actor: gSources.selectedValue, line: 18 };
 
       yield actions.addBreakpoint(location, "hello");
       yield actions.disableBreakpoint(location);
       yield actions.addBreakpoint(location);
 
       const bp = queries.getBreakpoint(getState(), location);
       is(bp.condition, "hello", "The conditional expression is correct.");
 
-      const finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
+      let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
       EventUtils.sendMouseEvent({ type: "click" },
                                 gDebugger.document.querySelector(".dbg-breakpoint"),
                                 gDebugger);
       yield finished;
 
       const textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox");
       is(textbox.value, "hello", "The expression is correct (2).")
 
+      yield waitForConditionUpdate();
+      yield actions.disableBreakpoint(location);
+      yield actions.setBreakpointCondition(location, "foo");
+      yield actions.addBreakpoint(location);
+
+      finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
+      EventUtils.sendMouseEvent({ type: "click" },
+                                gDebugger.document.querySelector(".dbg-breakpoint"),
+                                gDebugger);
+      yield finished;
+      is(textbox.value, "foo", "The expression is correct (3).")
+
       yield resumeDebuggerThenCloseAndFinish(gPanel);
     });
 
     callInTab(gTab, "ermahgerd");
   });
 }
--- a/devtools/client/shared/redux/non-react-subscriber.js
+++ b/devtools/client/shared/redux/non-react-subscriber.js
@@ -84,21 +84,16 @@ function makeStateBroadcaster(stillAlive
       enqueuedChanges.push([name, payload]);
     },
 
     subscribeToStore: store => {
       store.subscribe(() => {
         if (stillAliveFunc()) {
           enqueuedChanges.forEach(([name, payload]) => {
             if (listeners[name]) {
-              let payloadStr = payload;
-              try {
-                payloadStr = JSON.stringify(payload);
-              }
-              catch(e) {}
               listeners[name].forEach(listener => {
                 listener(payload)
               });
             }
           });
           enqueuedChanges = [];
         }
       });
--- a/devtools/client/sourceeditor/debugger.js
+++ b/devtools/client/sourceeditor/debugger.js
@@ -169,18 +169,38 @@ function addBreakpoint(ctx, line, cond) 
     DevToolsUtils.executeSoon(() => _addBreakpoint(ctx, line, cond));
   } else {
     _addBreakpoint(ctx, line, cond);
   }
   return deferred.promise;
 }
 
 /**
+ * Helps reset the debugger's breakpoint state
+ * - removes the breakpoints in the editor
+ * - cleares the debugger's breakpoint state
+ *
+ * Note, does not *actually* remove a source's breakpoints.
+ * The canonical state is kept in the app state.
+ *
+ */
+function removeBreakpoints(ctx) {
+  let { ed, cm } = ctx;
+
+  let meta = dbginfo.get(ed);
+  if (meta.breakpoints != null) {
+    meta.breakpoints = {};
+  }
+
+  cm.doc.iter((line) => { removeBreakpoint(ctx, line) });
+}
+
+/**
  * Removes a visual breakpoint from a specified line and
- * makes Editor to emit a breakpointRemoved event.
+ * makes Editor emit a breakpointRemoved event.
  */
 function removeBreakpoint(ctx, line) {
   if (!hasBreakpoint(ctx, line)) {
     return;
   }
 
   let { ed, cm } = ctx;
   let meta = dbginfo.get(ed);
@@ -298,12 +318,12 @@ function findNext(ctx, query) {
 function findPrev(ctx, query) {
   doSearch(ctx, true, query);
 }
 
 // Export functions
 
 [
   initialize, hasBreakpoint, addBreakpoint, removeBreakpoint, moveBreakpoint,
-  setBreakpointCondition, removeBreakpointCondition, getBreakpoints,
+  setBreakpointCondition, removeBreakpointCondition, getBreakpoints, removeBreakpoints,
   setDebugLocation, getDebugLocation, clearDebugLocation, find, findNext,
   findPrev
 ].forEach(func => module.exports[func.name] = func);
--- a/devtools/client/sourceeditor/editor.js
+++ b/devtools/client/sourceeditor/editor.js
@@ -236,16 +236,17 @@ function Editor(config) {
 
   events.decorate(this);
 }
 
 Editor.prototype = {
   container: null,
   version: null,
   config: null,
+  Doc: null,
 
   /**
    * Appends the current Editor instance to the element specified by
    * 'el'. You can also provide your won iframe to host the editor as
    * an optional second parameter. This method actually creates and
    * loads CodeMirror and all its dependencies.
    *
    * This method is asynchronous and returns a promise.
@@ -297,16 +298,17 @@ Editor.prototype = {
 
       win.CodeMirror.commands.save = () => this.emit("saveRequested");
 
       // Create a CodeMirror instance add support for context menus,
       // overwrite the default controller (otherwise items in the top and
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
+      this.Doc = win.CodeMirror.Doc;
 
       // Disable APZ for source editors. It currently causes the line numbers to
       // "tear off" and swim around on top of the content. Bug 1160601 tracks
       // finding a solution that allows APZ to work with CodeMirror.
       cm.getScrollerElement().addEventListener("wheel", ev => {
         // By handling the wheel events ourselves, we force the platform to
         // scroll synchronously, like it did before APZ. However, we lose smooth
         // scrolling for users with mouse wheels. This seems acceptible vs.
@@ -484,16 +486,32 @@ Editor.prototype = {
     if (!this.container) {
       throw new Error("Can't load a script until the editor is loaded.");
     }
     let win = this.container.contentWindow.wrappedJSObject;
     Services.scriptloader.loadSubScript(url, win, "utf8");
   },
 
   /**
+   * Creates a CodeMirror Document
+   * @returns CodeMirror.Doc
+   */
+  createDocument: function() {
+     return new this.Doc("");
+  },
+
+  /**
+   * Replaces the current document with a new source document
+   */
+  replaceDocument: function(doc) {
+    let cm = editors.get(this);
+    cm.swapDoc(doc);
+  },
+
+  /**
    * Changes the value of a currently used highlighting mode.
    * See Editor.modes for the list of all supported modes.
    */
   setMode: function(value) {
     this.setOption("mode", value);
 
     // If autocomplete was set up and the mode is changing, then
     // turn it off and back on again so the proper mode can be used.
--- a/devtools/client/themes/images/clear.svg
+++ b/devtools/client/themes/images/clear.svg
@@ -1,6 +1,7 @@
 <!-- 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/. -->
-<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
-  <path d="M8,1.1c-3.8,0-6.9,3-6.9,6.9s3,6.9,6.9,6.9s6.9-3,6.9-6.9S11.8,1.1,8,1.1z M13.1,7.9c0,1.1-0.4,2.1-1,3L5,3.8 c0.8-0.6,1.9-1,3-1C10.8,2.8,13.1,5.1,13.1,7.9z M2.9,7.9c0-1.1,0.3-2.1,0.9-2.9l7.1,7.1C10.1,12.7,9.1,13,8,13 C5.2,13,2.9,10.7,2.9,7.9z"/>
-</svg>
\ No newline at end of file
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#babec3">
+  <path d="M6 3h3V2c0-.003-3 0-3 0-.002 0 0 1 0 1zm-5 .5c0-.276.226-.5.494-.5h12.012c.273 0 .494.232.494.5 0 .276-.226.5-.494.5H1.494C1.22 4 1 3.768 1 3.5zM5 3V2c0-.553.444-1 1-1h3c.552 0 1 .443 1 1v1H5z"/>
+  <path d="M5 13h1V7H5v6zm4 0h1V7H9v6zm3-8v8.998c-.046.553-.45 1.002-1 1.002H4c-.55 0-.954-.456-1-1.002V5h9zm-5 8h1V7H7v6z"/>
+</svg>
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -102,41 +102,32 @@ HeapSnapshot::Create(JSContext* cx,
     rv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
   return snapshot.forget();
 }
 
 template<typename MessageType>
 static bool
-parseMessage(ZeroCopyInputStream& stream, MessageType& message)
+parseMessage(ZeroCopyInputStream& stream, uint32_t sizeOfMessage, MessageType& message)
 {
   // We need to create a new `CodedInputStream` for each message so that the
   // 64MB limit is applied per-message rather than to the whole stream.
   CodedInputStream codedStream(&stream);
 
   // The protobuf message nesting that core dumps exhibit is dominated by
   // allocation stacks' frames. In the most deeply nested case, each frame has
   // two messages: a StackFrame message and a StackFrame::Data message. These
   // frames are on top of a small constant of other messages. There are a
   // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for
   // the two messages per frame plus some head room for the constant number of
   // non-dominating messages.
   codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3);
 
-  // Because protobuf messages aren't self-delimiting, we serialize each message
-  // preceeded by its size in bytes. When deserializing, we read this size and
-  // then limit reading from the stream to the given byte size. If we didn't,
-  // then the first message would consume the entire stream.
-
-  uint32_t size = 0;
-  if (NS_WARN_IF(!codedStream.ReadVarint32(&size)))
-    return false;
-
-  auto limit = codedStream.PushLimit(size);
+  auto limit = codedStream.PushLimit(sizeOfMessage);
   if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) ||
       NS_WARN_IF(!codedStream.ConsumedEntireMessage()) ||
       NS_WARN_IF(codedStream.BytesUntilLimit() != 0))
   {
     return false;
   }
 
   codedStream.PopLimit(limit);
@@ -386,60 +377,54 @@ HeapSnapshot::saveStackFrame(const proto
 
   outFrameId = id;
   return true;
 }
 
 #undef GET_STRING_OR_REF_WITH_PROP_NAMES
 #undef GET_STRING_OR_REF
 
-static inline bool
-StreamHasData(GzipInputStream& stream)
+// Because protobuf messages aren't self-delimiting, we serialize each message
+// preceded by its size in bytes. When deserializing, we read this size and then
+// limit reading from the stream to the given byte size. If we didn't, then the
+// first message would consume the entire stream.
+static bool
+readSizeOfNextMessage(ZeroCopyInputStream& stream, uint32_t* sizep)
 {
-  // Test for the end of the stream. The protobuf library gives no way to tell
-  // the difference between an underlying read error and the stream being
-  // done. All we can do is attempt to read data and extrapolate guestimations
-  // from the result of that operation.
-
-  const void* buf;
-  int size;
-  bool more = stream.Next(&buf, &size);
-  if (!more)
-    // Could not read any more data. We are optimistic and assume the stream is
-    // just exhausted and there is not an underlying IO error, since this
-    // function is only called at message boundaries.
-    return false;
-
-  // There is more data still available in the stream. Return the data we read
-  // to the stream and let the parser get at it.
-  stream.BackUp(size);
-  return true;
+  MOZ_ASSERT(sizep);
+  CodedInputStream codedStream(&stream);
+  return codedStream.ReadVarint32(sizep) && *sizep > 0;
 }
 
 bool
 HeapSnapshot::init(JSContext* cx, const uint8_t* buffer, uint32_t size)
 {
   if (!nodes.init() || !frames.init())
     return false;
 
   ArrayInputStream stream(buffer, size);
   GzipInputStream gzipStream(&stream);
+  uint32_t sizeOfMessage = 0;
 
   // First is the metadata.
 
   protobuf::Metadata metadata;
-  if (!parseMessage(gzipStream, metadata))
+  if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+    return false;
+  if (!parseMessage(gzipStream, sizeOfMessage, metadata))
     return false;
   if (metadata.has_timestamp())
     timestamp.emplace(metadata.timestamp());
 
   // Next is the root node.
 
   protobuf::Node root;
-  if (!parseMessage(gzipStream, root))
+  if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+    return false;
+  if (!parseMessage(gzipStream, sizeOfMessage, root))
     return false;
 
   // Although the id is optional in the protobuf format for future proofing, we
   // can't currently do anything without it.
   if (NS_WARN_IF(!root.has_id()))
     return false;
   rootId = root.id();
 
@@ -448,19 +433,23 @@ HeapSnapshot::init(JSContext* cx, const 
   if (NS_WARN_IF(!edgeReferents.init()))
     return false;
 
   if (NS_WARN_IF(!saveNode(root, edgeReferents)))
     return false;
 
   // Finally, the rest of the nodes in the core dump.
 
-  while (StreamHasData(gzipStream)) {
+  // Test for the end of the stream. The protobuf library gives no way to tell
+  // the difference between an underlying read error and the stream being
+  // done. All we can do is attempt to read the size of the next message and
+  // extrapolate guestimations from the result of that operation.
+  while (readSizeOfNextMessage(gzipStream, &sizeOfMessage)) {
     protobuf::Node node;
-    if (!parseMessage(gzipStream, node))
+    if (!parseMessage(gzipStream, sizeOfMessage, node))
       return false;
     if (NS_WARN_IF(!saveNode(node, edgeReferents)))
       return false;
   }
 
   // Check the set of node ids referred to by edges we found and ensure that we
   // have the node corresponding to each id. If we don't have all of them, it is
   // unsafe to perform analyses of this heap snapshot.
--- a/dom/tests/mochitest/notification/mochitest.ini
+++ b/dom/tests/mochitest/notification/mochitest.ini
@@ -2,12 +2,13 @@
 
 support-files =
   MockServices.js
   NotificationTest.js
 
 [test_notification_basics.html]
 [test_notification_storage.html]
 [test_bug931307.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
 [test_notification_resend.html]
 skip-if = (buildapp != 'b2g' && buildapp != 'mulet') || e10s # On e10s, faking the app seems to be failing
 [test_notification_noresend.html]
 skip-if = (toolkit == 'gonk') # Mochitest on Gonk registers an app manifest that messes with the logic
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -206,19 +206,27 @@ public final class IntentHelper implemen
 
             // (Bug 1192436) We don't know if marketIntent matches any Activities (e.g. non-Play
             // Store devices). If it doesn't, clicking the link will cause no action to occur.
             ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
                     activity, activity.getSupportFragmentManager(), marketIntent);
             callback.sendSuccess(null);
 
         }  else {
+            // We originally loaded about:neterror when we failed to match the Intent. However, many
+            // websites worked around Chrome's implementation, which does nothing in this case. For
+            // example, the site might set a timeout and load a play store url for their app if the
+            // intent link fails to load, i.e. the app is not installed. These work-arounds would often
+            // end with our users seeing about:neterror instead of the intended experience. While I
+            // feel showing about:neterror is a better solution for users (when not hacked around),
+            // we should match the status quo for the good of our users.
+            //
             // Don't log the URI to prevent leaking it.
-            Log.w(LOGTAG, "Unable to open URI, default case - loading about:neterror");
-            callback.sendError(getUnknownProtocolErrorPageUri(intent.getData().toString()));
+            Log.w(LOGTAG, "Unable to open URI - ignoring click");
+            callback.sendSuccess(null); // pretend we opened the page.
         }
     }
 
     private static boolean isFallbackUrlValid(@Nullable final String fallbackUrl) {
         if (fallbackUrl == null) {
             return false;
         }
 
--- a/mobile/android/base/resources/drawable/tab_panel_tab_background.xml
+++ b/mobile/android/base/resources/drawable/tab_panel_tab_background.xml
@@ -20,17 +20,17 @@
             </item>
         </layer-list>
     </item>
 
     <item>
         <layer-list>
             <item>
                 <shape android:shape="rectangle">
-                    <solid android:color="@color/toolbar_grey"/>
+                    <solid android:color="@color/about_page_header_grey"/>
                 </shape>
             </item>
 
             <item>
                 <bitmap android:src="@drawable/globe_light"
                         android:gravity="center"/>
             </item>
         </layer-list>
--- a/mobile/android/chrome/content/content.js
+++ b/mobile/android/chrome/content/content.js
@@ -1,15 +1,16 @@
 /* -*- 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/. */
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
 
 var dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content");
@@ -99,8 +100,14 @@ var AboutReaderListener = {
     }
   },
 };
 AboutReaderListener.init();
 
 addMessageListener("RemoteLogins:fillForm", function(message) {
   LoginManagerContent.receiveMessage(message, content);
 });
+
+ExtensionContent.init(this);
+addEventListener("unload", () => {
+  ExtensionContent.uninit(this);
+});
+
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -305,16 +305,18 @@
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
 @BINPATH@/components/nsLoginManagerPrompter.js
 @BINPATH@/components/storage-mozStorage.js
 @BINPATH@/components/crypto-SDR.js
 @BINPATH@/components/NetworkGeolocationProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.js
 @BINPATH@/components/extensions.manifest
+@BINPATH@/components/utils.manifest
+@BINPATH@/components/simpleServices.js
 @BINPATH@/components/addonManager.js
 @BINPATH@/components/amContentHandler.js
 @BINPATH@/components/amInstallTrigger.js
 @BINPATH@/components/amWebInstallListener.js
 @BINPATH@/components/nsBlocklistService.js
 #ifndef RELEASE_BUILD
 @BINPATH@/components/TabSource.js
 #endif
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -1,14 +1,14 @@
 [DEFAULT]
-skip-if = os == 'android'
 support-files =
   file_download.html
   file_download.txt
   interruptible.sjs
   file_sample.html
 
 [test_chrome_ext_downloads_download.html]
 [test_chrome_ext_downloads_misc.html]
 skip-if = 1 # Currently causes too many intermittent failures.
 [test_chrome_ext_downloads_search.html]
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
+skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-skip-if = os == 'android' || buildapp == 'mulet'
+skip-if = buildapp == 'mulet' || asan
 support-files =
   head.js
   file_WebRequest_page1.html
   file_WebRequest_page2.html
   file_WebNavigation_page1.html
   file_WebNavigation_page2.html
   file_WebNavigation_page3.html
   file_image_good.png
@@ -39,40 +39,42 @@ skip-if = buildapp == 'b2g' # runat != d
 [test_ext_generate.html]
 [test_ext_idle.html]
 [test_ext_localStorage.html]
 [test_ext_onmessage_removelistener.html]
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
 skip-if = buildapp == 'b2g' # JavaScript error: jar:remoteopenfile:///data/local/tmp/generated-extension.xpi!/content.js, line 46: NS_ERROR_ILLEGAL_VALUE:
 [test_ext_runtime_connect.html]
-skip-if = buildapp == 'b2g' # port.sender.tab is undefined on b2g.
+skip-if = (os == 'android' || buildapp == 'b2g') # port.sender.tab is undefined on b2g. Bug 1258975 on android.
 [test_ext_runtime_connect2.html]
-skip-if = buildapp == 'b2g' # port.sender.tab is undefined on b2g.
+skip-if = (os == 'android' || buildapp == 'b2g') # port.sender.tab is undefined on b2g. Bug 1258975 on android.
 [test_ext_runtime_disconnect.html]
 [test_ext_runtime_getPlatformInfo.html]
 [test_ext_runtime_sendMessage.html]
 [test_ext_sandbox_var.html]
 [test_ext_sendmessage_reply.html]
-skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
+skip-if = (os == 'android' || buildapp == 'b2g') # sender.tab is undefined on b2g. Bug 1258975 on android.
 [test_ext_sendmessage_reply2.html]
-skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
+skip-if = (os == 'android' || buildapp == 'b2g') # sender.tab is undefined on b2g. Bug 1258975 on android.
 [test_ext_sendmessage_doublereply.html]
-skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
+skip-if = (os == 'android' || buildapp == 'b2g') # sender.tab is undefined on b2g. Bug 1258975 on android.
 [test_ext_storage.html]
 [test_ext_background_runtime_connect_params.html]
 [test_ext_cookies.html]
 [test_ext_cookies_permissions.html]
 skip-if = e10s || buildapp == 'b2g' # Uses cookie service via SpecialPowers.Services, which does not support e10s.
 [test_ext_bookmarks.html]
-skip-if = buildapp == 'b2g' # unimplemented api.
+skip-if = (os == 'android' || buildapp == 'b2g') # unimplemented api. Bug 1258975 on android.
 [test_ext_alarms.html]
 [test_ext_background_window_properties.html]
 [test_ext_background_sub_windows.html]
 [test_ext_background_api_injection.html]
 [test_ext_jsversion.html]
 skip-if = e10s || buildapp == 'b2g' # Uses a console monitor which doesn't work from a content process. The code being tested doesn't run in a tab content process in any case.
 [test_ext_i18n.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_web_accessible_resources.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_webrequest.html]
-skip-if = buildapp == 'b2g' # webrequest api uninplemented (bug 1199504)
+skip-if = (os == 'android' || buildapp == 'b2g') # webrequest api uninplemented (bug 1199504). Bug 1258975 on android.
 [test_ext_webnavigation.html]
-skip-if = buildapp == 'b2g' # needs TabManager which is not yet implemented
+skip-if = (os == 'android' || buildapp == 'b2g') # needs TabManager which is not yet implemented. Bug 1258975 on android.
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -14,25 +14,25 @@ support-files =
   subtst_privbrowsing_1.html
   subtst_privbrowsing_2.html
   subtst_privbrowsing_3.html
   subtst_privbrowsing_4.html
   subtst_prompt_async.html
 
 [test_basic_form_2pw_2.html]
 [test_basic_form_autocomplete.html]
-skip-if = toolkit == 'android'
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_bug_627616.html]
-skip-if = toolkit == 'android' #TIMED_OUT
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_master_password.html]
-skip-if = toolkit == 'android' #TIMED_OUT
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_master_password_cleanup.html]
-skip-if = toolkit == 'android'
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_notifications_popup.html]
-skip-if = true || os == "linux" || toolkit == 'android' # bug 934057
+skip-if = true || os == "linux" || toolkit == 'android' # bug 934057. Bug 1258975 on android.
 [test_prompt.html]
-skip-if = os == "linux" || toolkit == 'android' #TIMED_OUT
+skip-if = os == "linux" || toolkit == 'android' # Bug 1258975 on android.
 [test_prompt_async.html]
-skip-if = toolkit == 'android' #TIMED_OUT
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_xhr.html]
-skip-if = toolkit == 'android' #TIMED_OUT
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_xml_load.html]
-skip-if = toolkit == 'android' #TIMED_OUT
+skip-if = toolkit == 'android' # Bug 1258975 on android.
\ No newline at end of file
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -25,9 +25,10 @@ skip-if = toolkit == 'android' # autocom
 [test_form_action_javascript.html]
 [test_formless_autofill.html]
 skip-if = toolkit == 'android' # Bug 1259768
 [test_input_events.html]
 [test_input_events_for_identical_values.html]
 [test_maxlength.html]
 [test_passwords_in_type_password.html]
 [test_recipe_login_fields.html]
-[test_xhr_2.html]
+skip-if = (toolkit == 'android') # Bug 1258975 on android.
+[test_xhr_2.html]
\ No newline at end of file
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5582,31 +5582,33 @@
   "POPUP_NOTIFICATION_STATS": {
     "releaseChannelCollection": "opt-out",
     "alert_emails": ["firefox-dev@mozilla.org"],
     "bug_numbers": [1207089],
     "expires_in_version": "50",
     "kind": "enumerated",
     "keyed": true,
     "n_values": 40,
-    "description": "Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
+    "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
   },
   "POPUP_NOTIFICATION_MAIN_ACTION_MS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "48",
+    "bug_numbers": [1207089],
+    "expires_in_version": "52",
     "kind": "exponential",
     "keyed": true,
     "low": 100,
     "high": 600000,
     "n_buckets": 40,
     "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
   },
   "POPUP_NOTIFICATION_DISMISSAL_MS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "48",
+    "bug_numbers": [1207089],
+    "expires_in_version": "52",
     "kind": "exponential",
     "keyed": true,
     "low": 200,
     "high": 20000,
     "n_buckets": 50,
     "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
   },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_RELOAD_MS": {