Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 06 Jun 2016 17:14:49 -0700
changeset 376155 c276b302b73f6d86955dffdc6e061b759af4232e
parent 376154 93800cba65b28bd9f77c3e7b46cd681049b63743 (current diff)
parent 375957 1828937da9493b2cd54862b9c520b2ba5c7db92b (diff)
child 376156 c82675873549648be5c212b09dba4a4c9c6f4b42
push id20510
push usercholler@mozilla.com
push dateTue, 07 Jun 2016 13:42:30 +0000
reviewersmerge
milestone50.0a1
Merge m-c to inbound, a=merge
mobile/android/base/resources/values/xml.xml
toolkit/components/telemetry/Histograms.json
--- a/.hgtags
+++ b/.hgtags
@@ -121,8 +121,9 @@ 66a95a483d2c77dfc387019336d18093acd6aac2
 312c68b16549de9cea1557f461d5d234bd5e0a7d FIREFOX_AURORA_41_BASE
 7a19194812eb767bee7cdf8fc36ba9a383c1bead FIREFOX_AURORA_42_BASE
 fcef8ded82219c89298b4e376cfbdfba79a1d35a FIREFOX_AURORA_43_BASE
 67a788db9f07822cfef52351bbbe3745dff8bd7f FIREFOX_AURORA_44_BASE
 99137d6d4061f408ae0869122649d8bdf489cc30 FIREFOX_AURORA_45_BASE
 67c66c2878aed17ae3096d7db483ddbb2293c503 FIREFOX_AURORA_46_BASE
 68d3781deda0d4d58ec9877862830db89669b3a5 FIREFOX_AURORA_47_BASE
 1c6385ae1fe7e37d8f23f958ce14582f07af729e FIREFOX_AURORA_48_BASE
+d98f20c25feeac4dd7ebbd1c022957df1ef58af4 FIREFOX_AURORA_49_BASE
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1247047 - Update Android dependencies (Google Play Services)
+Merge day clobber
\ No newline at end of file
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -18,16 +18,17 @@ skip-if = os != "win" # This test tests 
 [browser_connection_bug388287.js]
 [browser_cookies_exceptions.js]
 [browser_defaultbrowser_alwayscheck.js]
 [browser_healthreport.js]
 skip-if = true || !healthreport # Bug 1185403 for the "true"
 [browser_homepages_filter_aboutpreferences.js]
 [browser_notifications_do_not_disturb.js]
 [browser_permissions_urlFieldHidden.js]
+skip-if = true # Bug 1278388
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
 [browser_privacypane_5.js]
 [browser_privacypane_8.js]
 [browser_sanitizeOnShutdown_prefLocked.js]
 [browser_searchsuggestions.js]
--- a/browser/config/version.txt
+++ b/browser/config/version.txt
@@ -1,1 +1,1 @@
-49.0a1
+50.0a1
--- a/browser/config/version_display.txt
+++ b/browser/config/version_display.txt
@@ -1,1 +1,1 @@
-49.0a1
+50.0a1
--- a/browser/extensions/loop/chrome/test/mochitest/browser.ini
+++ b/browser/extensions/loop/chrome/test/mochitest/browser.ini
@@ -15,12 +15,12 @@ skip-if = os == "linux" # Bug 1266041
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_chat.js]
 [browser_mozLoop_context.js]
 [browser_mozLoop_infobar.js]
 [browser_mozLoop_socialShare.js]
 [browser_panel_privateBrowsing.js]
 [browser_mozLoop_sharingListeners.js]
 [browser_mozLoop_telemetry.js]
-skip-if = os == win && !debug # Bug 1267562 zombiecheck | child process 1228 still alive after shutdown (on win7-vm specifically)
+skip-if = os == (win && !debug) || true # Bug 1267562 zombiecheck | child process 1228 still alive after shutdown (on win7-vm specifically) | Bug 1278389 for the true
 [browser_sharingTitleListeners.js]
 [browser_throttler.js]
 [browser_toolbarbutton.js]
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -1,14 +1,21 @@
 /* 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/. */
 
 #PopupSearchAutoComplete {
-  margin-inline-start: -24px;
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using ctrl+shift+X) */
+  margin-inline-start: -23px;
+  margin-inline-end: -16px;
   padding: 1px;
 }
 
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
--- a/browser/themes/osx/searchbar.css
+++ b/browser/themes/osx/searchbar.css
@@ -1,14 +1,21 @@
 /* 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/. */
 
 #PopupSearchAutoComplete {
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using command+shift+X) */
   margin-inline-start: -23px;
+  margin-inline-end: -21px;
 }
 
 .searchbar-textbox {
   border-radius: 10000px;
 }
 
 .searchbar-popup {
   margin-top: 4px;
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -1,14 +1,21 @@
 /* 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/. */
 
 #PopupSearchAutoComplete {
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using ctrl+shift+X) */
   margin-inline-start: -25px;
+  margin-inline-end: -18px;
 }
 
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
   margin: 0;
--- a/config/milestone.txt
+++ b/config/milestone.txt
@@ -5,9 +5,9 @@
 #    x.x.x.x
 #    x.x.x+
 #
 # Referenced by milestone.py.
 # Hopefully I'll be able to automate replacement of *all*
 # hardcoded milestones in the tree from these two files.
 #--------------------------------------------------------
 
-49.0a1
+50.0a1
--- a/devtools/client/eyedropper/test/browser.ini
+++ b/devtools/client/eyedropper/test/browser.ini
@@ -5,8 +5,9 @@ support-files =
   color-block.html
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_eyedropper_basic.js]
 skip-if = os == "win" && debug # bug 963492
 [browser_eyedropper_cmd.js]
+skip-if = true # bug 1278400
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -17,16 +17,18 @@ const ELLIPSIS = Services.prefs.getCompl
     Ci.nsIPrefLocalizedString).data;
 const MAX_LABEL_LENGTH = 40;
 
 const NS_XHTML = "http://www.w3.org/1999/xhtml";
 const SCROLL_REPEAT_MS = 100;
 
 loader.lazyRequireGetter(this, "EventEmitter",
                          "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "KeyShortcuts",
+                         "devtools/client/shared/key-shortcuts", true);
 
 /**
  * Component to replicate functionality of XUL arrowscrollbox
  * for breadcrumbs
  *
  * @param {Window} win The window containing the breadcrumbs
  * @parem {DOMNode} container The element in which to put the scroll box
  */
@@ -341,21 +343,28 @@ HTMLBreadcrumbs.prototype = {
     this.separators.className = "breadcrumb-separator-container";
     this.separators.innerHTML =
                       "<div id='breadcrumb-separator-before'></div>" +
                       "<div id='breadcrumb-separator-after'></div>" +
                       "<div id='breadcrumb-separator-normal'></div>";
     this.container.parentNode.appendChild(this.separators);
 
     this.outer.addEventListener("click", this, true);
-    this.outer.addEventListener("keypress", this, true);
     this.outer.addEventListener("mouseover", this, true);
     this.outer.addEventListener("mouseleave", this, true);
     this.outer.addEventListener("focus", this, true);
 
+    this.shortcuts = new KeyShortcuts({ window: this.chromeWin, target: this.outer });
+    this.handleShortcut = this.handleShortcut.bind(this);
+
+    this.shortcuts.on("Right", this.handleShortcut);
+    this.shortcuts.on("Left", this.handleShortcut);
+    this.shortcuts.on("Tab", this.handleShortcut);
+    this.shortcuts.on("Shift+Tab", this.handleShortcut);
+
     // We will save a list of already displayed nodes in this array.
     this.nodeHierarchy = [];
 
     // Last selected node in nodeHierarchy.
     this.currentIndex = -1;
 
     this.update = this.update.bind(this);
     this.updateSelectors = this.updateSelectors.bind(this);
@@ -464,18 +473,16 @@ HTMLBreadcrumbs.prototype = {
 
   /**
    * Generic event handler.
    * @param {DOMEvent} event.
    */
   handleEvent: function (event) {
     if (event.type == "click" && event.button == 0) {
       this.handleClick(event);
-    } else if (event.type == "keypress" && this.selection.isElementNode()) {
-      this.handleKeyPress(event);
     } else if (event.type == "mouseover") {
       this.handleMouseOver(event);
     } else if (event.type == "mouseleave") {
       this.handleMouseLeave(event);
     } else if (event.type == "focus") {
       this.handleFocus(event);
     }
   },
@@ -527,86 +534,71 @@ HTMLBreadcrumbs.prototype = {
    * On mouse leave, make sure to unhighlight.
    * @param {DOMEvent} event.
    */
   handleMouseLeave: function (event) {
     this.inspector.toolbox.highlighterUtils.unhighlight();
   },
 
   /**
-   * On keypress, navigate through the list of breadcrumbs with the left/right
-   * arrow keys.
-   * @param {DOMEvent} event.
+   * Handle a keyboard shortcut supported by the breadcrumbs widget.
+   *
+   * @param {String} name
+   *        Name of the keyboard shortcut received.
+   * @param {DOMEvent} event
+   *        Original event that triggered the shortcut.
    */
-  handleKeyPress: function (event) {
-    let win = this.chromeWin;
-    let {keyCode, shiftKey, metaKey, ctrlKey, altKey} = event;
-
-    // Only handle left, right, tab and shift tab, let anything else bubble up
-    // so native shortcuts work.
-    let hasModifier = metaKey || ctrlKey || altKey || shiftKey;
-    let isLeft = keyCode === win.KeyEvent.DOM_VK_LEFT && !hasModifier;
-    let isRight = keyCode === win.KeyEvent.DOM_VK_RIGHT && !hasModifier;
-    let isTab = keyCode === win.KeyEvent.DOM_VK_TAB && !hasModifier;
-    let isShiftTab = keyCode === win.KeyEvent.DOM_VK_TAB && shiftKey &&
-                     !metaKey && !ctrlKey && !altKey;
-
-    if (!isLeft && !isRight && !isTab && !isShiftTab) {
+  handleShortcut: function (name, event) {
+    if (!this.selection.isElementNode()) {
       return;
     }
 
     event.preventDefault();
     event.stopPropagation();
 
     this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => {
-      if (isLeft && this.currentIndex != 0) {
+      if (name === "Left" && this.currentIndex != 0) {
         let node = this.nodeHierarchy[this.currentIndex - 1].node;
         return this.selection.setNodeFront(node, "breadcrumbs");
-      } else if (isRight && this.currentIndex < this.nodeHierarchy.length - 1) {
+      } else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) {
         let node = this.nodeHierarchy[this.currentIndex + 1].node;
         return this.selection.setNodeFront(node, "breadcrumbs");
-      } else if (isTab || isShiftTab) {
-        // Tabbing when breadcrumbs or its contents are focused should move
-        // focus to next/previous focusable element relative to breadcrumbs
-        // themselves.
-        let elm, type;
-        if (shiftKey) {
-          elm = this.container;
-          type = FocusManager.MOVEFOCUS_BACKWARD;
-        } else {
-          // To move focus to next element following the breadcrumbs, relative
-          // element needs to be the last element in breadcrumbs' subtree.
-          let last = this.container.lastChild;
-          while (last && last.lastChild) {
-            last = last.lastChild;
-          }
-          elm = last;
-          type = FocusManager.MOVEFOCUS_FORWARD;
+      } else if (name === "Tab") {
+        // To move focus to next element following the breadcrumbs, relative
+        // element needs to be the last element in breadcrumbs' subtree.
+        let last = this.container.lastChild;
+        while (last && last.lastChild) {
+          last = last.lastChild;
         }
-        FocusManager.moveFocus(win, elm, type, 0);
+        FocusManager.moveFocus(this.chromeWin, last, FocusManager.MOVEFOCUS_FORWARD, 0);
+      } else if (name === "Shift+Tab") {
+        // Tabbing when breadcrumbs or its contents are focused should move focus to
+        // previous focusable element relative to breadcrumbs themselves.
+        let elt = this.container;
+        FocusManager.moveFocus(this.chromeWin, elt, FocusManager.MOVEFOCUS_BACKWARD, 0);
       }
 
       return null;
     });
   },
 
   /**
    * Remove nodes and clean up.
    */
   destroy: function () {
     this.selection.off("new-node-front", this.update);
     this.selection.off("pseudoclass", this.updateSelectors);
     this.selection.off("attribute-changed", this.updateSelectors);
     this.inspector.off("markupmutation", this.update);
 
     this.container.removeEventListener("click", this, true);
-    this.container.removeEventListener("keypress", this, true);
     this.container.removeEventListener("mouseover", this, true);
     this.container.removeEventListener("mouseleave", this, true);
     this.container.removeEventListener("focus", this, true);
+    this.shortcuts.destroy();
 
     this.empty();
     this.separators.remove();
 
     this.arrowScrollBox.off("overflow", this.scroll);
     this.arrowScrollBox.destroy();
     this.arrowScrollBox = null;
     this.outer = null;
@@ -678,23 +670,16 @@ HTMLBreadcrumbs.prototype = {
    */
   buildButton: function (node) {
     let button = this.chromeDoc.createElementNS(NS_XHTML, "button");
     button.appendChild(this.prettyPrintNodeAsXHTML(node));
     button.className = "breadcrumbs-widget-item";
 
     button.setAttribute("title", this.prettyPrintNodeAsText(node));
 
-    button.onkeypress = function onBreadcrumbsKeypress(e) {
-      if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
-          e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
-        button.click();
-      }
-    };
-
     button.onclick = () => {
       button.focus();
     };
 
     button.onBreadcrumbsClick = () => {
       this.selection.setNodeFront(node, "breadcrumbs");
     };
 
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -270,57 +270,59 @@
                 <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
                 <html:pre class="font-css-code"></html:pre>
               </html:div>
             </html:section>
           </html:div>
         </tabpanel>
 
         <tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div id="layout-container">
-            <html:p id="layout-header">
-              <html:span id="layout-element-size"></html:span>
-              <html:section id="layout-position-group">
-                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
-                <html:span id="layout-element-position"></html:span>
-              </html:section>
-            </html:p>
+          <html:div id="layout-wrapper">
+            <html:div id="layout-container">
+              <html:p id="layout-header">
+                <html:span id="layout-element-size"></html:span>
+                <html:section id="layout-position-group">
+                  <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+                  <html:span id="layout-element-position"></html:span>
+                </html:section>
+              </html:p>
 
-            <html:div id="layout-main">
-              <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
-              <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
-                <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
-                <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
-                  <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
-                  <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
-                    <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+              <html:div id="layout-main">
+                <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
+                <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
+                  <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
+                  <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
+                    <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
+                    <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
+                      <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+                      </html:div>
                     </html:div>
                   </html:div>
                 </html:div>
+
+                <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
+                <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
+                <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
+                <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
+
+                <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
+                <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
+                <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
+                <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
+
+                <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
+                <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
+                <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
+                <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
+
+                <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
               </html:div>
 
-              <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
-              <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
-              <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
-              <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
-
-              <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
-              <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
-              <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
-              <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
-
-              <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
-              <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
-              <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
-              <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
-
-              <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
-            </html:div>
-
-            <html:div style="display: none">
-              <html:p id="layout-dummy"></html:p>
+              <html:div style="display: none">
+                <html:p id="layout-dummy"></html:p>
+              </html:div>
             </html:div>
           </html:div>
         </tabpanel>
       </tabpanels>
     </tabbox>
   </box>
 </window>
--- a/devtools/client/inspector/layout/test/browser.ini
+++ b/devtools/client/inspector/layout/test/browser.ini
@@ -10,16 +10,17 @@ support-files =
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_layout.js]
 [browser_layout_editablemodel.js]
 # [browser_layout_editablemodel_allproperties.js]
 # Disabled for too many intermittent failures (bug 1009322)
+[browser_layout_editablemodel_bluronclick.js]
 [browser_layout_editablemodel_border.js]
 [browser_layout_editablemodel_stylerules.js]
 [browser_layout_guides.js]
 [browser_layout_rotate-labels-on-sides.js]
 [browser_layout_tooltips.js]
 [browser_layout_update-after-navigation.js]
 [browser_layout_update-after-reload.js]
 # [browser_layout_update-in-iframes.js]
--- a/devtools/client/inspector/layout/test/browser_layout_editablemodel.js
+++ b/devtools/client/inspector/layout/test/browser_layout_editablemodel.js
@@ -8,32 +8,29 @@
 // key bindings
 
 const TEST_URI = "<style>" +
   "div { margin: 10px; padding: 3px }" +
   "#div1 { margin-top: 5px }" +
   "#div2 { border-bottom: 1em solid black; }" +
   "#div3 { padding: 2em; }" +
   "#div4 { margin: 1px; }" +
-  "#div5 { margin: 1px; }" +
   "</style>" +
   "<div id='div1'></div><div id='div2'></div>" +
-  "<div id='div3'></div><div id='div4'></div>" +
-  "<div id='div5'></div>";
+  "<div id='div3'></div><div id='div4'></div>";
 
 add_task(function* () {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {inspector, view, testActor} = yield openLayoutView();
 
   yield testEditingMargins(inspector, view, testActor);
   yield testKeyBindings(inspector, view, testActor);
   yield testEscapeToUndo(inspector, view, testActor);
   yield testDeletingValue(inspector, view, testActor);
   yield testRefocusingOnClick(inspector, view, testActor);
-  yield testBluringOnClick(inspector, view);
 });
 
 function* testEditingMargins(inspector, view, testActor) {
   info("Test that editing margin dynamically updates the document, pressing " +
        "escape cancels the changes");
 
   is((yield getStyle(testActor, "#div1", "margin-top")), "",
      "Should be no margin-top on the element.");
@@ -190,29 +187,8 @@ function* testRefocusingOnClick(inspecto
   is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
      "Should have updated the margin.");
   EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
 
   is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
      "Should be the right margin-top on the element.");
   is(span.textContent, 2, "Should have the right value in the box model.");
 }
-
-function* testBluringOnClick(inspector, view) {
-  info("Test that clicking outside the editor blurs it");
-
-  yield selectNode("#div5", inspector);
-
-  let span = view.doc.querySelector(".layout-margin.layout-top > span");
-  is(span.textContent, 1, "Should have the right value in the box model.");
-
-  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
-  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
-  ok(editor, "Should have opened the editor.");
-
-  info("Click next to the opened editor input.");
-  let rect = editor.getBoundingClientRect();
-  EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
-    view.doc.defaultView);
-
-  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
-    "Inplace editor has been removed.");
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/test/browser_layout_editablemodel_bluronclick.js
@@ -0,0 +1,73 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that inplace editors can be blurred by clicking outside of the editor.
+
+const TEST_URI =
+  `<style>
+    #div1 {
+      margin: 10px;
+      padding: 3px;
+    }
+  </style>
+  <div id="div1"></div>`;
+
+add_task(function* () {
+  // Make sure the toolbox is tall enough to have empty space below the layout-container.
+  yield pushPref("devtools.toolbox.footer.height", 500);
+
+  yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openLayoutView();
+
+  yield selectNode("#div1", inspector);
+  yield testClickingOutsideEditor(view);
+  yield testClickingBelowContainer(view);
+});
+
+function* testClickingOutsideEditor(view) {
+  info("Test that clicking outside the editor blurs it");
+  let span = view.doc.querySelector(".layout-margin.layout-top > span");
+  is(span.textContent, 10, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+
+  info("Click next to the opened editor input.");
+  let onBlur = once(editor, "blur");
+  let rect = editor.getBoundingClientRect();
+  EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
+    view.doc.defaultView);
+  yield onBlur;
+
+  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+    "Inplace editor has been removed.");
+}
+
+function* testClickingBelowContainer(view) {
+  info("Test that clicking below the box-model container blurs it");
+  let span = view.doc.querySelector(".layout-margin.layout-top > span");
+  is(span.textContent, 10, "Should have the right value in the box model.");
+
+  info("Test that clicking below the layout-container blurs the opened editor");
+  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+
+  let onBlur = once(editor, "blur");
+  let container = view.doc.querySelector("#layout-container");
+  // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse)
+  // use an erroneous height of ~50px for the layout container.
+  let bounds = container.getBoxQuads({relativeTo: view.doc})[0].bounds;
+  EventUtils.synthesizeMouseAtPoint(
+    bounds.left + 10,
+    bounds.top + bounds.height + 10,
+    {}, view.doc.defaultView);
+  yield onBlur;
+
+  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+    "Inplace editor has been removed.");
+}
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -59,16 +59,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_markup_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markup_anonymous_03.js]
 [browser_markup_anonymous_04.js]
 [browser_markup_copy_image_data.js]
 subsuite = clipboard
 [browser_markup_css_completion_style_attribute_01.js]
 [browser_markup_css_completion_style_attribute_02.js]
+[browser_markup_css_completion_style_attribute_03.js]
 [browser_markup_dragdrop_autoscroll.js]
 [browser_markup_dragdrop_distance.js]
 [browser_markup_dragdrop_draggable.js]
 [browser_markup_dragdrop_dragRootNode.js]
 [browser_markup_dragdrop_escapeKeyPress.js]
 [browser_markup_dragdrop_invalidNodes.js]
 [browser_markup_dragdrop_reorder.js]
 [browser_markup_dragdrop_tooltip.js]
--- a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
@@ -65,13 +65,12 @@ const TEST_DATA = [
    45, 55, false],
   ["VK_RIGHT", "style=\"display:  inherit; color :chartreuse !important; ",
    55, 55, false],
   ["VK_RETURN", "style=\"display:  inherit; color :chartreuse !important;\"",
    -1, -1, false]
 ];
 
 add_task(function* () {
-  info("Opening the inspector on the test URL");
   let {inspector} = yield openInspectorForURL(TEST_URL);
 
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
 });
--- a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
@@ -93,15 +93,14 @@ const TEST_DATA_INNER = [
   ["c", "style=\"background:url('1'); color", 29, 33, true],
   ["VK_RIGHT", "style=\"background:url('1'); color", 33, 33, false],
   [":", "style=\"background:url('1'); color:aliceblue", 34, 43, true],
   ["b", "style=\"background:url('1'); color:beige", 35, 39, true],
   ["VK_RETURN", "style=\"background:url('1'); color:beige\"", -1, -1, false]
 ];
 
 add_task(function* () {
-  info("Opening the inspector on the test URL");
   let {inspector} = yield openInspectorForURL(TEST_URL);
 
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_DOUBLE);
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_SINGLE);
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_INNER);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js
@@ -0,0 +1,54 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_style_attr_test_runner.js */
+
+"use strict";
+
+// Test CSS autocompletion of the style attribute can be triggered when the
+// caret is before a non-word character.
+
+loadHelperScript("helper_style_attr_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+
+// test data format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    expected input.selectionStart,
+//    expected input.selectionEnd,
+//    is popup expected to be open ?
+//  ]
+const TEST_DATA = [
+  ["s", "s", 1, 1, false],
+  ["t", "st", 2, 2, false],
+  ["y", "sty", 3, 3, false],
+  ["l", "styl", 4, 4, false],
+  ["e", "style", 5, 5, false],
+  ["=", "style=", 6, 6, false],
+  ["\"", "style=\"", 7, 7, false],
+  ["\"", "style=\"\"", 8, 8, false],
+  ["VK_LEFT", "style=\"\"", 7, 7, false],
+  ["c", "style=\"color\"", 8, 12, true],
+  ["o", "style=\"color\"", 9, 12, true],
+  ["VK_RIGHT", "style=\"color\"", 12, 12, false],
+  [":", "style=\"color:aliceblue\"", 13, 22, true],
+  ["b", "style=\"color:beige\"", 14, 18, true],
+  ["VK_RIGHT", "style=\"color:beige\"", 18, 18, false],
+  [";", "style=\"color:beige;\"", 19, 19, false],
+  [";", "style=\"color:beige;;\"", 20, 20, false],
+  ["VK_LEFT", "style=\"color:beige;;\"", 19, 19, false],
+  ["p", "style=\"color:beige;padding;\"", 20, 26, true],
+  ["VK_RIGHT", "style=\"color:beige;padding;\"", 26, 26, false],
+  [":", "style=\"color:beige;padding:calc;\"", 27, 31, true],
+  ["0", "style=\"color:beige;padding:0;\"", 28, 28, false],
+  ["VK_RETURN", "style=\"color:beige;padding:0;\"",
+   -1, -1, false]
+];
+
+add_task(function* () {
+  let {inspector} = yield openInspectorForURL(TEST_URL);
+
+  yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
+});
--- a/devtools/client/locales/en-US/netmonitor.dtd
+++ b/devtools/client/locales/en-US/netmonitor.dtd
@@ -34,16 +34,20 @@
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.file): This is the label displayed
   -  in the network table toolbar, above the "file" column. -->
 <!ENTITY netmonitorUI.toolbar.file        "File">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.domain): This is the label displayed
   -  in the network table toolbar, above the "domain" column. -->
 <!ENTITY netmonitorUI.toolbar.domain      "Domain">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.cause): This is the label displayed
+  -  in the network table toolbar, above the "cause" column. -->
+<!ENTITY netmonitorUI.toolbar.cause        "Cause">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
   -  in the network table toolbar, above the "type" column. -->
 <!ENTITY netmonitorUI.toolbar.type        "Type">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.transferred): This is the label displayed
   -  in the network table toolbar, above the "transferred" column, which is the
   -  compressed / encoded size. -->
 <!ENTITY netmonitorUI.toolbar.transferred "Transferred">
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -428,16 +428,23 @@ var NetMonitorController = {
 
   /**
    * Getter that tells if the server can do network performance statistics.
    * @type boolean
    */
   get supportsPerfStats() {
     return this.tabClient &&
            (this.tabClient.traits.reconfigure || !this._target.isApp);
+  },
+
+  /**
+   * Open a given source in Debugger
+   */
+  viewSourceInDebugger(sourceURL, sourceLine) {
+    return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
   }
 };
 
 /**
  * Functions handling target-related lifetime events.
  */
 function TargetEventsHandler() {
   this._onTabNavigated = this._onTabNavigated.bind(this);
@@ -624,22 +631,24 @@ NetworkEventsHandler.prototype = {
    * @param object networkInfo
    *        The network request information.
    */
   _onNetworkEvent: function (type, networkInfo) {
     let { actor,
       startedDateTime,
       request: { method, url },
       isXHR,
+      cause,
       fromCache,
       fromServiceWorker
     } = networkInfo;
 
     NetMonitorView.RequestsMenu.addRequest(
-      actor, startedDateTime, method, url, isXHR, fromCache, fromServiceWorker
+      actor, startedDateTime, method, url, isXHR, cause, fromCache,
+        fromServiceWorker
     );
     window.emit(EVENTS.NETWORK_EVENT, actor);
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -25,17 +25,19 @@ const {LocalizationHelper} = require("de
 const {PrefsHelper} = require("devtools/client/shared/prefs");
 const {ViewHelpers, Heritage, WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
 
 /**
  * Localization convenience methods.
  */
 const NET_STRINGS_URI = "chrome://devtools/locale/netmonitor.properties";
+const WEBCONSOLE_STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
 var L10N = new LocalizationHelper(NET_STRINGS_URI);
+const WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI);
 
 // ms
 const WDA_DEFAULT_VERIFY_INTERVAL = 50;
 
 // Use longer timeout during testing as the tests need this process to succeed
 // and two seconds is quite short on slow debug builds. The timeout here should
 // be at least equal to the general mochitest timeout of 45 seconds so that this
 // never gets hit during testing.
@@ -56,16 +58,18 @@ const HTML_NS = "http://www.w3.org/1999/
 const EPSILON = 0.001;
 // 100 KB in bytes
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
 // ms
 const RESIZE_REFRESH_RATE = 50;
 // ms
 const REQUESTS_REFRESH_RATE = 50;
 const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 // px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
 // px
 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
 // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
 // px
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
@@ -97,16 +101,41 @@ const CONTENT_MIME_TYPE_MAPPINGS = {
   "/xml": Editor.modes.html,
   "/atom": Editor.modes.html,
   "/soap": Editor.modes.html,
   "/vnd.mpeg.dash.mpd": Editor.modes.html,
   "/rdf": Editor.modes.css,
   "/rss": Editor.modes.css,
   "/css": Editor.modes.css
 };
+const LOAD_CAUSE_STRINGS = {
+  [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+  [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+  [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+  [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+  [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+  [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+  [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+  [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+  [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh",
+  [Ci.nsIContentPolicy.TYPE_XBL]: "xbl",
+  [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+  [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+  [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+  [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+  [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+  [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+  [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+  [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+  [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+  [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+  [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+  [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+  [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest"
+};
 const DEFAULT_EDITOR_CONFIG = {
   mode: Editor.modes.text,
   readOnly: true,
   lineNumbers: true
 };
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   // ms
@@ -426,16 +455,30 @@ RequestsMenuView.prototype = Heritage.ex
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"));
     this._splitter = $("#network-inspector-view-splitter");
     this._summary = $("#requests-menu-network-summary-button");
     this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
     this.userInputTimer = Cc["@mozilla.org/timer;1"]
       .createInstance(Ci.nsITimer);
 
+    // Create a tooltip for the newly appended network request item.
+    this.tooltip = new Tooltip(document, {
+      closeOnEvents: [{
+        emitter: $("#requests-menu-contents"),
+        event: "scroll",
+        useCapture: true
+      }]
+    });
+    this.tooltip.startTogglingOnHover(this.widget, this._onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+    this.tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
+
     Prefs.filters.forEach(type => this.filterOn(type));
     this.sortContents(this._byTiming);
 
     this.allowFocusOnRightClick = true;
     this.maintainSelectionVisible = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
     this.widget.addEventListener("swap", this._onSwap, false);
@@ -632,25 +675,30 @@ RequestsMenuView.prototype = Heritage.ex
    *        A string representation of when the request was started, which
    *        can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
    * @param string method
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string url
    *        Specifies the request's url.
    * @param boolean isXHR
    *        True if this request was initiated via XHR.
+   * @param object cause
+   *        Specifies the request's cause. Has the following properties:
+   *        - type: nsContentPolicyType constant
+   *        - loadingDocumentUri: URI of the request origin
+   *        - stacktrace: JS stacktrace of the request
    * @param boolean fromCache
    *        Indicates if the result came from the browser cache
    * @param boolean fromServiceWorker
    *        Indicates if the request has been intercepted by a Service Worker
    */
-  addRequest: function (id, startedDateTime, method, url, isXHR, fromCache,
-    fromServiceWorker) {
-    this._addQueue.push([id, startedDateTime, method, url, isXHR, fromCache,
-      fromServiceWorker]);
+  addRequest: function (id, startedDateTime, method, url, isXHR, cause,
+    fromCache, fromServiceWorker) {
+    this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
+      fromCache, fromServiceWorker]);
 
     // Lazy updating is disabled in some tests.
     if (!this.lazyUpdate) {
       return void this._flushRequests();
     }
 
     this._flushRequestsTask.arm();
     return undefined;
@@ -880,17 +928,18 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
   cloneSelectedRequest: function () {
     let selected = this.selectedItem.attachment;
 
     // Create the element node for the network request item.
-    let menuView = this._createMenuView(selected.method, selected.url);
+    let menuView = this._createMenuView(selected.method, selected.url,
+      selected.cause);
 
     // Append a network request item to this container.
     let newItem = this.push([menuView], {
       attachment: Object.create(selected, {
         isCustom: { value: true }
       })
     });
 
@@ -1447,29 +1496,16 @@ RequestsMenuView.prototype = Heritage.ex
       } else {
         requestTarget.setAttribute("odd", "");
         requestTarget.removeAttribute("even");
       }
     }
   },
 
   /**
-   * Refreshes the toggling anchor for the specified item's tooltip.
-   *
-   * @param object item
-   *        The network request item in this container.
-   */
-  refreshTooltip: function (item) {
-    let tooltip = item.attachment.tooltip;
-    tooltip.hide();
-    tooltip.startTogglingOnHover(item.target, this._onHover);
-    tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
-  },
-
-  /**
    * Attaches security icon click listener for the given request menu item.
    *
    * @param object item
    *        The network request item to attach the listener to.
    */
   attachSecurityIconClickListener: function ({ target }) {
     let icon = $(".requests-security-state-icon", target);
     icon.addEventListener("click", this._onSecurityIconClick);
@@ -1505,52 +1541,42 @@ RequestsMenuView.prototype = Heritage.ex
     // Prevent displaying any updates received after the target closed.
     if (NetMonitorView._isDestroyed) {
       return;
     }
 
     let widget = NetMonitorView.RequestsMenu.widget;
     let isScrolledToBottom = widget.isScrolledToBottom();
 
-    for (let [id, startedDateTime, method, url, isXHR, fromCache,
+    for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
       fromServiceWorker] of this._addQueue) {
       // Convert the received date/time string to a unix timestamp.
       let unixTime = Date.parse(startedDateTime);
 
       // Create the element node for the network request item.
-      let menuView = this._createMenuView(method, url);
+      let menuView = this._createMenuView(method, url, cause);
 
       // Remember the first and last event boundaries.
       this._registerFirstRequestStart(unixTime);
       this._registerLastRequestEnd(unixTime);
 
       // Append a network request item to this container.
       let requestItem = this.push([menuView, id], {
         attachment: {
           startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
           startedMillis: unixTime,
           method: method,
           url: url,
           isXHR: isXHR,
+          cause: cause,
           fromCache: fromCache,
           fromServiceWorker: fromServiceWorker
         }
       });
 
-      // Create a tooltip for the newly appended network request item.
-      requestItem.attachment.tooltip = new Tooltip(document, {
-        closeOnEvents: [{
-          emitter: $("#requests-menu-contents"),
-          event: "scroll",
-          useCapture: true
-        }]
-      });
-
-      this.refreshTooltip(requestItem);
-
       if (id == this._preferredItemId) {
         this.selectedItem = requestItem;
       }
 
       window.emit(EVENTS.REQUEST_ADDED, id);
     }
 
     if (isScrolledToBottom && this._addQueue.length) {
@@ -1749,31 +1775,36 @@ RequestsMenuView.prototype = Heritage.ex
 
   /**
    * Customization function for creating an item's UI.
    *
    * @param string method
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string url
    *        Specifies the request's url.
+   * @param object cause
+   *        Specifies the request's cause. Has two properties:
+   *        - type: nsContentPolicyType constant
+   *        - uri: URI of the request origin
    * @return nsIDOMNode
    *         The network request view.
    */
-  _createMenuView: function (method, url) {
+  _createMenuView: function (method, url, cause) {
     let template = $("#requests-menu-item-template");
     let fragment = document.createDocumentFragment();
 
-    this.updateMenuView(template, "method", method);
-    this.updateMenuView(template, "url", url);
-
     // Flatten the DOM by removing one redundant box (the template container).
     for (let node of template.childNodes) {
       fragment.appendChild(node.cloneNode(true));
     }
 
+    this.updateMenuView(fragment, "method", method);
+    this.updateMenuView(fragment, "url", url);
+    this.updateMenuView(fragment, "cause", cause);
+
     return fragment;
   },
 
   /**
    * Get a human-readable string from a number of bytes, with the B, KB, MB, or
    * GB value. Note that the transition between abbreviations is by 1000 rather
    * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
    * more awkward than 0.99 MB"
@@ -1895,16 +1926,30 @@ RequestsMenuView.prototype = Heritage.ex
         codeNode.setAttribute("value", value.status);
         break;
       }
       case "statusText": {
         let node = $(".requests-menu-status", target);
         node.setAttribute("tooltiptext", value);
         break;
       }
+      case "cause": {
+        let labelNode = $(".requests-menu-cause-label", target);
+        let text = LOAD_CAUSE_STRINGS[value.type] || "unknown";
+        labelNode.setAttribute("value", text);
+        if (value.loadingDocumentUri) {
+          labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
+        }
+
+        let stackNode = $(".requests-menu-cause-stack", target);
+        if (value.stacktrace && value.stacktrace.length > 0) {
+          stackNode.removeAttribute("hidden");
+        }
+        break;
+      }
       case "contentSize": {
         let node = $(".requests-menu-size", target);
 
         let text = this.getFormattedSize(value);
 
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", text);
         break;
@@ -2225,21 +2270,16 @@ RequestsMenuView.prototype = Heritage.ex
     }
   },
 
   /**
    * The swap listener for this container.
    * Called when two items switch places, when the contents are sorted.
    */
   _onSwap: function ({ detail: [firstItem, secondItem] }) {
-    // Sorting will create new anchor nodes for all the swapped request items
-    // in this container, so it's necessary to refresh the Tooltip instances.
-    this.refreshTooltip(firstItem);
-    this.refreshTooltip(secondItem);
-
     // Reattach click listener to the security icons
     this.attachSecurityIconClickListener(firstItem);
     this.attachSecurityIconClickListener(secondItem);
   },
 
   /**
    * The predicate used when deciding whether a popup should be shown
    * over a request item or not.
@@ -2247,36 +2287,100 @@ RequestsMenuView.prototype = Heritage.ex
    * @param nsIDOMNode target
    *        The element node currently being hovered.
    * @param object tooltip
    *        The current tooltip instance.
    * @return {Promise}
    */
   _onHover: Task.async(function* (target, tooltip) {
     let requestItem = this.getItemForElement(target);
-    if (!requestItem || !requestItem.attachment.responseContent) {
+    if (!requestItem) {
       return false;
     }
 
     let hovered = requestItem.attachment;
-    let { mimeType, text, encoding } = hovered.responseContent.content;
-
-    if (mimeType && mimeType.includes("image/") && (
-      target.classList.contains("requests-menu-icon") ||
-      target.classList.contains("requests-menu-file"))) {
-      let string = yield gNetwork.getString(text);
-      let anchor = $(".requests-menu-icon", requestItem.target);
-      let src = formDataURI(mimeType, encoding, string);
-
-      tooltip.setImageContent(src, {
-        maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM
-      });
-      return anchor;
+    if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
+      return this._setTooltipImageContent(tooltip, requestItem);
+    } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
+      return this._setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  }),
+
+  _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
+    let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
+
+    if (!mimeType || !mimeType.includes("image/")) {
+      return false;
+    }
+
+    let string = yield gNetwork.getString(text);
+    let anchor = $(".requests-menu-icon", requestItem.target);
+    let src = formDataURI(mimeType, encoding, string);
+
+    tooltip.setImageContent(src, {
+      maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM
+    });
+
+    return anchor;
+  }),
+
+  _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
+    let {stacktrace} = requestItem.attachment.cause;
+
+    if (!stacktrace || stacktrace.length == 0) {
+      return false;
     }
-    return false;
+
+    let doc = tooltip.doc;
+    let el = doc.createElement("vbox");
+    el.className = "requests-menu-stack-trace";
+
+    for (let f of stacktrace) {
+      let { functionName, filename, lineNumber, columnNumber } = f;
+
+      let frameEl = doc.createElement("hbox");
+      frameEl.className = "requests-menu-stack-frame devtools-monospace";
+
+      let funcEl = doc.createElement("label");
+      funcEl.className = "requests-menu-stack-frame-function-name";
+      funcEl.setAttribute("value",
+        functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction"));
+      frameEl.appendChild(funcEl);
+
+      let fileEl = doc.createElement("label");
+      fileEl.className = "requests-menu-stack-frame-file-name";
+      // Parse a stack frame in format "url -> url"
+      let sourceUrl = filename.split(" -> ").pop();
+      fileEl.setAttribute("value", sourceUrl);
+      fileEl.setAttribute("tooltiptext", sourceUrl);
+      fileEl.setAttribute("crop", "start");
+      frameEl.appendChild(fileEl);
+
+      let lineEl = doc.createElement("label");
+      lineEl.className = "requests-menu-stack-frame-line";
+      lineEl.setAttribute("value", `:${lineNumber}:${columnNumber}`);
+      frameEl.appendChild(lineEl);
+
+      frameEl.addEventListener("click", () => {
+        // avoid an ugly visual artefact when the view is switched to debugger and the
+        // tooltip is hidden only after a delay - the tooltip is moved outside the browser
+        // window.
+        tooltip.hide();
+        NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+      }, false);
+
+      el.appendChild(frameEl);
+    }
+
+    tooltip.content = el;
+    tooltip.panel.setAttribute("wide", "");
+
+    return true;
   }),
 
   /**
    * A handler that opens the security tab in the details view if secure or
    * broken security indicator is clicked.
    */
   _onSecurityIconClick: function (e) {
     let state = this.selectedItem.attachment.securityState;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -216,16 +216,27 @@
                 <button id="requests-menu-domain-button"
                         class="requests-menu-header-button requests-menu-security-and-domain"
                         data-key="domain"
                         label="&netmonitorUI.toolbar.domain;"
                         crop="end"
                         flex="1">
                 </button>
               </hbox>
+              <hbox id="requests-menu-cause-header-box"
+                    class="requests-menu-header requests-menu-cause"
+                    align="center">
+                <button id="requests-menu-cause-button"
+                        class="requests-menu-header-button requests-menu-cause"
+                        data-key="cause"
+                        label="&netmonitorUI.toolbar.cause;"
+                        crop="end"
+                        flex="1">
+                </button>
+              </hbox>
               <hbox id="requests-menu-type-header-box"
                     class="requests-menu-header requests-menu-type"
                     align="center">
                 <button id="requests-menu-type-button"
                         class="requests-menu-header-button requests-menu-type"
                         data-key="type"
                         label="&netmonitorUI.toolbar.type;"
                         crop="end"
@@ -318,16 +329,20 @@
               </hbox>
               <hbox class="requests-menu-subitem requests-menu-security-and-domain"
                     align="center">
                 <image class="requests-security-state-icon" />
                 <label class="plain requests-menu-domain"
                        crop="end"
                        flex="1"/>
               </hbox>
+              <hbox class="requests-menu-subitem requests-menu-cause" align="center">
+                <label class="requests-menu-cause-stack" value="JS" hidden="true"/>
+                <label class="plain requests-menu-cause-label" flex="1" crop="end"/>
+              </hbox>
               <label class="plain requests-menu-subitem requests-menu-type"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-transferred"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-size"
                      crop="end"/>
               <hbox class="requests-menu-subitem requests-menu-waterfall"
                     align="center"
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -11,16 +11,17 @@ const { Task } = require("devtools/share
 
 function NetMonitorPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
 
   this._view = this.panelWin.NetMonitorView;
   this._controller = this.panelWin.NetMonitorController;
   this._controller._target = this.target;
+  this._controller._toolbox = this._toolbox;
 
   EventEmitter.decorate(this);
 }
 
 exports.NetMonitorPanel = NetMonitorPanel;
 
 NetMonitorPanel.prototype = {
   /**
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
+  html_cause-test-page.html
   html_content-type-test-page.html
   html_content-type-without-cache-test-page.html
   html_cors-test-page.html
   html_custom-get-page.html
   html_single-get-page.html
   html_cyrillic-test-page.html
   html_filter-test-page.html
   html_infinite-get-page.html
@@ -28,30 +29,33 @@ support-files =
   html_statistics-test-page.html
   html_status-codes-test-page.html
   html_api-calls-test-page.html
   html_copy-as-curl.html
   html_curl-utils.html
   sjs_content-type-test-server.sjs
   sjs_cors-test-server.sjs
   sjs_https-redirect-test-server.sjs
+  sjs_hsts-test-server.sjs
   sjs_simple-test-server.sjs
   sjs_sorting-test-server.sjs
   sjs_status-codes-test-server.sjs
   test-image.png
   service-workers/status-codes.html
   service-workers/status-codes-service-worker.js
 
 [browser_net_aaa_leaktest.js]
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
 skip-if = (toolkit == "cocoa" && e10s) # bug 1252254
 [browser_net_api-calls.js]
 [browser_net_autoscroll.js]
 [browser_net_cached-status.js]
+[browser_net_cause.js]
+[browser_net_cause_redirect.js]
 [browser_net_service-worker-status.js]
 [browser_net_charts-01.js]
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
 [browser_net_charts-05.js]
 [browser_net_charts-06.js]
 [browser_net_charts-07.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request cause is reported correctly.
+ */
+
+const CAUSE_FILE_NAME = "html_cause-test-page.html";
+const CAUSE_URL = EXAMPLE_URL + CAUSE_FILE_NAME;
+
+const EXPECTED_REQUESTS = [
+  {
+    method: "GET",
+    url: CAUSE_URL,
+    causeType: "document",
+    causeUri: "",
+    // The document load is from JS function in e10s, native in non-e10s
+    hasStack: !gMultiProcessBrowser
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "stylesheet_request",
+    causeType: "stylesheet",
+    causeUri: CAUSE_URL,
+    hasStack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "img_request",
+    causeType: "img",
+    causeUri: CAUSE_URL,
+    hasStack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "xhr_request",
+    causeType: "xhr",
+    causeUri: CAUSE_URL,
+    hasStack: { fn: "performXhrRequest", file: CAUSE_FILE_NAME, line: 22 }
+  },
+  {
+    method: "POST",
+    url: EXAMPLE_URL + "beacon_request",
+    causeType: "beacon",
+    causeUri: CAUSE_URL,
+    hasStack: { fn: "performBeaconRequest", file: CAUSE_FILE_NAME, line: 26 }
+  },
+];
+
+var test = Task.async(function* () {
+  // the initNetMonitor function clears the network request list after the
+  // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+  // and only then load the real thing from CAUSE_URL - we want to catch
+  // all the requests the page is making, not only the XHRs.
+  // We can't use about:blank here, because initNetMonitor checks that the
+  // page has actually made at least one request.
+  let [, debuggee, monitor] = yield initNetMonitor(SIMPLE_URL);
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  debuggee.location = CAUSE_URL;
+
+  yield waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+
+  is(RequestsMenu.itemCount, EXPECTED_REQUESTS.length,
+    "All the page events should be recorded.");
+
+  EXPECTED_REQUESTS.forEach((spec, i) => {
+    let { method, url, causeType, causeUri, hasStack } = spec;
+
+    let requestItem = RequestsMenu.getItemAtIndex(i);
+    verifyRequestItemTarget(requestItem,
+      method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+    );
+
+    let { stacktrace } = requestItem.attachment.cause;
+    let stackLen = stacktrace ? stacktrace.length : 0;
+
+    if (hasStack) {
+      ok(stacktrace, `Request #${i} has a stacktrace`);
+      ok(stackLen > 0,
+        `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+      // if "hasStack" is object, check the details about the top stack frame
+      if (typeof hasStack === "object") {
+        is(stacktrace[0].functionName, hasStack.fn,
+          `Request #${i} has the correct function on top of the JS stack`);
+        is(stacktrace[0].filename.split("/").pop(), hasStack.file,
+          `Request #${i} has the correct file on top of the JS stack`);
+        is(stacktrace[0].lineNumber, hasStack.line,
+          `Request #${i} has the correct line number on top of the JS stack`);
+      }
+    } else {
+      is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+    }
+  });
+
+  yield teardown(monitor);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request JS stack is property reported if the request is internally
+ * redirected without hitting the network (HSTS is one of such cases)
+ */
+
+var test = Task.async(function* () {
+  const EXPECTED_REQUESTS = [
+    // Request to HTTP URL, redirects to HTTPS, has callstack
+    { status: 302, hasStack: true },
+    // Serves HTTPS, sets the Strict-Transport-Security header, no stack
+    { status: 200, hasStack: false },
+    // Second request to HTTP redirects to HTTPS internally
+    { status: 200, hasStack: true },
+  ];
+
+  let [, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
+  let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  debuggee.performRequests(2, HSTS_SJS);
+  yield waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+
+  EXPECTED_REQUESTS.forEach(({status, hasStack}, i) => {
+    let { attachment } = RequestsMenu.getItemAtIndex(i);
+
+    is(attachment.status, status, `Request #${i} has the expected status`);
+
+    let { stacktrace } = attachment.cause;
+    let stackLen = stacktrace ? stacktrace.length : 0;
+
+    if (hasStack) {
+      ok(stacktrace, `Request #${i} has a stacktrace`);
+      ok(stackLen > 0, `Request #${i} has a stacktrace with ${stackLen} items`);
+    } else {
+      is(stackLen, 0, `Request #${i} has an empty stacktrace`);
+    }
+  });
+
+  // Send a request to reset the HSTS policy to state before the test
+  debuggee.performRequests(1, HSTS_SJS + "?reset");
+  yield waitForNetworkEvents(monitor, 1);
+
+  yield teardown(monitor);
+  finish();
+});
--- a/devtools/client/netmonitor/test/browser_net_image-tooltip.js
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -18,42 +18,39 @@ add_task(function* test() {
   let onEvents = waitForNetworkEvents(monitor, 7);
   let onThumbnail = waitFor(monitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
 
   debuggee.performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a few requests were made...");
-  yield showTooltipAndVerify(RequestsMenu.items[5]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[5]);
 
   // 7 XHRs as before + 1 extra document reload
   onEvents = waitForNetworkEvents(monitor, 8);
   onThumbnail = waitFor(monitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
 
   info("Reloading the debuggee and performing all requests again...");
   yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   debuggee.performRequests();
   yield onEvents;
   yield onThumbnail;
 
   info("Checking the image thumbnail after a reload.");
-  yield showTooltipAndVerify(RequestsMenu.items[6]);
+  yield showTooltipAndVerify(RequestsMenu.tooltip, RequestsMenu.items[6]);
 
   yield teardown(monitor);
   finish();
 
   /**
    * Show a tooltip on the {requestItem} and verify that it was displayed
    * with the expected content.
    */
-  function* showTooltipAndVerify(requestItem) {
-    let { tooltip } = requestItem.attachment;
-    ok(tooltip, "There should be a tooltip instance for the image request.");
-
+  function* showTooltipAndVerify(tooltip, requestItem) {
     let anchor = $(".requests-menu-file", requestItem.target);
     yield showTooltipOn(tooltip, anchor);
 
     info("Tooltip was successfully opened for the image request.");
     is(tooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
       "The tooltip's image content is displayed correctly.");
   }
 
--- a/devtools/client/netmonitor/test/browser_net_service-worker-status.js
+++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.js
@@ -8,48 +8,64 @@
  */
 
 // Service workers only work on https
 const URL = EXAMPLE_URL.replace("http:", "https:");
 
 const TEST_URL = URL + "service-workers/status-codes.html";
 
 var test = Task.async(function* () {
-  let [tab, debuggee, monitor] = yield initNetMonitor(TEST_URL, null, true);
+  let [, debuggee, monitor] = yield initNetMonitor(TEST_URL, null, true);
   info("Starting test... ");
 
-  let { document, L10N, NetMonitorView } = monitor.panelWin;
-  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   const REQUEST_DATA = [
     {
       method: "GET",
       uri: URL + "service-workers/test/200",
       details: {
         status: 200,
         statusText: "OK (service worker)",
         displayedStatus: "service worker",
         type: "plain",
         fullMimeType: "text/plain; charset=UTF-8"
-      }
+      },
+      stackFunctions: ["doXHR", "performRequests"]
     },
   ];
 
   info("Registering the service worker...");
   yield debuggee.registerServiceWorker();
 
   info("Performing requests...");
   debuggee.performRequests();
   yield waitForNetworkEvents(monitor, REQUEST_DATA.length);
 
   let index = 0;
   for (let request of REQUEST_DATA) {
     let item = RequestsMenu.getItemAtIndex(index);
 
-    info("Verifying request #" + index);
+    info(`Verifying request #${index}`);
     yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
 
+    let { stacktrace } = item.attachment.cause;
+    let stackLen = stacktrace ? stacktrace.length : 0;
+
+    ok(stacktrace, `Request #${index} has a stacktrace`);
+    ok(stackLen >= request.stackFunctions.length,
+      `Request #${index} has a stacktrace with enough (${stackLen}) items`);
+
+    request.stackFunctions.forEach((functionName, j) => {
+      is(stacktrace[j].functionName, functionName,
+      `Request #${index} has the correct function at position #${j} on the stack`);
+    });
+
     index++;
   }
 
+  info("Unregistering the service worker...");
+  yield debuggee.unregisterServiceWorker();
+
   yield teardown(monitor);
   finish();
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -45,16 +45,20 @@ const SEND_BEACON_URL = EXAMPLE_URL + "h
 const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
 const CORS_SJS_PATH = "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs";
+const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs";
+
+const HSTS_BASE_URL = EXAMPLE_URL;
+const HSTS_PAGE_URL = CUSTOM_GET_URL;
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
 
 DevToolsUtils.testing = true;
 SimpleTest.registerCleanupFunction(() => {
@@ -279,17 +283,17 @@ function verifyRequestItemTarget(aReques
 
   let requestsMenu = aRequestItem.ownerView;
   let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
   let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
 
   info("Widget index of item: " + widgetIndex);
   info("Visible index of item: " + visibleIndex);
 
-  let { fuzzyUrl, status, statusText, type, fullMimeType,
+  let { fuzzyUrl, status, statusText, cause, type, fullMimeType,
         transferred, size, time, displayedStatus } = aData;
   let { attachment, target } = aRequestItem;
 
   let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
   let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aUrl));
   let name = NetworkHelper.convertToUnicode(unescape(uri.fileName || uri.filePath || "/"));
   let query = NetworkHelper.convertToUnicode(unescape(uri.query));
   let hostPort = uri.hostPort;
@@ -331,16 +335,25 @@ function verifyRequestItemTarget(aReques
     let tooltip = target.querySelector(".requests-menu-status").getAttribute("tooltiptext");
     info("Displayed status: " + value);
     info("Displayed code: " + codeValue);
     info("Tooltip status: " + tooltip);
     is(value, displayedStatus ? displayedStatus : status, "The displayed status is correct.");
     is(codeValue, status, "The displayed status code is correct.");
     is(tooltip, status + " " + statusText, "The tooltip status is correct.");
   }
+  if (cause !== undefined) {
+    let causeLabel = target.querySelector(".requests-menu-cause-label");
+    let value = causeLabel.getAttribute("value");
+    let tooltip = causeLabel.getAttribute("tooltiptext");
+    info("Displayed cause: " + value);
+    info("Tooltip cause: " + tooltip);
+    is(value, cause.type, "The displayed cause is correct.");
+    is(tooltip, cause.loadingDocumentUri, "The tooltip cause is correct.")
+  }
   if (type !== undefined) {
     let value = target.querySelector(".requests-menu-type").getAttribute("value");
     let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
     info("Displayed type: " + value);
     info("Tooltip type: " + tooltip);
     is(value, type, "The displayed type is correct.");
     is(tooltip, fullMimeType, "The tooltip type is correct.");
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_cause-test-page.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <title>Network Monitor test page</title>
+    <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+  </head>
+
+  <body>
+    <p>Request cause test</p>
+    <img src="img_request" />
+    <script type="text/javascript">
+      function performXhrRequest() {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", "xhr_request", true);
+        xhr.send();
+      }
+
+      function performBeaconRequest() {
+        navigator.sendBeacon("beacon_request");
+      }
+
+      performXhrRequest();
+      performBeaconRequest();
+    </script>
+  </body>
+</html>
--- a/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
+++ b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js
@@ -1,8 +1,15 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-addEventListener("fetch", function (event) {
+"use strict";
+
+self.addEventListener("activate", event => {
+  // start controlling the already loaded page
+  event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("fetch", event => {
   let response = new Response("Service worker response");
   event.respondWith(response);
 });
--- a/devtools/client/netmonitor/test/service-workers/status-codes.html
+++ b/devtools/client/netmonitor/test/service-workers/status-codes.html
@@ -10,30 +10,50 @@
     <meta http-equiv="Expires" content="0" />
     <title>Network Monitor test page</title>
   </head>
 
   <body>
     <p>Status codes test</p>
 
     <script type="text/javascript">
-      function get(url) {
-        return new Promise(done => {
-          let iframe = document.createElement("iframe");
-          iframe.setAttribute("src", url);
-          document.documentElement.appendChild(iframe);
-          iframe.contentWindow.onload = done;
-        });
+      let swRegistration;
+
+      function registerServiceWorker() {
+        let sw = navigator.serviceWorker;
+        return sw.register("status-codes-service-worker.js")
+          .then(registration => {
+            swRegistration = registration;
+            console.log("Registered, scope is:", registration.scope);
+            return sw.ready;
+          }).then(() => {
+            // wait until the page is controlled
+            return new Promise(resolve => {
+              if (sw.controller) {
+                resolve();
+              } else {
+                sw.addEventListener('controllerchange', function onControllerChange() {
+                  sw.removeEventListener('controllerchange', onControllerChange);
+                  resolve();
+                });
+              }
+            });
+          }).catch(err => {
+            console.error("Registration failed");
+          });
       }
 
-      function registerServiceWorker() {
-        return navigator.serviceWorker.register("status-codes-service-worker.js")
-                        .then(() => navigator.serviceWorker.ready);
+      function unregisterServiceWorker() {
+        return swRegistration.unregister();
       }
 
       function performRequests() {
-        return get("test/200");
+        return new Promise(function doXHR(done) {
+          let xhr = new XMLHttpRequest();
+          xhr.open("GET", "test/200", true);
+          xhr.onreadystatechange = done;
+          xhr.send(null);
+        });
       }
-
     </script>
   </body>
 
 </html>
--- a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
+++ b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs
@@ -73,17 +73,17 @@ function handleRequest(request, response
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
         setCacheHeaders();
         response.write("<label value='greeting'>Hello XML!</label>");
         response.finish();
         break;
       }
       case "html": {
-        let content = params.filter((s) => s.includes("res="))[0].split("=")[1];
+        let content = (params.filter((s) => s.includes("res="))[0] || "").split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
         setCacheHeaders();
         response.write(content || "<p>Hello HTML!</p>");
         response.finish();
         break;
       }
       case "html-long": {
@@ -115,26 +115,26 @@ function handleRequest(request, response
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/json; charset=utf-8", false);
         setCacheHeaders();
         response.write("{ \"greeting\": \"Hello JSON!\" }");
         response.finish();
         break;
       }
       case "jsonp": {
-        let fun = params.filter((s) => s.includes("jsonp="))[0].split("=")[1];
+        let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
         setCacheHeaders();
         response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
         response.finish();
         break;
       }
       case "jsonp2": {
-        let fun = params.filter((s) => s.includes("jsonp="))[0].split("=")[1];
+        let fun = (params.filter((s) => s.includes("jsonp="))[0] || "").split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
         setCacheHeaders();
         response.write(" " + fun + " ( { \"greeting\": \"Hello weird JSONP!\" } ) ; ");
         response.finish();
         break;
       }
       case "json-long": {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+  response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+  response.setHeader("Pragma", "no-cache");
+  response.setHeader("Expires", "0");
+
+  if (request.queryString === "reset") {
+    // Reset the HSTS policy, prevent influencing other tests
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.setHeader("Strict-Transport-Security", "max-age=0");
+    response.write("Resetting HSTS");
+  } else if (request.scheme === "http") {
+    response.setStatusLine(request.httpVersion, 302, "Found");
+    response.setHeader("Location", "https://" + request.host + request.path);
+  } else {
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.setHeader("Strict-Transport-Security", "max-age=100");
+    response.write("Page was accessed over HTTPS!");
+  }
+}
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -1292,24 +1292,28 @@ InplaceEditor.prototype = {
       if (input.value.length - preTimeoutQuery.length > 1) {
         return;
       }
       let query = input.value.slice(0, input.selectionStart);
       let startCheckQuery = query;
       if (query == null) {
         return;
       }
-      // If nothing is selected and there is a non-space character after the
-      // cursor, do not autocomplete.
+      // If nothing is selected and there is a word (\w) character after the cursor, do
+      // not autocomplete.
       if (input.selectionStart == input.selectionEnd &&
-          input.selectionStart < input.value.length &&
-          input.value.slice(input.selectionStart)[0] != " ") {
-        // This emit is mainly to make the test flow simpler.
-        this.emit("after-suggest", "nothing to autocomplete");
-        return;
+          input.selectionStart < input.value.length) {
+        let nextChar = input.value.slice(input.selectionStart)[0];
+        // Check if the next character is a valid word character, no suggestion should be
+        // provided when preceeding a word.
+        if (/[\w-]/.test(nextChar)) {
+          // This emit is mainly to make the test flow simpler.
+          this.emit("after-suggest", "nothing to autocomplete");
+          return;
+        }
       }
       let list = [];
       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
         list = this._getCSSPropertyList();
       } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
         // Get the last query to be completed before the caret.
         let match = /([^\s,.\/]+$)/.exec(query);
         if (match) {
--- a/devtools/client/shared/key-shortcuts.js
+++ b/devtools/client/shared/key-shortcuts.js
@@ -45,16 +45,17 @@ const ElectronKeysMapping = {
   "Left": "DOM_VK_LEFT",
   "Right": "DOM_VK_RIGHT",
   "Home": "DOM_VK_HOME",
   "End": "DOM_VK_END",
   "PageUp": "DOM_VK_PAGE_UP",
   "PageDown": "DOM_VK_PAGE_DOWN",
   "Escape": "DOM_VK_ESCAPE",
   "Esc": "DOM_VK_ESCAPE",
+  "Tab": "DOM_VK_TAB",
   "VolumeUp": "DOM_VK_VOLUME_UP",
   "VolumeDown": "DOM_VK_VOLUME_DOWN",
   "VolumeMute": "DOM_VK_VOLUME_MUTE",
   "PrintScreen": "DOM_VK_PRINTSCREEN",
 };
 
 /**
  * Helper to listen for keyboard events decribed in .properties file.
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -2,16 +2,22 @@
  * 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/ */
 
 #sidebar-panel-layoutview {
   display: block;
   overflow: auto;
 }
 
+#layout-wrapper {
+  /* The sidebar-panel is not focusable, this wrapper will catch click events in
+     all the empty area around the layout-container */
+  height: 100%;
+}
+
 #layout-container {
   /* The view will grow bigger as the window gets resized, until 400px */
   max-width: 400px;
   margin: 0px auto;
   padding: 0;
   /* "Contain" the absolutely positioned #layout-main element */
   position: relative;
 }
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -241,16 +241,35 @@
 
 .requests-menu-type,
 .requests-menu-size {
   max-width: 6em;
   text-align: center;
   width: 8vw;
 }
 
+.requests-menu-cause {
+  max-width: 8em;
+  width: 8vw;
+}
+
+.requests-menu-cause-stack {
+  background-color: var(--theme-body-color-alt);
+  color: var(--theme-body-background);
+  font-size: 8px;
+  font-weight: bold;
+  line-height: 10px;
+  border-radius: 3px;
+  padding: 0 2px;
+  margin: 0;
+  margin-inline-end: 3px;
+  -moz-user-select: none;
+  cursor: pointer;
+}
+
 .requests-menu-transferred {
   max-width: 8em;
   text-align: center;
   width: 8vw;
 }
 
 /* Network requests table: status codes */
 
@@ -671,16 +690,50 @@
   background-color: var(--theme-selection-background-semitransparent);
 }
 
 .requests-menu-filter-button:not(:active)[checked] {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
+/* Requests menu stacktrace tooltip */
+.requests-menu-stack-trace {
+  max-height: 400px;
+  width: 586px;
+  overflow-y: auto;
+}
+
+.requests-menu-stack-frame {
+  color: var(--theme-body-color-alt);
+  cursor: pointer;
+  display: flex;
+}
+
+.requests-menu-stack-frame:hover {
+  background-color: var(--theme-selection-background-semitransparent);
+}
+
+.requests-menu-stack-frame-function-name {
+  color: var(--theme-highlight-blue);
+  cursor: inherit;
+  flex-grow: 1;
+}
+
+.requests-menu-stack-frame-file-name {
+  cursor: inherit;
+  margin-inline-end: 0;
+}
+
+.requests-menu-stack-frame-line {
+  color: var(--theme-highlight-orange);
+  cursor: inherit;
+  margin-inline-start: 0;
+}
+
 /* Performance analysis buttons */
 
 #requests-menu-network-summary-button {
   background: none;
   box-shadow: none;
   border-color: transparent;
   list-style-image: url(images/profiler-stopwatch.svg);
   padding-inline-end: 0;
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -13,16 +13,17 @@ const { EnvironmentActor } = require("de
 const { ThreadActor } = require("devtools/server/actors/script");
 const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const ErrorDocs = require("devtools/server/actors/errordocs");
 
 loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
+loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true);
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
@@ -593,30 +594,34 @@ WebConsoleActor.prototype =
             this.consoleAPIListener =
               new ConsoleAPIListener(window, this);
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           if (!this.networkMonitor) {
+            // Create a StackTraceCollector that's going to be shared both by the
+            // NetworkMonitorChild (getting messages about requests from parent) and
+            // by the NetworkMonitor that directly watches service workers requests.
+            this.stackTraceCollector = new StackTraceCollector({ window, appId });
+            this.stackTraceCollector.init();
+
             if (appId || messageManager) {
               // Start a network monitor in the parent process to listen to
               // most requests than happen in parent
               this.networkMonitor =
                 new NetworkMonitorChild(appId, messageManager,
                                         this.parentActor.actorID, this);
               this.networkMonitor.init();
               // Spawn also one in the child to listen to service workers
-              this.networkMonitorChild = new NetworkMonitor({ window: window },
-                                                            this);
+              this.networkMonitorChild = new NetworkMonitor({ window }, this);
               this.networkMonitorChild.init();
-            }
-            else {
-              this.networkMonitor = new NetworkMonitor({ window: window }, this);
+            } else {
+              this.networkMonitor = new NetworkMonitor({ window }, this);
               this.networkMonitor.init();
             }
           }
           startedListeners.push(listener);
           break;
         case "FileActivity":
           if (this.window instanceof Ci.nsIDOMWindow) {
             if (!this.consoleProgressListener) {
@@ -695,16 +700,20 @@ WebConsoleActor.prototype =
           if (this.networkMonitor) {
             this.networkMonitor.destroy();
             this.networkMonitor = null;
           }
           if (this.networkMonitorChild) {
             this.networkMonitorChild.destroy();
             this.networkMonitorChild = null;
           }
+          if (this.stackTraceCollector) {
+            this.stackTraceCollector.destroy();
+            this.stackTraceCollector = null;
+          }
           stoppedListeners.push(listener);
           break;
         case "FileActivity":
           if (this.consoleProgressListener) {
             this.consoleProgressListener.stopMonitor(this.consoleProgressListener.
                                                      MONITOR_FILE_ACTIVITY);
             this.consoleProgressListener = null;
           }
@@ -1823,16 +1832,17 @@ NetworkEventActor.prototype =
   {
     return {
       actor: this.actorID,
       startedDateTime: this._startedDateTime,
       timeStamp: Date.parse(this._startedDateTime),
       url: this._request.url,
       method: this._request.method,
       isXHR: this._isXHR,
+      cause: this._cause,
       fromCache: this._fromCache,
       fromServiceWorker: this._fromServiceWorker,
       private: this._private,
     };
   },
 
   /**
    * Releases this actor from the pool.
@@ -1868,16 +1878,17 @@ NetworkEventActor.prototype =
    *
    * @param object aNetworkEvent
    *        The network event associated with this actor.
    */
   init: function NEA_init(aNetworkEvent)
   {
     this._startedDateTime = aNetworkEvent.startedDateTime;
     this._isXHR = aNetworkEvent.isXHR;
+    this._cause = aNetworkEvent.cause;
     this._fromCache = aNetworkEvent.fromCache;
     this._fromServiceWorker = aNetworkEvent.fromServiceWorker;
 
     for (let prop of ["method", "url", "httpVersion", "headersSize"]) {
       this._request[prop] = aNetworkEvent[prop];
     }
 
     this._discardRequestBody = aNetworkEvent.discardRequestBody;
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -95,16 +95,17 @@ WebConsoleClient.prototype = {
         discardRequestBody: true,
         discardResponseBody: true,
         startedDateTime: actor.startedDateTime,
         request: {
           url: actor.url,
           method: actor.method,
         },
         isXHR: actor.isXHR,
+        cause: actor.cause,
         response: {},
         timings: {},
         // track the list of network event updates
         updates: [],
         private: actor.private,
         fromCache: actor.fromCache,
         fromServiceWorker: actor.fromServiceWorker
       };
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
-const {Cc, Ci, Cu, Cr} = require("chrome");
+const {Cc, Ci, Cm, Cu, Cr, components} = require("chrome");
 const Services = require("Services");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
                          "devtools/shared/webconsole/network-helper");
 loader.lazyRequireGetter(this, "DevToolsUtils",
                          "devtools/shared/DevToolsUtils");
@@ -32,16 +32,246 @@ const HTTP_MOVED_PERMANENTLY = 301;
 const HTTP_FOUND = 302;
 const HTTP_SEE_OTHER = 303;
 const HTTP_TEMPORARY_REDIRECT = 307;
 
 // The maximum number of bytes a NetworkResponseListener can hold: 1 MB
 const RESPONSE_BODY_LIMIT = 1048576;
 
 /**
+ * Check if a given network request should be logged by a network monitor
+ * based on the specified filters.
+ *
+ * @param nsIHttpChannel channel
+ *        Request to check.
+ * @param filters
+ *        NetworkMonitor filters to match against.
+ * @return boolean
+ *         True if the network request should be logged, false otherwise.
+ */
+function matchRequest(channel, filters) {
+  // Log everything if no filter is specified
+  if (!filters.topFrame && !filters.window && !filters.appId) {
+    return true;
+  }
+
+  // Ignore requests from chrome or add-on code when we are monitoring
+  // content.
+  // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
+  // the DevToolsUtils.testing check. We will move to a better way to serve
+  // its needs in bug 1167188, where this check should be removed.
+  if (!DevToolsUtils.testing && channel.loadInfo &&
+      channel.loadInfo.loadingDocument === null &&
+      channel.loadInfo.loadingPrincipal ===
+      Services.scriptSecurityManager.getSystemPrincipal()) {
+    return false;
+  }
+
+  if (filters.window) {
+    // Since frames support, this.window may not be the top level content
+    // frame, so that we can't only compare with win.top.
+    let win = NetworkHelper.getWindowForRequest(channel);
+    while (win) {
+      if (win == filters.window) {
+        return true;
+      }
+      if (win.parent == win) {
+        break;
+      }
+      win = win.parent;
+    }
+  }
+
+  if (filters.topFrame) {
+    let topFrame = NetworkHelper.getTopFrameForRequest(channel);
+    if (topFrame && topFrame === filters.topFrame) {
+      return true;
+    }
+  }
+
+  if (filters.appId) {
+    let appId = NetworkHelper.getAppIdForRequest(channel);
+    if (appId && appId == filters.appId) {
+      return true;
+    }
+  }
+
+  // The following check is necessary because beacon channels don't come
+  // associated with a load group. Bug 1160837 will hopefully introduce a
+  // platform fix that will render the following code entirely useless.
+  if (channel.loadInfo &&
+      channel.loadInfo.externalContentPolicyType ==
+      Ci.nsIContentPolicy.TYPE_BEACON) {
+    let nonE10sMatch = filters.window &&
+        channel.loadInfo.loadingDocument === filters.window.document;
+    const loadingPrincipal = channel.loadInfo.loadingPrincipal;
+    let e10sMatch = filters.topFrame &&
+        filters.topFrame.contentPrincipal &&
+        filters.topFrame.contentPrincipal.equals(loadingPrincipal) &&
+        filters.topFrame.contentPrincipal.URI.spec == channel.referrer.spec;
+    let b2gMatch = filters.appId && loadingPrincipal.appId === filters.appId;
+    if (nonE10sMatch || e10sMatch || b2gMatch) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/**
+ * This is a nsIChannelEventSink implementation that monitors channel redirects and
+ * informs the registered StackTraceCollector about the old and new channels.
+ */
+const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
+const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
+
+function ChannelEventSink() {
+  this.wrappedJSObject = this;
+  this.collectors = new Set();
+}
+
+ChannelEventSink.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
+
+  registerCollector(collector) {
+    this.collectors.add(collector);
+  },
+
+  unregisterCollector(collector) {
+    this.collectors.delete(collector);
+
+    if (this.collectors.size == 0) {
+      ChannelEventSinkFactory.unregister();
+    }
+  },
+
+  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+    for (let collector of this.collectors) {
+      try {
+        collector.onChannelRedirect(oldChannel, newChannel, flags);
+      } catch (ex) {
+        console.error("StackTraceCollector.onChannelRedirect threw an exception", ex);
+      }
+    }
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  }
+};
+
+const ChannelEventSinkFactory = XPCOMUtils.generateSingletonFactory(ChannelEventSink);
+
+ChannelEventSinkFactory.register = function () {
+  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
+    return;
+  }
+
+  registrar.registerFactory(SINK_CLASS_ID,
+                            SINK_CLASS_DESCRIPTION,
+                            SINK_CONTRACT_ID,
+                            ChannelEventSinkFactory);
+
+  XPCOMUtils.categoryManager.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+    SINK_CONTRACT_ID, false, true);
+};
+
+ChannelEventSinkFactory.unregister = function () {
+  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);
+
+  XPCOMUtils.categoryManager.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+    false);
+};
+
+ChannelEventSinkFactory.getService = function () {
+  // Make sure the ChannelEventSink service is registered before accessing it
+  ChannelEventSinkFactory.register();
+
+  return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject;
+};
+
+function StackTraceCollector(filters) {
+  this.filters = filters;
+  this.stacktracesById = new Map();
+}
+
+StackTraceCollector.prototype = {
+  init() {
+    Services.obs.addObserver(this, "http-on-opening-request", false);
+    ChannelEventSinkFactory.getService().registerCollector(this);
+  },
+
+  destroy() {
+    Services.obs.removeObserver(this, "http-on-opening-request");
+    ChannelEventSinkFactory.getService().unregisterCollector(this);
+  },
+
+  _saveStackTrace(channel, stacktrace) {
+    this.stacktracesById.set(channel.channelId, stacktrace);
+  },
+
+  observe(subject) {
+    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+    if (!matchRequest(channel, this.filters)) {
+      return;
+    }
+
+    // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
+    // passed around through message managers etc.
+    let frame = components.stack;
+    let stacktrace = [];
+    if (frame && frame.caller) {
+      frame = frame.caller;
+      while (frame) {
+        stacktrace.push({
+          filename: frame.filename,
+          lineNumber: frame.lineNumber,
+          columnNumber: frame.columnNumber,
+          functionName: frame.name
+        });
+        if (frame.asyncCaller) {
+          frame = frame.asyncCaller;
+        } else {
+          frame = frame.caller;
+        }
+      }
+    }
+
+    this._saveStackTrace(channel, stacktrace);
+  },
+
+  onChannelRedirect(oldChannel, newChannel, flags) {
+    // We can be called with any nsIChannel, but are interested only in HTTP channels
+    try {
+      oldChannel.QueryInterface(Ci.nsIHttpChannel);
+      newChannel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      return;
+    }
+
+    let oldId = oldChannel.channelId;
+    let stacktrace = this.stacktracesById.get(oldId);
+    if (stacktrace) {
+      this.stacktracesById.delete(oldId);
+      this._saveStackTrace(newChannel, stacktrace);
+    }
+  },
+
+  getStackTrace(channelId) {
+    let trace = this.stacktracesById.get(channelId);
+    this.stacktracesById.delete(channelId);
+    return trace;
+  }
+};
+
+exports.StackTraceCollector = StackTraceCollector;
+
+/**
  * The network response listener implements the nsIStreamListener and
  * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature
  * to get the response body of the request.
  *
  * The code is mostly based on code listings from:
  *
  *   http://www.softwareishard.com/blog/firebug/
  *      nsitraceablechannel-intercept-http-traffic/
@@ -58,17 +288,16 @@ function NetworkResponseListener(owner, 
   this.owner = owner;
   this.receivedData = "";
   this.httpActivity = httpActivity;
   this.bodySize = 0;
   let channel = this.httpActivity.channel;
   this._wrappedNotificationCallbacks = channel.notificationCallbacks;
   channel.notificationCallbacks = this;
 }
-exports.NetworkResponseListener = NetworkResponseListener;
 
 NetworkResponseListener.prototype = {
   QueryInterface:
     XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
                            Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor,
                            Ci.nsISupports]),
 
   // nsIInterfaceRequestor implementation
@@ -457,56 +686,48 @@ NetworkResponseListener.prototype = {
  * requests. The nsIObserverService is also used for monitoring
  * http-on-examine-response notifications. All network request information is
  * routed to the remote Web Console.
  *
  * @constructor
  * @param object filters
  *        Object with the filters to use for network requests:
  *        - window (nsIDOMWindow): filter network requests by the associated
- *        window object.
+ *          window object.
  *        - appId (number): filter requests by the appId.
  *        - topFrame (nsIDOMElement): filter requests by their topFrameElement.
  *        Filters are optional. If any of these filters match the request is
  *        logged (OR is applied). If no filter is provided then all requests are
  *        logged.
  * @param object owner
  *        The network monitor owner. This object needs to hold:
  *        - onNetworkEvent(requestInfo, channel, networkMonitor).
- *        This method is invoked once for every new network request and it is
- *        given the following arguments: the initial network request
- *        information, and the channel. The third argument is the NetworkMonitor
- *        instance.
- *        onNetworkEvent() must return an object which holds several add*()
- *        methods which are used to add further network request/response
- *        information.
+ *          This method is invoked once for every new network request and it is
+ *          given the following arguments: the initial network request
+ *          information, and the channel. The third argument is the NetworkMonitor
+ *          instance. onNetworkEvent() must return an object which holds several add*()
+ *          methods which are used to add further network request/response
+ *          information.
+ *        - stackTraceCollector If the owner has this optional property, it will
+ *          be used as a StackTraceCollector by the NetworkMonitor.
  */
 function NetworkMonitor(filters, owner) {
-  if (filters) {
-    this.window = filters.window;
-    this.appId = filters.appId;
-    this.topFrame = filters.topFrame;
-  }
-  if (!this.window && !this.appId && !this.topFrame) {
-    this._logEverything = true;
-  }
+  this.filters = filters;
   this.owner = owner;
   this.openRequests = {};
   this.openResponses = {};
   this._httpResponseExaminer =
     DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
   this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
 }
+
 exports.NetworkMonitor = NetworkMonitor;
 
 NetworkMonitor.prototype = {
-  _logEverything: false,
-  window: null,
-  appId: null,
-  topFrame: null,
+  filters: null,
 
   httpTransactionCodes: {
     0x5001: "REQUEST_HEADER",
     0x5002: "REQUEST_BODY_SENT",
     0x5003: "RESPONSE_START",
     0x5004: "RESPONSE_HEADER",
     0x5005: "RESPONSE_COMPLETE",
     0x5006: "TRANSACTION_CLOSE",
@@ -561,17 +782,17 @@ NetworkMonitor.prototype = {
     // everything else only happens in the parent process
     Services.obs.addObserver(this._serviceWorkerRequest,
                              "service-worker-synthesized-response", false);
   },
 
   _serviceWorkerRequest: function (subject, topic, data) {
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
 
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
     this.interceptedChannels.add(subject);
 
     // On e10s, we never receive http-on-examine-cached-response, so fake one.
     if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
       this._httpResponseExaminer(channel, "http-on-examine-cached-response");
@@ -597,17 +818,17 @@ NetworkMonitor.prototype = {
         (topic != "http-on-examine-response" &&
          topic != "http-on-examine-cached-response") ||
         !(subject instanceof Ci.nsIHttpChannel)) {
       return;
     }
 
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
 
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
     let response = {
       id: gSequenceId(),
       channel: channel,
       headers: [],
       cookies: [],
@@ -752,94 +973,16 @@ NetworkMonitor.prototype = {
         this._onTransactionClose(httpActivity);
         break;
       default:
         break;
     }
   }),
 
   /**
-   * Check if a given network request should be logged by this network monitor
-   * instance based on the current filters.
-   *
-   * @private
-   * @param nsIHttpChannel channel
-   *        Request to check.
-   * @return boolean
-   *         True if the network request should be logged, false otherwise.
-   */
-  _matchRequest: function (channel) {
-    if (this._logEverything) {
-      return true;
-    }
-
-    // Ignore requests from chrome or add-on code when we are monitoring
-    // content.
-    // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
-    // the DevToolsUtils.testing check. We will move to a better way to serve
-    // its needs in bug 1167188, where this check should be removed.
-    if (!DevToolsUtils.testing && channel.loadInfo &&
-        channel.loadInfo.loadingDocument === null &&
-        channel.loadInfo.loadingPrincipal ===
-        Services.scriptSecurityManager.getSystemPrincipal()) {
-      return false;
-    }
-
-    if (this.window) {
-      // Since frames support, this.window may not be the top level content
-      // frame, so that we can't only compare with win.top.
-      let win = NetworkHelper.getWindowForRequest(channel);
-      while (win) {
-        if (win == this.window) {
-          return true;
-        }
-        if (win.parent == win) {
-          break;
-        }
-        win = win.parent;
-      }
-    }
-
-    if (this.topFrame) {
-      let topFrame = NetworkHelper.getTopFrameForRequest(channel);
-      if (topFrame && topFrame === this.topFrame) {
-        return true;
-      }
-    }
-
-    if (this.appId) {
-      let appId = NetworkHelper.getAppIdForRequest(channel);
-      if (appId && appId == this.appId) {
-        return true;
-      }
-    }
-
-    // The following check is necessary because beacon channels don't come
-    // associated with a load group. Bug 1160837 will hopefully introduce a
-    // platform fix that will render the following code entirely useless.
-    if (channel.loadInfo &&
-        channel.loadInfo.externalContentPolicyType ==
-        Ci.nsIContentPolicy.TYPE_BEACON) {
-      let nonE10sMatch = this.window &&
-          channel.loadInfo.loadingDocument === this.window.document;
-      const loadingPrincipal = channel.loadInfo.loadingPrincipal;
-      let e10sMatch = this.topFrame &&
-          this.topFrame.contentPrincipal &&
-          this.topFrame.contentPrincipal.equals(loadingPrincipal) &&
-          this.topFrame.contentPrincipal.URI.spec == channel.referrer.spec;
-      let b2gMatch = this.appId && loadingPrincipal.appId === this.appId;
-      if (nonE10sMatch || e10sMatch || b2gMatch) {
-        return true;
-      }
-    }
-
-    return false;
-  },
-
-  /**
    *
    */
   _createNetworkEvent: function (channel, { timestamp, extraStringData,
                                            fromCache, fromServiceWorker }) {
     let win = NetworkHelper.getWindowForRequest(channel);
     let httpActivity = this.createActivityObject(channel);
 
     // see _onRequestBodySent()
@@ -852,36 +995,51 @@ NetworkMonitor.prototype = {
       httpActivity.timings.REQUEST_HEADER = {
         first: timestamp,
         last: timestamp
       };
     }
 
     let event = {};
     event.method = channel.requestMethod;
+    event.channelId = channel.channelId;
     event.url = channel.URI.spec;
     event.private = httpActivity.private;
     event.headersSize = 0;
     event.startedDateTime =
       (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date())
       .toISOString();
     event.fromCache = fromCache;
     event.fromServiceWorker = fromServiceWorker;
     httpActivity.fromServiceWorker = fromServiceWorker;
 
     if (extraStringData) {
       event.headersSize = extraStringData.length;
     }
 
-    // Determine if this is an XHR request.
+    // Determine the cause and if this is an XHR request.
+    let causeType = channel.loadInfo.externalContentPolicyType;
+    let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+    let causeUri = loadingPrincipal ? loadingPrincipal.URI : null;
+    let stacktrace;
+    // If this is the parent process, there is no stackTraceCollector - the stack
+    // trace will be added in NetworkMonitorChild._onNewEvent.
+    if (this.owner.stackTraceCollector) {
+      stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId);
+    }
+
+    event.cause = {
+      type: causeType,
+      loadingDocumentUri: causeUri ? causeUri.spec : null,
+      stacktrace
+    };
+
     httpActivity.isXHR = event.isXHR =
-      (channel.loadInfo.externalContentPolicyType ===
-       Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
-       channel.loadInfo.externalContentPolicyType ===
-       Ci.nsIContentPolicy.TYPE_FETCH);
+        (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
+         causeType === Ci.nsIContentPolicy.TYPE_FETCH);
 
     // Determine the HTTP version.
     let httpVersionMaj = {};
     let httpVersionMin = {};
     channel.QueryInterface(Ci.nsIHttpChannelInternal);
     channel.getRequestVersion(httpVersionMaj, httpVersionMin);
 
     event.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
@@ -927,22 +1085,21 @@ NetworkMonitor.prototype = {
    *
    * @private
    * @param nsIHttpChannel channel
    * @param number timestamp
    * @param string extraStringData
    * @return void
    */
   _onRequestHeader: function (channel, timestamp, extraStringData) {
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
-    this._createNetworkEvent(channel, { timestamp: timestamp,
-                                         extraStringData: extraStringData });
+    this._createNetworkEvent(channel, { timestamp, extraStringData });
   },
 
   /**
    * Create the empty HTTP activity object. This object is used for storing all
    * the request and response information.
    *
    * This is a HAR-like object. Conformance to the spec is not guaranteed at
    * this point.
@@ -1222,18 +1379,17 @@ NetworkMonitor.prototype = {
 
     Services.obs.removeObserver(this._serviceWorkerRequest,
                                 "service-worker-synthesized-response");
 
     this.interceptedChannels.clear();
     this.openRequests = {};
     this.openResponses = {};
     this.owner = null;
-    this.window = null;
-    this.topFrame = null;
+    this.filters = null;
   },
 };
 
 /**
  * The NetworkMonitorChild is used to proxy all of the network activity of the
  * child app process from the main process. The child WebConsoleActor creates an
  * instance of this object.
  *
@@ -1259,33 +1415,33 @@ function NetworkMonitorChild(appId, mess
   this.appId = appId;
   this.connID = connID;
   this.owner = owner;
   this._messageManager = messageManager;
   this._onNewEvent = this._onNewEvent.bind(this);
   this._onUpdateEvent = this._onUpdateEvent.bind(this);
   this._netEvents = new Map();
 }
+
 exports.NetworkMonitorChild = NetworkMonitorChild;
 
 NetworkMonitorChild.prototype = {
   appId: null,
   owner: null,
   _netEvents: null,
   _saveRequestAndResponseBodies: true,
 
   get saveRequestAndResponseBodies() {
     return this._saveRequestAndResponseBodies;
   },
 
   set saveRequestAndResponseBodies(val) {
     this._saveRequestAndResponseBodies = val;
 
     this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, {
-      appId: this.appId,
       action: "setPreferences",
       preferences: {
         saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
       },
     });
   },
 
   init: function () {
@@ -1297,16 +1453,23 @@ NetworkMonitorChild.prototype = {
     mm.sendAsyncMessage("debug:netmonitor:" + this.connID, {
       appId: this.appId,
       action: "start",
     });
   },
 
   _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
     let {id, event} = msg.data;
+
+    // Try to add stack trace to the event data received from parent
+    if (this.owner.stackTraceCollector) {
+      event.cause.stacktrace =
+        this.owner.stackTraceCollector.getStackTrace(event.channelId);
+    }
+
     let actor = this.owner.onNetworkEvent(event);
     this._netEvents.set(id, Cu.getWeakReference(actor));
   }),
 
   _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) {
     let {id, method, args} = msg.data;
     let weakActor = this._netEvents.get(id);
     let actor = weakActor ? weakActor.get() : null;
@@ -1443,29 +1606,29 @@ NetworkMonitorManager.prototype = {
   /**
    * Handler for "debug:monitor" messages received through the message manager
    * from the content process.
    *
    * @param object msg
    *        Message from the content.
    */
   onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) {
-    let { action, appId } = msg.json;
+    let {action} = msg.json;
     // Pipe network monitor data from parent to child via the message manager.
     switch (action) {
       case "start":
         if (!this.netMonitor) {
+          let {appId} = msg.json;
           this.netMonitor = new NetworkMonitor({
             topFrame: this.frame,
             appId: appId,
           }, this);
           this.netMonitor.init();
         }
         break;
-
       case "setPreferences": {
         let {preferences} = msg.json;
         for (let key of Object.keys(preferences)) {
           if (key == "saveRequestAndResponseBodies" && this.netMonitor) {
             this.netMonitor.saveRequestAndResponseBodies = preferences[key];
           }
         }
         break;
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -919,10 +919,9 @@ pref("identity.fxaccounts.remote.oauth.u
 // Token server used by Firefox Account-authenticated Sync.
 pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
 
 // Enable Presentation API
 pref("dom.presentation.enabled", true);
 pref("dom.presentation.discovery.enabled", true);
 
 pref("dom.audiochannel.audioCompeting", true);
-// TODO : turn this pref default on in bug1264901
-pref("dom.audiochannel.mediaControl", false);
+pref("dom.audiochannel.mediaControl", true);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/adjust-sdk-sandbox.token
@@ -0,0 +1,1 @@
+ABCDEFGHIJKL
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -1,32 +1,38 @@
 package org.mozilla.gecko.media;
 
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.PrefsHelper;
-
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
-import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.support.v4.app.NotificationManagerCompat;
 import android.util.Log;
 
-public class MediaControlService extends Service {
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.lang.ref.WeakReference;
+
+public class MediaControlService extends Service implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "MediaControlService";
 
     public static final String ACTION_START          = "action_start";
     public static final String ACTION_PLAY           = "action_play";
     public static final String ACTION_PAUSE          = "action_pause";
     public static final String ACTION_STOP           = "action_stop";
     public static final String ACTION_REMOVE_CONTROL = "action_remove_control";
 
@@ -39,26 +45,38 @@ public class MediaControlService extends
     private MediaController mController;
 
     private PrefsHelper.PrefHandler mPrefsObserver;
     private final String[] mPrefs = { MEDIA_CONTROL_PREF };
 
     private boolean mIsInitMediaSession = false;
     private boolean mIsMediaControlPrefOn = true;
 
+    private static WeakReference<Tab> mTabReference = null;
+
+    private int coverSize;
+
     @Override
     public void onCreate() {
+        mTabReference = new WeakReference<>(null);
+
         getGeckoPreference();
         initMediaSession();
+
+        coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
+
+        Tabs.registerOnTabsChangedListener(this);
     }
 
     @Override
     public void onDestroy() {
         notifyControlInterfaceChanged(ACTION_REMOVE_CONTROL);
         PrefsHelper.removeObserver(mPrefsObserver);
+
+        Tabs.unregisterOnTabsChangedListener(this);
     }
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         handleIntent(intent);
         return super.onStartCommand(intent, flags, startId);
     }
 
@@ -73,16 +91,32 @@ public class MediaControlService extends
         return super.onUnbind(intent);
     }
 
     @Override
     public void onTaskRemoved(Intent rootIntent) {
         stopSelf();
     }
 
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+        if (!mIsInitMediaSession) {
+            return;
+        }
+
+        if (tab == mTabReference.get()) {
+            return;
+        }
+
+        if (msg == Tabs.TabEvents.AUDIO_PLAYING_CHANGE && tab.isAudioPlaying()) {
+            mTabReference = new WeakReference<Tab>(tab);
+            notifyControlInterfaceChanged(ACTION_PAUSE);
+        }
+    }
+
     private boolean isAndroidVersionLollopopOrHigher() {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
     }
 
     private void handleIntent(Intent intent) {
         if (intent == null || intent.getAction() == null ||
            !mIsInitMediaSession) {
             return;
@@ -196,88 +230,117 @@ public class MediaControlService extends
         GeckoAppShell.notifyObservers(topic, data);
     }
 
     private boolean isNeedToRemoveControlInterface(String action) {
         return (action.equals(ACTION_STOP) ||
                 action.equals(ACTION_REMOVE_CONTROL));
     }
 
-    private void notifyControlInterfaceChanged(String action) {
+    private void notifyControlInterfaceChanged(final String action) {
         Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + action);
-        NotificationManager notificationManager = (NotificationManager)
-            getSystemService(Context.NOTIFICATION_SERVICE);
 
         if (isNeedToRemoveControlInterface(action)) {
-            notificationManager.cancel(MEDIA_CONTROL_ID);
+            NotificationManagerCompat.from(this).cancel(MEDIA_CONTROL_ID);
             return;
         }
 
         if (!mIsMediaControlPrefOn) {
             return;
         }
 
-        notificationManager.notify(MEDIA_CONTROL_ID, getNotification(action));
+        final Tab tab = mTabReference.get();
+
+        if (tab == null) {
+            return;
+        }
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                updateNotification(tab, action);
+            }
+        });
     }
 
-    private Notification getNotification(String action) {
-        // TODO : use website name, content and favicon in bug1264901.
-        return new Notification.Builder(this)
-            .setSmallIcon(android.R.drawable.ic_media_play)
-            .setContentTitle("Media Title")
-            .setContentText("Media Artist")
-            .setDeleteIntent(getDeletePendingIntent())
-            .setContentIntent(getClickPendingIntent())
-            .setStyle(getMediaStyle())
-            .addAction(getAction(action))
+    private void updateNotification(Tab tab, String action) {
+        ThreadUtils.assertNotOnUiThread();
+
+        final Notification.MediaStyle style = new Notification.MediaStyle();
+        style.setShowActionsInCompactView(0);
+
+        final Notification notification = new Notification.Builder(this)
+            .setSmallIcon(R.drawable.flat_icon)
+            .setLargeIcon(generateCoverArt(tab))
+            .setContentTitle(tab.getTitle())
+            .setContentText(tab.getURL())
+            .setContentIntent(createContentIntent())
+            .setDeleteIntent(createDeleteIntent())
+            .setStyle(style)
+            .addAction(createNotificationAction(action))
             .setOngoing(action.equals(ACTION_PAUSE))
+            .setShowWhen(false)
+            .setWhen(0)
             .build();
+
+        NotificationManagerCompat.from(this)
+                .notify(MEDIA_CONTROL_ID, notification);
     }
 
-    private Notification.Action getAction(String action) {
-        int icon = getActionIcon(action);
-        String title = getActionTitle(action);
+    private Notification.Action createNotificationAction(String action) {
+        boolean isPlayAction = action.equals(ACTION_PLAY);
+
+        int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
+        String title = getString(isPlayAction ? R.string.media_pause : R.string.media_pause);
 
-        Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
+        final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
         intent.setAction(action);
-        PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
+        final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
+
+        //noinspection deprecation - The new constructor is only for API > 23
         return new Notification.Action.Builder(icon, title, pendingIntent).build();
     }
 
-    private int getActionIcon(String action) {
-        switch (action) {
-            case ACTION_PLAY :
-                return android.R.drawable.ic_media_play;
-            case ACTION_PAUSE :
-                return android.R.drawable.ic_media_pause;
-            default:
-                return 0;
-        }
-    }
-
-    private String getActionTitle(String action) {
-        switch (action) {
-            case ACTION_PLAY :
-                return "Play";
-            case ACTION_PAUSE :
-                return "Pause";
-            default:
-                return null;
-        }
-    }
-
-    private PendingIntent getDeletePendingIntent() {
-        Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
-        intent.setAction(ACTION_REMOVE_CONTROL);
-        return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
-    }
-
-    private PendingIntent getClickPendingIntent() {
+    private PendingIntent createContentIntent() {
         Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
         return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
     }
 
-    private Notification.MediaStyle getMediaStyle() {
-        Notification.MediaStyle style = new Notification.MediaStyle();
-        style.setShowActionsInCompactView(0);
-        return style;
+    private PendingIntent createDeleteIntent() {
+        Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
+        intent.setAction(ACTION_REMOVE_CONTROL);
+        return  PendingIntent.getService(getApplicationContext(), 1, intent, 0);
+    }
+
+    private Bitmap generateCoverArt(Tab tab) {
+        final Bitmap favicon = tab.getFavicon();
+
+        // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
+        if (favicon == null || favicon.getWidth() < 72 || favicon.getHeight() < 72) {
+            // Use the launcher icon as fallback
+            return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
+        }
+
+        // Favicon should at least have half of the size of the cover
+        int width = Math.max(favicon.getWidth(), coverSize / 2);
+        int height = Math.max(favicon.getHeight(), coverSize / 2);
+
+        final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(coverArt);
+        canvas.drawColor(0xFF777777);
+
+        int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
+        int right = Math.min(coverSize, left + width);
+        int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
+        int bottom = Math.min(coverSize, top + height);
+
+        final Paint paint = new Paint();
+        paint.setAntiAlias(true);
+
+        canvas.drawBitmap(favicon,
+                new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
+                new Rect(left, top, right, bottom),
+                paint);
+
+        return coverArt;
+
     }
 }
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1701f34b01c3f6fdab2a6a1b58d7ab0224d91295
GIT binary patch
literal 103
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;o~MgrNCjiE1LNWZkw5tk4mFQj
xc%7Ikj=apju)=XMTd$N!7LYMT{2x%BfkFHwlX~y3uO&d844$rjF6*2UngGUE9ZCQI
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f77ad6b57b7168dfe5a49a0ba455eac2e7e58a0a
GIT binary patch
literal 194
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8r>Bc!NCo5DDF+!D19?~<K3l`6
z`os1`lh%qRy)e<P8$w~s)n!eL?hMQ)L?)<IzF#UNqo@$sD%O)B(aNx_WR4R<@+?0^
zjhB|K3~F2EI2j~6+|^*2FnNyC8{b5cU3W$1q-Jw14@eWy>bWY$)0_N!3$OFHmGvGj
qUc1~fj34)k982CEn(^=_`$4IwCxutdiZ*~;$l&Sf=d#Wzp$Pz=#6ji&
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c85e32c01989d27652abdc114c48b300d2cc2c4f
GIT binary patch
literal 621
zc%17D@N?(olHy`uVBq!ia0vp^2SAtuNHCOdH@?llz+~d-;uuoF`1V$FacrPS>&JiB
z)wkv*2i)G4TO4rv+1b47E2VAsuX(xM{k2M^XPxoyd8_X#&lG1M3pC_ktv`MDd-?Mp
zOw0E5bZ9Kw#S~W87xUbfac|#|{;E_?i6Z5?u!kH0zyE95H{7}}x!{{Q+ls$)8MErs
zA>8*|29Gk%=Wz%8E@zeasB>;xNpQoh{oD=z{&y_CD-4pq@Y-|5H#2#I&!1LjM8`5+
znxquMJ&X6oHK{;Bg|#Q<*BI~E*)Vf$pr*oF?yH^8nDks(8w8_+*Jb}oGF-Q^Vfs_%
zE&pZCBwkUk{aO>u7-*;Pe*W*`JImIKIqzj)p8wRwd*^lUqq%VnTTGv;*G~DaX=lK^
zgm>YsRTh@V7p66ogm$Z2+J(hDW0Q!>Fbyr-eyPsfVb`D6_v03noaNi#vHG-E^3-`Z
z7}YXL<1;5*%C>f>UwcpcYtXMh`x!G7`5v8)dYiqww@B%6(4hxOeWzbG`rpmZaI?v+
z_RcH|{=em|@80XlQy=Eaul|s5`rAb%{_?-_>%)IFCGRM^y!hDq87daHlm1T1|8X&H
zPuf%0?VndTUcdS}!^I%)@PWy%Pe?k~=km31ziVE9`HXx2bE7}JWJO{^R6OGw6_@3U
SH*D$#@jYGrT-G@yGywq2?D(nx
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f49aed757118a941b567629ec217cde1aaf257e8
GIT binary patch
literal 90
zc%17D@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PU<QV=$!9HqJYi24$B+uf
k<OM<x{vY_Cm=S!1k)ch%{N(L`^Zg)Ap00i_>zopr0A<G){{R30
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9cc777c2c44a05f4bce98104d54d7116b8508785
GIT binary patch
literal 214
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Dxt=bLAr*{ouX!^v7K*qPzJJOx
zp@ApJ!;w4af>Y<q1&w=`e&6^<NalPs>yNWDKG&B_sF!CH32<PvKBmXuKCQ%;VL|FE
zrh>GyCm4;Y)AShlx0&!wur%1-;PYr|!;#gDo#8A(>)BM|1U>ffO{mx}QL$m+&rAI^
zX{l+qj10GTvBuVK{=Mq0m1EuGi8B>M6&$S`nzpR#`(L+PZ$SgoyoJlPLKVb%T!9W{
N@O1TaS?83{1OVvqPUHXp
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6d9a57966dea46ca479c346c3f124e10192bb73d
GIT binary patch
literal 859
zc%17D@N?(olHy`uVBq!ia0y~yU}OMc4j{=;DfxXD0|T>?r;B4q1>@U0&6{N%1&%)a
zbG`e<wzEk&Hoy1Y*p|DxS-5)DqP@?{yKJky<L}0u^I3n!hJkW0L4s(dZ6<T73pOV=
zXgkFH*SYuP7lVHTqx+-ST{?`%;`_f>hDb5#R0_!(U*uEB@A5ak&g<~@zoGXThAaQ}
zF=W+8Gi=?j-*D@`xWn7`+zY;KXASs0n`y=0$qZH3g&J<zb1r!I=fEw-fZytjS6pMH
zVze2u{;M?XGJhQY>pBZio_B#Cd!Lvo!-8-A%n<_bFNJD0+>6$$TYpiQanoCQ{!sRW
zS6c(>m>u?6Px;-f&1~?>^N(G_nj7+`{;gtoviwmwQ_9ne0cqhZ33-3y8m2ZIr+%=N
zwFwt&cy^yTKs>xH<<y}7v2f;u>-h{9zW#qz-N<ZJwM~klx{kf!UH!VRp{CDT`TVyq
zuHas38@z4Rn{A94oKKI2Ton$n)5?iT_{z;2qZc|q%<?dsLbUiPt*NI&O@A_^95*cr
zy&4euyqBTP{@MkTsnhR?o#&QsSo3>xeeLvB8{HTto0d3U)e7zIW!Q8&zkS}zc>7@c
z8!uQk1Scnd=~=tzB<F&Mm$UyXh|cw6IBl}cA)htm4MT(21C<l06QmU%M?W;Q;hAa2
z#~)mJ?{BMz^7<ncny-FF)y%${{jr}ZT>D32i+^&B%-qMnE9Y<u`AiI47FRk?Woc%S
z_?%@Y4Fe~w+{Afu_Fi>w<Jftu!a6g5RwyOhl;Kv7w0g}jp+G6&r;W76hE}%OD>n4<
zNoz{xFTMJ9*UYG9v*6u^2db?1M$hfL{Zf^M3Mr6iP{F!idGmR{wRMj`JWp3Ymvv4F
FO#r&o8)N_g
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7192ad487eacb4f8f530ebe2878760e2528fbc5f
GIT binary patch
literal 92
zc%17D@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd7_>!jv*C{
m$r5W0{Noqkjk>0hJ%NeAb!~d=tj~okAZ?zmelF{r5}E*xz!xz9
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e0f6a850d17d4b29baca6be83c6f27207841c225
GIT binary patch
literal 279
zc%17D@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw?s>X6hEy=Vy|Hm8V<3Y|qQef>
z6%A4$3%PbJ;M#SeMXp3aoQZD+%Y*|gpYO8O^;_<KKh40JkyRwXfswVOnu#mJ=`uG9
z_pd2)9T<Xd#VJG>E_ukpG~?n&ri1egTZK6i{H}AeXt22{96i9fM53Zmgjsz;(gW5Y
zo;eO(jp81LADA@R{D6|&OC&6SlKMbN<|_H-hHpI@pBC2{{buA{Zm?~RoUHAs${kVr
z>-I=i#3h;*h)-a9$8hOD?f<FgQV;)be16D_xtuZc0iOb|jrjBXmQd#b3HGz;_Z<(E
Smr4MA%HZkh=d#Wzp$Pzz6Jk&R
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9046219e40a73a1857528dd8d31ba1c55edba3db
GIT binary patch
literal 1379
zc$~GAeK6Yx7{{NVA7WCp96^_`IlHT!HFvDu>ZPp-U8__@M@o81T)e+TNr&Hb*L170
zo8D-n3DwOInO42VkqvQICCtf+rbK#_h^xlDM9B2N-R-Wom)jrTe?H%PzV|$TJm0Uv
zLMdjZ_ND+}77`qI768HC$Yc+|7lNgD03(->z+>m~5VaiDI8z8>#YPNYXDx`=M5g#`
zNZLB77gLq}3>k0nq<kT2<&b}5-C>d5nmOf+><2J_0D!$P1RMbjVK4iy@a^uuh5v9Q
z&F-hC4^q{iYQTL_`27~cX9dWgk2^3}ucF~ZJDo)ucLDVBcIi=J4D6dK=e*v0Za8{L
z7u5*vcKV4#I4vn+K>&9(4oM>KB9CnKFjZOvse}Glo6;d*eAS{_y@?ym)uBl~yNEmS
z&$j+7bZ59?B#GV&Ey-Rm1GcSx!Gt{WMDBrl&hWf%RT$E%dfPXSKtYMV1ufA?*J)@q
zXSIlmW$wx)AHo`WS86~W5hg^yN9s*~A91m7{wW9KdS(9F^f2zVC818OCFY;9Fvz=f
zWj(B_$C=xbywUv*u2M_Rreo)uT}3_o49p@c59=t5%k}jh1B(Pa*0GR5y&|llV~b6x
zFjv3KPA*1Bj}@@O|2kgl*LW5G0by390Lj)6EoXT8_QYU0zB!YOWhM&wgbRA<(yNcH
z@Xba0F|F>RoR(8iF~PCIe}f`>>wgzYVW|~5Q9;WX10;32<%K>0?>Y(HJ7N3Yo!`vF
ztz&VpG88}6LT)H~*yY8>2Xx;ZT#G#Ck@$Pi8Uf2&3C@+<Of*E(2)@RSB+sgeJ3o@@
z2WmC<2BANDlEr8mA=<xm3x`z}btbS?VAER?i;~mN_<DCYQ{w~!u4BnCcU!<~98h(g
zrXOLOiPuWmZSD=eCx{uSV{AQxGtbOSaP_m<F{X!8^?n0fMSJZG^mnm0kGH$++fbQ6
z`b3|Uspdt;9Pq`>)<ksM^>oT)6e-ftLrQCCO}^$6j~DCM9gVViqDYnwiA#!_Xzq#(
zfLshuzvMg_3QO<bm9O8dEPJ0vT&^{N8@NrfIaXF%@E$6T`nKm5ZkBPVu+m6;#%jlt
zfbr|LuNq^vcjphE#9iCEVTswc?v#@-+bu3r>CY_Fg^O0hjhrafo&sf#moPC4`Jw$j
z%6<2<`B6X4daLT8WkyOuEooAwouyI;Maq|RHCcJ>N)=_YHOIwRYGm$2@T8p|=hN&l
z+s<F^uR8lVRcyNVNSH6|8zqF!o3s})P0*}M-*fik1fH;kRpW-OA4L9^<`4diK&nUa
zFc_z3l}8^$!c>}JI+7$h@XWM&F{s%a9z8QLkHuFXyX-pbKb;>1Nx!-(t$o^>3K9z6
k{^Yud|56qIL*?w4UQk)P5A^N^ziH2qPeKEQ$LYm?0El(~fdBvi
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -212,9 +212,11 @@
     <dimen name="action_bar_divider_height">2dp</dimen>
 
     <!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
     <item name="match_parent" type="dimen">-1</item>
     <item name="wrap_content" type="dimen">-2</item>
 
     <item name="tab_strip_content_start" type="dimen">12dp</item>
     <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
+
+    <item name="notification_media_cover" type="dimen">128dp</item>
 </resources>
deleted file mode 100644
--- a/mobile/android/base/resources/values/xml.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?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>
-    <!-- These items are v11+ resources but are referenced in code shipped with
-         API 9 builds. Since v11+ resources don't ship on API 9 builds, in order
-         for the resource ID to be found (and thus compilation to succeed), we
-         provide dummy values below. -->
-    <item type="xml" name="preferences_privacy_clear_tablet">@null</item>
-</resources>
--- a/mobile/android/config/mozconfigs/common
+++ b/mobile/android/config/mozconfigs/common
@@ -51,16 +51,25 @@ ac_add_options --enable-update-channel=$
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-fennec-geoloc-api.key
 if test "$MOZ_UPDATE_CHANNEL" = "release" ; then
     ac_add_options --with-adjust-sdk-keyfile=/builds/adjust-sdk.token
 elif test "$MOZ_UPDATE_CHANNEL" = "beta" ; then
     ac_add_options --with-adjust-sdk-keyfile=/builds/adjust-sdk-beta.token
+else
+    # (bug 1277553) In Aurora -> Beta simulation builds, no update channel is
+    # specified, causing an assertion to throw that MOZ_INSTALL_TRACKING is
+    # specified but the keyfile is not. In this case, we add a default keyfile.
+    # This has the disadvantage that if our beta/release checks above ever
+    # fail, we'll come to this default case and the compile-time check to
+    # specify a valid keyfile will be broken. I don't have any better
+    # alternatives.
+    ac_add_options --with-adjust-sdk-keyfile="$topsrcdir/mobile/android/base/adjust-sdk-sandbox.token"
 fi
 export SOCORRO_SYMBOL_UPLOAD_TOKEN_FILE=/builds/crash-stats-api.token
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 # Use ccache
 . "$topsrcdir/build/mozconfig.cache"
--- a/mobile/android/docs/adjust.rst
+++ b/mobile/android/docs/adjust.rst
@@ -118,27 +118,46 @@ likely to uniquely fingerprint the devic
 uniquely fingerprint the user.
 
 Technical notes
 ~~~~~~~~~~~~~~~
 
 Build flags controlling the Adjust SDK integration
 ==================================================
 
-The Adjust SDK feature is controlled by the build flag ``MOZ_INSTALL_TRACKING``.  No trace of the
-Adjust SDK should be present in Fennec if this is not defined.
+Add the following to your mozconfig to compile with the Adjust SDK::
+
+ export MOZ_INSTALL_TRACKING=1
+ export MOZ_NATIVE_DEVICES=1
+ export RELEASE_BUILD=1
+ ac_add_options --with-adjust-sdk-keyfile="$topsrcdir/mobile/android/base/adjust-sdk-sandbox.token"
+
+``MOZ_NATIVE_DEVICES`` && ``RELEASE_BUILD`` are required for an unknown
+reason.  If you build without them, the ``StubAdjustHelper`` will be
+returned.
+
+No trace of the Adjust SDK should be present in Fennec if
+``MOZ_INSTALL_TRACKING`` is not defined.
 
-Access to the Adjust backend is controlled by a private App-specific token.  Fennec's token is
-managed by Release Engineering and should not be exposed if at all possible; for example, it should
-*not* leak to build logs.  The value of the token is read from the file specified using the
-``configure`` flag ``--with-adjust-sdk-keyfile=KEYFILE`` and stored in the build variable
-``MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN``.  Nota bene: if ``MOZ_INSTALL_TRACKING`` is defined
-but the App-specific token is not specified, Fennec will submit data to a special Adjust sandbox.
-This makes it possible to test the Adjust flow without submitting false data to the install tracking
-backend.
+Access to the Adjust backend is controlled by a private App-specific
+token. Fennec's token is managed by Release Engineering and should not
+be exposed if at all possible; for example, it should *not* leak to build
+logs.  The value of the token is read from the file specified using the
+``configure`` flag ``--with-adjust-sdk-keyfile=KEYFILE`` and stored in
+the build variable ``MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN``. The
+mozconfig specified above defaults to submitting data to a special Adjust
+sandbox allowing a developer to test Adjust without submitting false
+data to our backend.
+
+We throw an assertion if ``MOZ_INSTALL_TRACKING`` is specified but
+``--with-adjust-sdk-keyfile`` is not to ensure our builders have a proper
+adjust token for release and beta builds.  It's great to catch some
+errors at compile-time rather than in release. That being said, ideally
+we'd specify a default ``--with-adjust-sdk-keyfile`` for developer builds
+but I don't know how to do that.
 
 Technical notes on the Adjust SDK integration
 =============================================
 
 The *Adjust install tracking SDK* is a pure-Java library that is conditionally compiled into Fennec.
 It's not trivial to integrate such conditional feature libraries into Fennec without pre-processing.
 To minimize such pre-processing, we define a trivial ``AdjustHelperInterface`` and define two
 implementations: the real ``AdjustHelper``, which requires the Adjust SDK, and a no-op
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -39,17 +39,17 @@ EXTRA_JS_MODULES['services-sync'] += [
     'modules/util.js',
 ]
 
 EXTRA_PP_JS_MODULES['services-sync'] += [
     'modules/constants.js',
 ]
 
 # Definitions used by constants.js
-DEFINES['weave_version'] = '1.51.0'
+DEFINES['weave_version'] = '1.52.0'
 DEFINES['weave_id'] = '{340c2bbc-ce74-4362-90b5-7c26312808ef}'
 
 EXTRA_JS_MODULES['services-sync'].engines += [
     'modules/engines/addons.js',
     'modules/engines/bookmarks.js',
     'modules/engines/clients.js',
     'modules/engines/forms.js',
     'modules/engines/history.js',
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5581,25 +5581,25 @@
     "bug_numbers": [1268424],
     "kind": "linear",
     "high": 200,
     "n_buckets": 50,
     "description": "Recorded once per session near startup: records the search plugin count, including both built-in plugins (including the ones the user has hidden) and user-installed plugins."
   },
   "SEARCH_SERVICE_HAS_UPDATES": {
     "alert_emails": ["florian@mozilla.com"],
-    "expires_in_version": "50",
+    "expires_in_version": "55",
     "kind": "boolean",
     "bug_numbers": [1259510],
     "description": "Recorded once per session near startup: records true/false whether the search service has engines with update URLs.",
     "releaseChannelCollection": "opt-out"
   },
   "SEARCH_SERVICE_HAS_ICON_UPDATES": {
     "alert_emails": ["florian@mozilla.com"],
-    "expires_in_version": "50",
+    "expires_in_version": "55",
     "kind": "boolean",
     "bug_numbers": [1259510],
     "description": "Recorded once per session near startup: records true/false whether the search service has engines with icon update URLs.",
     "releaseChannelCollection": "opt-out"
   },
   "SEARCH_SERVICE_BUILD_CACHE_MS": {
     "expires_in_version": "40",
     "kind": "exponential",
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -276,16 +276,23 @@ function getHistogram(histogramId) {
   return Telemetry.getHistogramById(histogramId);
 }
 
 // Short-hand for retrieving the snapshot of the Histogram with that id.
 function getSnapshot(histogramId) {
   return Telemetry.getHistogramById(histogramId).snapshot();
 }
 
+// Helper for setting an empty list of Environment preferences to watch.
+function setEmptyPrefWatchlist() {
+  let TelemetryEnvironment =
+    Cu.import("resource://gre/modules/TelemetryEnvironment.jsm").TelemetryEnvironment;
+  TelemetryEnvironment.testWatchPreferences(new Map());
+}
+
 if (runningInParent) {
   // Set logging preferences for all the tests.
   Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
   // Telemetry archiving should be on.
   Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
   // Telemetry xpcshell tests cannot show the infobar.
   Services.prefs.setBoolPref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
   // FHR uploads should be enabled.
--- a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -66,16 +66,20 @@ add_task(function*() {
     return;
   }
 
   // Setup.
   do_get_profile(true);
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   yield TelemetryController.testSetup();
+  if (runningInParent) {
+    // Make sure we don't generate unexpected pings due to pref changes.
+    setEmptyPrefWatchlist();
+  }
 
   // Run test in child, don't wait for it to finish.
   let childPromise = run_test_in_child("test_ChildHistograms.js");
   yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
 
   // Gather payload from child.
   dump("... requesting child payloads\n");
   let promiseMessage = do_await_remote_message(MESSAGE_TELEMETRY_PAYLOAD);
--- a/toolkit/components/telemetry/tests/unit/test_PingAPI.js
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -61,16 +61,18 @@ var getArchivedPingsInfo = Task.async(fu
 
   // Sort the list by creation date and then return it.
   archivedPings.sort((a, b) => b.timestamp - a.timestamp);
   return archivedPings;
 });
 
 function run_test() {
   do_get_profile(true);
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   run_next_test();
 }
 
 add_task(function* test_archivedPings() {
   // TelemetryController should not be fully initialized at this point.
   // Submitting pings should still work fine.
 
--- a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -85,16 +85,18 @@ var promiseValidateArchivedPings = Task.
 });
 
 function run_test() {
   do_test_pending();
 
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
 
   Preferences.set(PREF_TELEMETRY_ENABLED, true);
 
   run_next_test();
 }
 
 add_task(function* test_subsessionsChaining() {
   if (gIsAndroid) {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -91,16 +91,18 @@ function checkPingFormat(aPing, aType, a
 }
 
 function run_test() {
   do_test_pending();
 
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
 }
 
 add_task(function* asyncSetup() {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -25,16 +25,18 @@ function contentHandler(metadata, respon
   response.processAsync();
   response.setHeader("Content-Type", "text/plain");
 }
 
 function run_test() {
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   run_next_test();
 }
 
 /**
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -15,16 +15,19 @@ Cu.import("resource://gre/modules/Teleme
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 
 var gHttpServer = null;
 
 function run_test() {
   do_test_pending();
   do_get_profile();
 
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
+
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   // Start the webserver to check if the pending ping correctly arrives.
   gHttpServer = new HttpServer();
   gHttpServer.start(-1);
 
   run_next_test();
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -56,16 +56,19 @@ function setMinimumPolicyVersion(aNewPol
   Preferences.set(PREF_MINIMUM_POLICY_VERSION, aNewPolicyVersion);
 }
 
 function run_test() {
   // Addon manager needs a profile directory
   do_get_profile(true);
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
+
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   // Don't bypass the notifications in this test, we'll fake it.
   Services.prefs.setBoolPref(PREF_BYPASS_NOTIFICATION, false);
 
   TelemetryReportingPolicy.setup();
 
   run_next_test();
 }
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -67,16 +67,18 @@ var checkPingsSaved = Task.async(functio
   }
 
   return allFound;
 });
 
 function run_test() {
   // Trigger a proper telemetry init.
   do_get_profile(true);
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   run_next_test();
 }
 
 // Test the ping sending logic.
 add_task(function* test_sendPendingPings() {
   const TYPE_PREFIX = "test-sendPendingPings-";
   const TEST_TYPE_A = TYPE_PREFIX + "A";
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -144,16 +144,18 @@ function pingHandler(aRequest) {
   gSeenPings++;
 }
 
 function run_test() {
   PingServer.start();
   PingServer.registerPingHandler(pingHandler);
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,
                              "http://localhost:" + PingServer.port);
   run_next_test();
 }
 
 /**
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -407,16 +407,18 @@ function write_fake_failedprofilelocks_f
 }
 
 function run_test() {
   do_test_pending();
 
   // Addon manager needs a profile directory
   do_get_profile();
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   // Make it look like we've previously failed to lock a profile a couple times.
   write_fake_failedprofilelocks_file();
 
   // Make it look like we've shutdown before.
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -36,16 +36,19 @@ function initialiseTelemetry() {
 }
 
 function run_test() {
   // Telemetry needs the AddonManager.
   loadAddonManager();
   // Make profile available for |TelemetryController.testShutdown()|.
   do_get_profile();
 
+  // Make sure we don't generate unexpected pings due to pref changes.
+  setEmptyPrefWatchlist();
+
   do_test_pending();
   const Telemetry = Services.telemetry;
   Telemetry.asyncFetchTelemetryData(run_next_test);
 }
 
 add_task(function* actualTest() {
   yield initialiseTelemetry();
 
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -205,17 +205,26 @@ Finder.prototype = {
       // Look for any selected text on the actual page.
       selText = focusedWindow.getSelection().toString();
     }
 
     if (!selText)
       return "";
 
     // Process our text to get rid of unwanted characters.
-    return selText.trim().replace(/\s+/g, " ").substr(0, kSelectionMaxLen);
+    selText = selText.trim().replace(/\s+/g, " ");
+    let truncLength = kSelectionMaxLen;
+    if (selText.length > truncLength) {
+      let truncChar = selText.charAt(truncLength).charCodeAt(0);
+      if (truncChar >= 0xDC00 && truncChar <= 0xDFFF)
+        truncLength++;
+      selText = selText.substr(0, truncLength);
+    }
+
+    return selText;
   },
 
   enableSelection: function() {
     this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON);
     this._restoreOriginalOutline();
   },
 
   removeSelection: function() {
--- a/xpcom/components/Module.h
+++ b/xpcom/components/Module.h
@@ -17,17 +17,17 @@ namespace mozilla {
 /**
  * A module implements one or more XPCOM components. This structure is used
  * for both binary and script modules, but the registration members
  * (cids/contractids/categoryentries) are unused for modules which are loaded
  * via a module loader.
  */
 struct Module
 {
-  static const unsigned int kVersion = 49;
+  static const unsigned int kVersion = 50;
 
   struct CIDEntry;
 
   typedef already_AddRefed<nsIFactory> (*GetFactoryProcPtr)(
     const Module& module, const CIDEntry& entry);
 
   typedef nsresult (*ConstructorProcPtr)(nsISupports* aOuter,
                                          const nsIID& aIID,