Merge fx-team to m-c on a CLOSED TREE.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 12 Jun 2014 17:21:11 -0400
changeset 188445 aab3362f97e90becbc8d928367229b82e56bfa7c
parent 188419 3a81f39bf585bfe182e10e62672634aa462cbe03 (current diff)
parent 188444 f79cfc8384306603630d881c774234ceb8c05984 (diff)
child 188513 9b5a8c3a9d79677b8a57897793d9084993ae0615
child 188529 71d42a28af2448de52104edb2dadf63a914be6a6
child 188688 8c4371b5482e4f8c0bd20547ed7783073ff917bd
push id26955
push userryanvm@gmail.com
push dateThu, 12 Jun 2014 21:21:04 +0000
treeherdermozilla-central@aab3362f97e9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c on a CLOSED TREE.
mobile/android/base/resources/values-v19/colors.xml
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -1,14 +1,14 @@
 /* - This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this file,
    - You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
- 
+
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
@@ -37,17 +37,19 @@ function init_all() {
     if (event.keyCode == KeyEvent.DOM_VK_TAB) {
       categories.setAttribute("keyboard-navigation", "true");
     }
   });
   categories.addEventListener("mousedown", function() {
     this.removeAttribute("keyboard-navigation");
   });
 
-  gotoPref("paneGeneral");
+  if (document.getElementById("category-general").selected) {
+    gotoPref("paneGeneral");
+  }
 }
 
 function selectCategory(name) {
   let categories = document.getElementById("categories");
   let item = categories.querySelector(".category[value=" + name + "]");
   categories.selectedItem = item;
   gotoPref(name);
 }
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 support-files =
   head.js
   privacypane_tests_perwindow.js
 
 [browser_advanced_update.js]
 [browser_bug410900.js]
 [browser_bug731866.js]
+[browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_bug795764_cachedisabled.js]
 [browser_connection.js]
 [browser_connection_bug388287.js]
 [browser_healthreport.js]
 skip-if = !healthreport || (os == 'linux' && debug)
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function test() {
+  waitForExplicitFinish();
+  let deferred = Promise.defer();
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  openPreferences("paneContent");
+  let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+
+  newTabBrowser.addEventListener("Initialized", function PrefInit() {
+    newTabBrowser.removeEventListener("Initialized", PrefInit, true);
+    newTabBrowser.contentWindow.addEventListener("load", function prefLoad() {
+      newTabBrowser.contentWindow.removeEventListener("load", prefLoad);
+      let sel = gBrowser.contentWindow.history.state;
+      is(sel, "paneContent", "Content pane was selected");
+      deferred.resolve();
+      gBrowser.removeCurrentTab();
+    });
+  }, true);
+
+  yield deferred.promise;
+
+  finish();
+});
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -219,16 +219,24 @@ let NetMonitorController = {
     // the RemoteTarget.
     this._connection = null;
     this.client = null;
     this.tabClient = null;
     this.webConsoleClient = null;
   },
 
   /**
+   * Checks whether the netmonitor connection is active.
+   * @return boolean
+   */
+  isConnected: function() {
+    return !!this.client;
+  },
+
+  /**
    * Sets up a monitoring session.
    *
    * @param DebuggerClient aClient
    *        The debugger client.
    * @param object aTabGrip
    *        The remote protocol grip of the tab.
    * @param function aCallback
    *        A function to invoke once the client attached to the console client.
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1975,16 +1975,18 @@ NetworkDetailsView.prototype = {
     $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the NetworkDetailsView");
+
+    $("tabpanels", this.widget).removeEventListener("select", this._onTabSelect);
   },
 
   /**
    * Populates this view with the specified data.
    *
    * @param object aData
    *        The data source (this should be the attachment of a request item).
    * @return object
@@ -2063,17 +2065,20 @@ NetworkDetailsView.prototype = {
           yield view._setTimingsInformation(src.eventTimings);
           break;
         case 5: // "Preview"
           yield view._setHtmlPreview(src.responseContent);
           break;
       }
       populated[tab] = true;
       window.emit(EVENTS.TAB_UPDATED);
-      NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+
+      if (NetMonitorController.isConnected()) {
+        NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
+      }
     });
   },
 
   /**
    * Sets the network request summary shown in this view.
    *
    * @param object aData
    *        The data source (this should be the attachment of a request item).
--- a/browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
+++ b/browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
@@ -34,18 +34,19 @@ function test() {
       '-H "Accept-Encoding: gzip, deflate"',
       '-H "X-Custom-Header-1: Custom value"',
       '-H "X-Custom-Header-2: 8.8.8.8"',
       '-H "X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"',
       '-H "Referer: ' + CURL_URL + '"',
       '-H "Connection: keep-alive"'
     ].join(" ");
 
-    const EXPECTED_RESULT = Services.appinfo.OS == "WINNT" ?
-                            EXPECTED_WIN_RESULT : EXPECTED_POSIX_RESULT;
+    const EXPECTED_RESULT = Services.appinfo.OS == "WINNT"
+      ? EXPECTED_WIN_RESULT
+      : EXPECTED_POSIX_RESULT;
 
     let { NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
--- a/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
@@ -26,17 +26,17 @@ function test() {
         document.getElementById("details-pane-toggle"));
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.querySelectorAll("#details-pane tab")[3]);
 
       let RESPONSE_BODY_DISPLAYED = aMonitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED;
       waitFor(aMonitor.panelWin, RESPONSE_BODY_DISPLAYED).then(() =>
         NetMonitorView.editor("#response-content-textarea")
       ).then((aEditor) => {
-        is(aEditor.getText().indexOf("\u044F"), 302, // я
+        is(aEditor.getText().indexOf("\u044F"), 486, // я
           "The text shown in the source editor is incorrect.");
         is(aEditor.getMode(), Editor.modes.html,
           "The mode active in the source editor is incorrect.");
 
         teardown(aMonitor).then(finish);
       });
     });
 
--- a/browser/devtools/netmonitor/test/browser_net_simple-init.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-init.js
@@ -1,16 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Simple check if the network monitor starts up and shuts down properly.
  */
 
 function test() {
+  // These test suite functions are removed from the global scope inside a
+  // cleanup function. However, we still need them.
+  let gInfo = info;
+  let gOk = ok;
+
   initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     is(aTab.linkedBrowser.contentWindow.wrappedJSObject.location, SIMPLE_URL,
       "The current tab's location is the correct one.");
     is(aDebuggee.location, SIMPLE_URL,
       "The current debuggee's location is the correct one.");
 
@@ -31,30 +36,30 @@ function test() {
         "There should be a client available at this point (" + aTag + ").");
       ok(aMonitor._controller.tabClient,
         "There should be a tabClient available at this point (" + aTag + ").");
       ok(aMonitor._controller.webConsoleClient,
         "There should be a webConsoleClient available at this point (" + aTag + ").");
     }
 
     function checkIfDestroyed(aTag) {
-      info("Checking if destruction is ok.");
+      gInfo("Checking if destruction is ok.");
 
-      ok(aMonitor._view,
+      gOk(aMonitor._view,
         "The network monitor view object still exists (" + aTag + ").");
-      ok(aMonitor._controller,
+      gOk(aMonitor._controller,
         "The network monitor controller object still exists (" + aTag + ").");
-      ok(aMonitor._controller._shutdown,
+      gOk(aMonitor._controller._shutdown,
         "The network monitor controller object still exists and is destroyed (" + aTag + ").");
 
-      ok(!aMonitor._controller.client,
+      gOk(!aMonitor._controller.client,
         "There shouldn't be a client available after destruction (" + aTag + ").");
-      ok(!aMonitor._controller.tabClient,
+      gOk(!aMonitor._controller.tabClient,
         "There shouldn't be a tabClient available after destruction (" + aTag + ").");
-      ok(!aMonitor._controller.webConsoleClient,
+      gOk(!aMonitor._controller.webConsoleClient,
         "There shouldn't be a webConsoleClient available after destruction (" + aTag + ").");
     }
 
     executeSoon(() => {
       checkIfInitialized(1);
 
       aMonitor._controller.startupNetMonitor()
         .then(() => {
@@ -70,19 +75,19 @@ function test() {
         .then(finish);
     });
 
     registerCleanupFunction(() => {
       checkIfDestroyed(1);
 
       aMonitor._controller.shutdownNetMonitor()
         .then(() => {
-          info("Shutting down again shouldn't do anything special.");
+          gInfo("Shutting down again shouldn't do anything special.");
           checkIfDestroyed(2);
           return aMonitor._controller.disconnect();
         })
         .then(() => {
-          info("Disconnecting again shouldn't do anything special.");
+          gInfo("Disconnecting again shouldn't do anything special.");
           checkIfDestroyed(3);
         });
     });
   });
 }
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js
@@ -81,20 +81,18 @@ function test() {
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.RECEIVED_REQUEST_HEADERS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.attachment.requestHeaders,
         "There should be a requestHeaders attachment available.");
-      ok(requestItem.attachment.requestHeaders.headers.length >= 6,
+      is(requestItem.attachment.requestHeaders.headers.length, 8,
         "The requestHeaders attachment has an incorrect |headers| property.");
-      // Can't test for an exact total number of headers, because it seems to
-      // vary across pgo/non-pgo builds.
       isnot(requestItem.attachment.requestHeaders.headersSize, 0,
         "The requestHeaders attachment has an incorrect |headersSize| property.");
       // Can't test for the exact request headers size because the value may
       // vary across platforms ("User-Agent" header differs).
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
@@ -113,19 +111,19 @@ function test() {
       ok(false, "Trap listener: this request doesn't have any post data.")
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
       ok(requestItem.attachment.responseHeaders,
         "There should be a responseHeaders attachment available.");
-      is(requestItem.attachment.responseHeaders.headers.length, 6,
+      is(requestItem.attachment.responseHeaders.headers.length, 9,
         "The responseHeaders attachment has an incorrect |headers| property.");
-      is(requestItem.attachment.responseHeaders.headersSize, 173,
+      is(requestItem.attachment.responseHeaders.headersSize, 255,
         "The responseHeaders attachment has an incorrect |headersSize| property.");
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS);
     });
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
@@ -141,17 +139,17 @@ function test() {
       let requestItem = RequestsMenu.getItemAtIndex(0);
 
       is(requestItem.attachment.httpVersion, "HTTP/1.1",
         "The httpVersion attachment has an incorrect value.");
       is(requestItem.attachment.status, "200",
         "The status attachment has an incorrect value.");
       is(requestItem.attachment.statusText, "Och Aye",
         "The statusText attachment has an incorrect value.");
-      is(requestItem.attachment.headersSize, 173,
+      is(requestItem.attachment.headersSize, 255,
         "The headersSize attachment has an incorrect value.");
 
       verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, {
         status: "200",
         statusText: "Och Aye"
       });
     });
 
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
@@ -58,76 +58,74 @@ function test() {
         "GET", "The method summary value is incorrect.");
       is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("code"),
         "200", "The status summary code is incorrect.");
       is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
         "200 Och Aye", "The status summary value is incorrect.");
 
       is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
         "There should be 2 header scopes displayed in this tabpanel.");
-      ok(tabpanel.querySelectorAll(".variable-or-property").length >= 12,
-        "There should be at least 12 header values displayed in this tabpanel.");
-      // Can't test for an exact total number of headers, because it seems to
-      // vary across pgo/non-pgo builds.
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 17,
+        "There should be 17 header values displayed in this tabpanel.");
 
       is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
         "The empty notice should not be displayed in this tabpanel.");
 
       let responseScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
       let requestScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
 
       is(responseScope.querySelector(".name").getAttribute("value"),
         L10N.getStr("responseHeaders") + " (" +
-        L10N.getFormatStr("networkMenu.sizeKB", L10N.numberWithDecimals(173/1024, 3)) + ")",
+        L10N.getFormatStr("networkMenu.sizeKB", L10N.numberWithDecimals(255/1024, 3)) + ")",
         "The response headers scope doesn't have the correct title.");
 
       ok(requestScope.querySelector(".name").getAttribute("value").contains(
         L10N.getStr("requestHeaders") + " (0"),
         "The request headers scope doesn't have the correct title.");
       // Can't test for full request headers title because the size may
       // vary across platforms ("User-Agent" header differs). We're pretty
       // sure it's smaller than 1 MB though, so it starts with a 0.
 
       is(responseScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
-        "Connection", "The first response header name was incorrect.");
+        "Cache-Control", "The first response header name was incorrect.");
       is(responseScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
-        "\"close\"", "The first response header value was incorrect.");
+        "\"no-cache, no-store, must-revalidate\"", "The first response header value was incorrect.");
       is(responseScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
-        "Content-Length", "The second response header name was incorrect.");
+        "Connection", "The second response header name was incorrect.");
       is(responseScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
-        "\"12\"", "The second response header value was incorrect.");
+        "\"close\"", "The second response header value was incorrect.");
       is(responseScope.querySelectorAll(".variables-view-variable .name")[2].getAttribute("value"),
-        "Content-Type", "The third response header name was incorrect.");
+        "Content-Length", "The third response header name was incorrect.");
       is(responseScope.querySelectorAll(".variables-view-variable .value")[2].getAttribute("value"),
-        "\"text/plain; charset=utf-8\"", "The third response header value was incorrect.");
-      is(responseScope.querySelectorAll(".variables-view-variable .name")[5].getAttribute("value"),
+        "\"12\"", "The third response header value was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[3].getAttribute("value"),
+        "Content-Type", "The fourth response header name was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[3].getAttribute("value"),
+        "\"text/plain; charset=utf-8\"", "The fourth response header value was incorrect.");
+      is(responseScope.querySelectorAll(".variables-view-variable .name")[8].getAttribute("value"),
         "foo-bar", "The last response header name was incorrect.");
-      is(responseScope.querySelectorAll(".variables-view-variable .value")[5].getAttribute("value"),
+      is(responseScope.querySelectorAll(".variables-view-variable .value")[8].getAttribute("value"),
         "\"baz\"", "The last response header value was incorrect.");
 
       is(requestScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
         "Host", "The first request header name was incorrect.");
       is(requestScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
         "\"example.com\"", "The first request header value was incorrect.");
       is(requestScope.querySelectorAll(".variables-view-variable .name")[5].getAttribute("value"),
-        "Connection", "The penultimate request header name was incorrect.");
+        "Connection", "The ante-penultimate request header name was incorrect.");
       is(requestScope.querySelectorAll(".variables-view-variable .value")[5].getAttribute("value"),
-        "\"keep-alive\"", "The penultimate request header value was incorrect.");
-
-      let lastReqHeaderName = requestScope.querySelectorAll(".variables-view-variable .name")[6];
-      let lastReqHeaderValue = requestScope.querySelectorAll(".variables-view-variable .value")[6];
-      if (lastReqHeaderName && lastReqHeaderValue) {
-        is(lastReqHeaderName.getAttribute("value"),
-          "Cache-Control", "The last request header name was incorrect.");
-        is(lastReqHeaderValue.getAttribute("value"),
-          "\"max-age=0\"", "The last request header value was incorrect.");
-      } else {
-        info("The number of request headers was 6 instead of 7. Technically, " +
-             "not a failure in this particular test, but needs investigation.");
-      }
+        "\"keep-alive\"", "The ante-penultimate request header value was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .name")[6].getAttribute("value"),
+        "Pragma", "The penultimate request header name was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .value")[6].getAttribute("value"),
+        "\"no-cache\"", "The penultimate request header value was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .name")[7].getAttribute("value"),
+        "Cache-Control", "The last request header name was incorrect.");
+      is(requestScope.querySelectorAll(".variables-view-variable .value")[7].getAttribute("value"),
+        "\"no-cache\"", "The last request header value was incorrect.");
     }
 
     function testCookiesTab() {
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.querySelectorAll("#details-pane tab")[1]);
 
       let tab = document.querySelectorAll("#details-pane tab")[1];
       let tabpanel = document.querySelectorAll("#details-pane tabpanel")[1];
--- a/browser/devtools/netmonitor/test/browser_net_timing-division.js
+++ b/browser/devtools/netmonitor/test/browser_net_timing-division.js
@@ -10,21 +10,48 @@ function test() {
     info("Starting test... ");
 
     let { $all, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 2).then(() => {
-      let divisions = $all(".requests-menu-timings-division[division-scale=second]");
+      let millisecondDivs = $all(".requests-menu-timings-division[division-scale=millisecond]");
+      let secondDivs = $all(".requests-menu-timings-division[division-scale=second]");
+      let minuteDivs = $all(".requests-menu-timings-division[division-scale=minute]");
+
+      info("Number of millisecond divisions: " + millisecondDivs.length);
+      info("Number of second divisions: " + secondDivs.length);
+      info("Number of minute divisions: " + minuteDivs.length);
 
-      ok(divisions.length,
+      for (let div of millisecondDivs) {
+        info("Millisecond division: " + div.getAttribute("value"));
+      }
+      for (let div of secondDivs) {
+        info("Second division: " + div.getAttribute("value"));
+      }
+      for (let div of minuteDivs) {
+        info("Minute division: " + div.getAttribute("value"));
+      }
+
+      is(RequestsMenu.itemCount, 2,
+        "There should be only two requests made.");
+
+      let firstRequest = RequestsMenu.getItemAtIndex(0);
+      let lastRequest = RequestsMenu.getItemAtIndex(1);
+
+      info("First request happened at: " +
+        firstRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+      info("Last request happened at: " +
+        lastRequest.attachment.responseHeaders.headers.find(e => e.name == "Date").value);
+
+      ok(secondDivs.length,
         "There should be at least one division on the seconds time scale.");
-      ok(divisions[0].getAttribute("value").match(/\d+\.\d{2}\s\w+/),
+      ok(secondDivs[0].getAttribute("value").match(/\d+\.\d{2}\s\w+/),
         "The division on the seconds time scale looks legit.");
 
       teardown(aMonitor).then(finish);
     });
 
     aDebuggee.performRequests(1);
     window.setTimeout(() => aDebuggee.performRequests(1), 2000);
   });
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -92,34 +92,59 @@ function removeTab(aTab, aWindow) {
   info("Removing tab.");
 
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
 
   targetBrowser.removeTab(aTab);
 }
 
+function waitForNavigation(aTarget) {
+  let deferred = promise.defer();
+  aTarget.once("will-navigate", () => {
+    aTarget.once("navigate", () => {
+      deferred.resolve();
+    });
+  });
+  return deferred.promise;
+}
+
+function reconfigureTab(aTarget, aOptions) {
+  let deferred = promise.defer();
+  aTarget.activeTab.reconfigure(aOptions, deferred.resolve);
+  return deferred.promise;
+};
+
+function toggleCache(aTarget, aEnabled) {
+  let options = { cacheEnabled: aEnabled, performReload: true };
+  let navigationFinished = waitForNavigation(aTarget);
+  return reconfigureTab(aTarget, options).then(() => navigationFinished);
+}
+
 function initNetMonitor(aUrl, aWindow) {
   info("Initializing a network monitor pane.");
 
-  return addTab(aUrl).then((aTab) => {
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
     info("Net tab added successfully: " + aUrl);
 
-    let deferred = promise.defer();
-    let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
-    let target = TargetFactory.forTab(aTab);
+    let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
+    let target = TargetFactory.forTab(tab);
+
+    yield target.makeRemote();
+    info("Target remoted.");
 
-    gDevTools.showToolbox(target, "netmonitor").then((aToolbox) => {
-      info("Netork monitor pane shown successfully.");
+    yield toggleCache(target, false);
+    info("Network cache disabled");
 
-      let monitor = aToolbox.getCurrentPanel();
-      deferred.resolve([aTab, debuggee, monitor]);
-    });
+    let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
+    info("Netork monitor pane shown successfully.");
 
-    return deferred.promise;
+    let monitor = toolbox.getCurrentPanel();
+    return [tab, debuggee, monitor];
   });
 }
 
 function restartNetMonitor(aMonitor, aNewUrl) {
   info("Restarting the specified network monitor.");
 
   let deferred = promise.defer();
   let tab = aMonitor.target.tab;
--- a/browser/devtools/netmonitor/test/html_content-type-test-page.html
+++ b/browser/devtools/netmonitor/test/html_content-type-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Content type test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_content-type-without-cache-test-page.html
+++ b/browser/devtools/netmonitor/test/html_content-type-without-cache-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Content type test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_copy-as-curl.html
+++ b/browser/devtools/netmonitor/test/html_copy-as-curl.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Performing a GET request</p>
 
     <script type="text/javascript">
       function performRequest(aUrl) {
--- a/browser/devtools/netmonitor/test/html_curl-utils.html
+++ b/browser/devtools/netmonitor/test/html_curl-utils.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Performing requests</p>
 
     <p>
       <canvas width="100" height="100"></canvas>
--- a/browser/devtools/netmonitor/test/html_custom-get-page.html
+++ b/browser/devtools/netmonitor/test/html_custom-get-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Performing a custom number of GETs</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_cyrillic-test-page.html
+++ b/browser/devtools/netmonitor/test/html_cyrillic-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Cyrillic type test</p>
     <p>Братан, ты вообще качаешься?</p>
 
     <script type="text/javascript">
--- a/browser/devtools/netmonitor/test/html_filter-test-page.html
+++ b/browser/devtools/netmonitor/test/html_filter-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Filtering test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_infinite-get-page.html
+++ b/browser/devtools/netmonitor/test/html_infinite-get-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Infinite GETs</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_json-custom-mime-test-page.html
+++ b/browser/devtools/netmonitor/test/html_json-custom-mime-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>JSONP test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_json-long-test-page.html
+++ b/browser/devtools/netmonitor/test/html_json-long-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>JSON long string test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_json-malformed-test-page.html
+++ b/browser/devtools/netmonitor/test/html_json-malformed-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>JSON malformed test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_json-text-mime-test-page.html
+++ b/browser/devtools/netmonitor/test/html_json-text-mime-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>JSON text test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_jsonp-test-page.html
+++ b/browser/devtools/netmonitor/test/html_jsonp-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>JSONP test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/html_navigate-test-page.html
+++ b/browser/devtools/netmonitor/test/html_navigate-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Navigation test</p>
   </body>
 
 </html>
--- a/browser/devtools/netmonitor/test/html_params-test-page.html
+++ b/browser/devtools/netmonitor/test/html_params-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Request params type test</p>
 
     <script type="text/javascript">
       function post(aAddress, aQuery, aContentType, aPostBody) {
--- a/browser/devtools/netmonitor/test/html_post-data-test-page.html
+++ b/browser/devtools/netmonitor/test/html_post-data-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
     <style>
       input {
         display: block;
         margin: 12px;
       }
     </style>
   </head>
--- a/browser/devtools/netmonitor/test/html_post-raw-test-page.html
+++ b/browser/devtools/netmonitor/test/html_post-raw-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>POST raw test</p>
 
     <script type="text/javascript">
       function post(aAddress, aMessage, aCallback) {
--- a/browser/devtools/netmonitor/test/html_post-raw-with-headers-test-page.html
+++ b/browser/devtools/netmonitor/test/html_post-raw-with-headers-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>POST raw with headers test</p>
 
     <script type="text/javascript">
       function post(aAddress, aMessage, aCallback) {
--- a/browser/devtools/netmonitor/test/html_simple-test-page.html
+++ b/browser/devtools/netmonitor/test/html_simple-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Simple test</p>
   </body>
 
 </html>
--- a/browser/devtools/netmonitor/test/html_single-get-page.html
+++ b/browser/devtools/netmonitor/test/html_single-get-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Performing a custom number of GETs</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
@@ -20,15 +23,14 @@
           if (this.readyState == this.DONE) {
             aCallback();
           }
         };
         xhr.send(null);
       }
 
       (function performRequests() {
-        get("request_0", function() {
-        });
+        get("request_0", function() {});
       })();
     </script>
   </body>
 
 </html>
--- a/browser/devtools/netmonitor/test/html_sorting-test-page.html
+++ b/browser/devtools/netmonitor/test/html_sorting-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Sorting test</p>
 
     <script type="text/javascript">
       function get(aAddress, aIndex, aCallback) {
--- a/browser/devtools/netmonitor/test/html_statistics-test-page.html
+++ b/browser/devtools/netmonitor/test/html_statistics-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Statistics test</p>
 
     <script type="text/javascript">
       function get(aAddress) {
@@ -25,13 +28,13 @@
       get("sjs_content-type-test-server.sjs?sts=304&fmt=js");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=json");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=font");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=image");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=audio");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=video");
       get("sjs_content-type-test-server.sjs?sts=304&fmt=flash");
-      get("test-image.png");
+      get("test-image.png?v=" + Math.random());
     </script>
   </body>
 
 </html>
--- a/browser/devtools/netmonitor/test/html_status-codes-test-page.html
+++ b/browser/devtools/netmonitor/test/html_status-codes-test-page.html
@@ -1,15 +1,18 @@
 <!-- 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>
   </head>
 
   <body>
     <p>Status codes test</p>
 
     <script type="text/javascript">
       function get(aAddress, aCallback) {
--- a/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
@@ -8,177 +8,181 @@ function handleRequest(request, response
 
   let params = request.queryString.split("&");
   let format = (params.filter((s) => s.contains("fmt="))[0] || "").split("=")[1];
   let status = (params.filter((s) => s.contains("sts="))[0] || "").split("=")[1] || 200;
 
   let cachedCount = 0;
   let cacheExpire = 60; // seconds
 
-  function maybeMakeCached() {
+  function setCacheHeaders() {
     if (status != 304) {
+      response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+      response.setHeader("Pragma", "no-cache");
+      response.setHeader("Expires", "0");
       return;
     }
     // Spice things up a little!
     if (cachedCount % 2) {
       response.setHeader("Cache-Control", "max-age=" + cacheExpire, false);
     } else {
       response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
     }
     cachedCount++;
   }
 
   Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
     switch (format) {
       case "txt": {
         response.setStatusLine(request.httpVersion, status, "DA DA DA");
         response.setHeader("Content-Type", "text/plain", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("Братан, ты вообще качаешься?");
         response.finish();
         break;
       }
       case "xml": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("<label value='greeting'>Hello XML!</label>");
         response.finish();
         break;
       }
       case "html": {
         let content = params.filter((s) => s.contains("res="))[0].split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write(content || "<p>Hello HTML!</p>");
         response.finish();
         break;
       }
       case "html-long": {
         let str = new Array(102400 /* 100 KB in bytes */).join(".");
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("<p>" + str + "</p>");
         response.finish();
         break;
       }
       case "css": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/css; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("body:pre { content: 'Hello CSS!' }");
         response.finish();
         break;
       }
       case "js": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("function() { return 'Hello JS!'; }");
         response.finish();
         break;
       }
       case "json": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("{ \"greeting\": \"Hello JSON!\" }");
         response.finish();
         break;
       }
       case "jsonp": {
         let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
         response.finish();
         break;
       }
       case "jsonp2": {
         let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write(" " + fun + " ( { \"greeting\": \"Hello weird JSONP!\" } ) ; ");
         response.finish();
         break;
       }
       case "json-long": {
         let str = "{ \"greeting\": \"Hello long string JSON!\" },";
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
         response.finish();
         break;
       }
       case "json-malformed": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
         response.finish();
         break;
       }
       case "json-text-mime": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("{ \"greeting\": \"Hello third-party JSON!\" }");
         response.finish();
         break;
       }
       case "json-custom-mime": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }");
         response.finish();
         break;
       }
       case "font": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "font/woff", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.finish();
         break;
       }
       case "image": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "image/png", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.finish();
         break;
       }
       case "audio": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "audio/ogg", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.finish();
         break;
       }
       case "video": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "video/webm", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.finish();
         break;
       }
       case "flash": {
         response.setStatusLine(request.httpVersion, status, "OK");
         response.setHeader("Content-Type", "application/x-shockwave-flash", false);
-        maybeMakeCached();
+        setCacheHeaders();
         response.finish();
         break;
       }
       default: {
         response.setStatusLine(request.httpVersion, 404, "Not Found");
         response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+        setCacheHeaders();
         response.write("<blink>Not Found</blink>");
         response.finish();
         break;
       }
     }
   }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
 }
--- a/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_simple-test-server.sjs
@@ -1,9 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function handleRequest(request, response) {
   response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+  response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+  response.setHeader("Pragma", "no-cache");
+  response.setHeader("Expires", "0");
+
   response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
   response.setHeader("Foo-Bar", "baz", false);
   response.write("Hello world!");
 }
--- a/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs
@@ -6,13 +6,18 @@ const { classes: Cc, interfaces: Ci } = 
 function handleRequest(request, response) {
   response.processAsync();
 
   let params = request.queryString.split("&");
   let index = params.filter((s) => s.contains("index="))[0].split("=")[1];
 
   Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
     response.setStatusLine(request.httpVersion, index == 1 ? 101 : index * 100, "Meh");
+
+    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+    response.setHeader("Pragma", "no-cache");
+    response.setHeader("Expires", "0");
+
     response.setHeader("Content-Type", "text/" + index, false);
     response.write(new Array(index * 10).join(index)); // + 0.01 KB
     response.finish();
   }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
 }
--- a/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_status-codes-test-server.sjs
@@ -22,13 +22,18 @@ function handleRequest(request, response
         break;
       case "400":
         response.setStatusLine(request.httpVersion, 404, "Not Found");
         break;
       case "500":
         response.setStatusLine(request.httpVersion, 501, "Not Implemented");
         break;
     }
+
+    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+    response.setHeader("Pragma", "no-cache");
+    response.setHeader("Expires", "0");
+
     response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
     response.write("Hello status code " + status + "!");
     response.finish();
   }, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
 }
--- a/browser/devtools/shared/test/browser_outputparser.js
+++ b/browser/devtools/shared/test/browser_outputparser.js
@@ -32,29 +32,29 @@ function testParseCssProperty() {
     colorSwatchClass: "test-colorswatch"
   });
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
   target.appendChild(frag);
 
   is(target.innerHTML,
-     '1px solid <span style="background-color:red" class="test-colorswatch"></span><span>#F00</span>',
+     '1px solid <span data-color="#F00"><span style="background-color:red" class="test-colorswatch"></span><span>#F00</span></span>',
      "CSS property correctly parsed");
 
   target.innerHTML = "";
 
   let frag = parser.parseCssProperty("background-image", "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))", {
     colorSwatchClass: "test-colorswatch",
     colorClass: "test-color"
   });
   target.appendChild(frag);
   is(target.innerHTML,
-     'linear-gradient(to right, <span style="background-color:#F60" class="test-colorswatch"></span><span class="test-color">#F60</span> 10%, ' +
-     '<span style="background-color:rgba(0,0,0,1)" class="test-colorswatch"></span><span class="test-color">#000</span>)',
+     'linear-gradient(to right, <span data-color="#F60"><span style="background-color:#F60" class="test-colorswatch"></span><span class="test-color">#F60</span></span> 10%, ' +
+     '<span data-color="#000"><span style="background-color:rgba(0,0,0,1)" class="test-colorswatch"></span><span class="test-color">#000</span></span>)',
      "Gradient CSS property correctly parsed");
 
   target.innerHTML = "";
 
   testParseHTMLAttribute();
 }
 
 function testParseHTMLAttribute() {
@@ -64,17 +64,17 @@ function testParseHTMLAttribute() {
     urlClass: "theme-link",
     colorClass: "theme-color"
   });
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
   target.appendChild(frag);
 
-  let expected = 'color:<span class="theme-color">#F00</span>; font-size: 12px; ' +
+  let expected = 'color:<span data-color="#F00"><span class="theme-color">#F00</span></span>; font-size: 12px; ' +
                  'background-image: url(\'<a href="chrome://branding/content/about-logo.png" ' +
                  'class="theme-link" ' +
                  'target="_blank">chrome://branding/content/about-logo.png</a>\')';
 
   is(target.innerHTML, expected, "HTML Attribute correctly parsed");
   target.innerHTML = "";
   testParseNonCssHTMLAttribute();
 }
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -1006,16 +1006,17 @@ SwatchColorPickerTooltip.prototype = Her
 
   _onSpectrumColorChange: function(event, rgba, cssColor) {
     this._selectColor(cssColor);
   },
 
   _selectColor: function(color) {
     if (this.activeSwatch) {
       this.activeSwatch.style.backgroundColor = color;
+      this.activeSwatch.parentNode.dataset.color = color;
       this.currentSwatchColor.textContent = color;
       this.preview(color);
     }
   },
 
  _openEyeDropper: function() {
     let chromeWindow = this.tooltip.doc.defaultView.top;
     let windowType = chromeWindow.document.documentElement
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -144,16 +144,17 @@ function CssHtmlTree(aStyleInspector, aP
 
   // Create bound methods.
   this.focusWindow = this.focusWindow.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onCopy = this._onCopy.bind(this);
+  this._onCopyColor = this._onCopyColor.bind(this);
 
   this.styleDocument.addEventListener("copy", this._onCopy);
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
 
   // Nodes used in templating
   this.root = this.styleDocument.getElementById("root");
   this.templateRoot = this.styleDocument.getElementById("templateRoot");
@@ -579,16 +580,23 @@ CssHtmlTree.prototype = {
 
     // Copy
     this.menuitemCopy = createMenuItem(this._contextmenu, {
       label: "computedView.contextmenu.copy",
       accesskey: "computedView.contextmenu.copy.accessKey",
       command: this._onCopy
     });
 
+    // Copy color
+    this.menuitemCopyColor = createMenuItem(this._contextmenu, {
+      label: "ruleView.contextmenu.copyColor",
+      accesskey: "ruleView.contextmenu.copyColor.accessKey",
+      command: this._onCopyColor
+    });
+
     // Show Original Sources
     this.menuitemSources= createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.showOrigSources",
       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
       command: this._onToggleOrigSources
     });
 
     let popupset = doc.documentElement.querySelector("popupset");
@@ -614,23 +622,57 @@ CssHtmlTree.prototype = {
       label = "ruleView.contextmenu.showCSSSources";
     }
     this.menuitemSources.setAttribute("label",
                                       CssHtmlTree.l10n(label));
 
     let accessKey = label + ".accessKey";
     this.menuitemSources.setAttribute("accesskey",
                                       CssHtmlTree.l10n(accessKey));
+
+    this.menuitemCopyColor.hidden = !this._isColorPopup();
+  },
+
+  /**
+   * A helper that determines if the popup was opened with a click to a color
+   * value and saves the color to this._colorToCopy.
+   *
+   * @return {Boolean}
+   *         true if click on color opened the popup, false otherwise.
+   */
+  _isColorPopup: function () {
+    this._colorToCopy = "";
+
+    let trigger = this.popupNode;
+    if (!trigger) {
+      return false;
+    }
+
+    let container = (trigger.nodeType == trigger.TEXT_NODE) ?
+                     trigger.parentElement : trigger;
+
+    let isColorNode = el => el.dataset && "color" in el.dataset;
+
+    while (!isColorNode(container)) {
+      container = container.parentNode;
+      if (!container) {
+        return false;
+      }
+    }
+
+    this._colorToCopy = container.dataset["color"];
+    return true;
   },
 
   /**
    * Context menu handler.
    */
   _onContextMenu: function(event) {
     try {
+      this.popupNode = event.explicitOriginalTarget;
       this.styleDocument.defaultView.focus();
       this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
@@ -655,16 +697,20 @@ CssHtmlTree.prototype = {
       event.stopPropagation();
       event.preventDefault();
       let browserWin = this.styleInspector.inspector.target
                            .tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(target.href, "tab");
     }
   },
 
+  _onCopyColor: function() {
+    clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
+  },
+
   /**
    * Copy selected text.
    *
    * @param event The event object
    */
   _onCopy: function(event)
   {
     try {
@@ -746,22 +792,28 @@ CssHtmlTree.prototype = {
       // Destroy the Select All menuitem.
       this.menuitemCopy.removeEventListener("command", this._onCopy);
       this.menuitemCopy = null;
 
       // Destroy the Copy menuitem.
       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
       this.menuitemSelectAll = null;
 
+      // Destroy Copy Color menuitem.
+      this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
+      this.menuitemCopyColor = null;
+
       // Destroy the context menu.
       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
       this._contextmenu.parentNode.removeChild(this._contextmenu);
       this._contextmenu = null;
     }
 
+    this.popupNode = null;
+
     this.tooltip.stopTogglingOnHover(this.propertyContainer);
     this.tooltip.destroy();
 
     // Remove bound listeners
     this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
     this.styleDocument.removeEventListener("copy", this._onCopy);
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
 
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1074,16 +1074,17 @@ function CssRuleView(aInspector, aDoc, a
   this.element.flex = 1;
 
   this._outputParser = new OutputParser();
 
   this._buildContextMenu = this._buildContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onCopy = this._onCopy.bind(this);
+  this._onCopyColor = this._onCopyColor.bind(this);
   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
 
   this.element.addEventListener("copy", this._onCopy);
 
   this._handlePrefChange = this._handlePrefChange.bind(this);
   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
 
   this._prefObserver = new PrefObserver("devtools.");
@@ -1133,16 +1134,21 @@ CssRuleView.prototype = {
       accesskey: "ruleView.contextmenu.selectAll.accessKey",
       command: this._onSelectAll
     });
     this.menuitemCopy = createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.copy",
       accesskey: "ruleView.contextmenu.copy.accessKey",
       command: this._onCopy
     });
+    this.menuitemCopyColor = createMenuItem(this._contextmenu, {
+      label: "ruleView.contextmenu.copyColor",
+      accesskey: "ruleView.contextmenu.copyColor.accessKey",
+      command: this._onCopyColor
+    });
     this.menuitemSources= createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.showOrigSources",
       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
       command: this._onToggleOrigSources
     });
 
     let popupset = doc.documentElement.querySelector("popupset");
     if (!popupset) {
@@ -1256,31 +1262,63 @@ CssRuleView.prototype = {
           selectionStart !== selectionEnd) {
         copy = true;
       }
     } else {
       // No text selected, disable copy.
       copy = false;
     }
 
+    this.menuitemCopyColor.hidden = !this._isColorPopup();
     this.menuitemCopy.disabled = !copy;
 
     let label = "ruleView.contextmenu.showOrigSources";
     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
       label = "ruleView.contextmenu.showCSSSources";
     }
     this.menuitemSources.setAttribute("label",
                                       _strings.GetStringFromName(label));
 
     let accessKey = label + ".accessKey";
     this.menuitemSources.setAttribute("accesskey",
                                       _strings.GetStringFromName(accessKey));
   },
 
   /**
+   * A helper that determines if the popup was opened with a click to a color
+   * value and saves the color to this._colorToCopy.
+   *
+   * @return {Boolean}
+   *         true if click on color opened the popup, false otherwise.
+   */
+  _isColorPopup: function () {
+    this._colorToCopy = "";
+
+    let trigger = this.doc.popupNode;
+    if (!trigger) {
+      return false;
+    }
+
+    let container = (trigger.nodeType == trigger.TEXT_NODE) ?
+                     trigger.parentElement : trigger;
+
+    let isColorNode = el => el.dataset && "color" in el.dataset;
+
+    while (!isColorNode(container)) {
+      container = container.parentNode;
+      if (!container) {
+        return false;
+      }
+    }
+
+    this._colorToCopy = container.dataset["color"];
+    return true;
+  },
+
+  /**
    * Select all text.
    */
   _onSelectAll: function() {
     let win = this.doc.defaultView;
     let selection = win.getSelection();
 
     selection.selectAllChildren(this.doc.documentElement);
   },
@@ -1323,16 +1361,23 @@ CssRuleView.prototype = {
       clipboardHelper.copyString(text, this.doc);
       event.preventDefault();
     } catch(e) {
       console.error(e);
     }
   },
 
   /**
+   * Copy the most recently selected color value to clipboard.
+   */
+  _onCopyColor: function() {
+    clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
+  },
+
+  /**
    *  Toggle the original sources pref.
    */
   _onToggleOrigSources: function() {
     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
   setPageStyle: function(aPageStyle) {
@@ -1395,16 +1440,20 @@ CssRuleView.prototype = {
       // Destroy the Select All menuitem.
       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
       this.menuitemSelectAll = null;
 
       // Destroy the Copy menuitem.
       this.menuitemCopy.removeEventListener("command", this._onCopy);
       this.menuitemCopy = null;
 
+      // Destroy Copy Color menuitem.
+      this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
+      this.menuitemCopyColor = null;
+
       this.menuitemSources.removeEventListener("command", this._onToggleOrigSources);
       this.menuitemSources = null;
 
       // Destroy the context menu.
       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
       this._contextmenu.parentNode.removeChild(this._contextmenu);
       this._contextmenu = null;
     }
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -82,16 +82,18 @@ skip-if = os == "win" && debug # bug 963
 [browser_ruleview_refresh-on-attribute-change_02.js]
 [browser_ruleview_refresh-on-style-change.js]
 [browser_ruleview_select-and-copy-styles.js]
 [browser_ruleview_style-editor-link.js]
 [browser_ruleview_urls-clickable.js]
 [browser_ruleview_user-agent-styles.js]
 [browser_ruleview_user-agent-styles-uneditable.js]
 [browser_ruleview_user-property-reset.js]
+[browser_styleinspector_context-menu-copy-color_01.js]
+[browser_styleinspector_context-menu-copy-color_02.js]
 [browser_styleinspector_csslogic-content-stylesheets.js]
 [browser_styleinspector_csslogic-inherited-properties.js]
 [browser_styleinspector_csslogic-specificity.js]
 [browser_styleinspector_inplace-editor.js]
 [browser_styleinspector_output-parser.js]
 [browser_styleinspector_tooltip-background-image.js]
 [browser_styleinspector_tooltip-closes-on-new-selection.js]
 [browser_styleinspector_tooltip-longhand-fontfamily.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -0,0 +1,139 @@
+/* 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/ */
+"use strict";
+
+// Test "Copy color" item of the context menu #1: Test _isColorPopup.
+
+const TEST_COLOR = "#123ABC";
+const COLOR_SELECTOR = "span[data-color]";
+
+let test = asyncTest(function* () {
+  const TEST_DOC = '<html>                                              \
+                      <body>                                            \
+                        <div style="color: ' + TEST_COLOR + ';          \
+                                    margin: 0px;                        \
+                                    background: ' + TEST_COLOR + ';">   \
+                          Test "Copy color" context menu option         \
+                        </div>                                          \
+                      </body>                                           \
+                    </html>';
+
+  const TEST_CASES = [
+    {
+      viewName: "RuleView",
+      initializer: openRuleView
+    },
+    {
+      viewName: "ComputedView",
+      initializer: openComputedView
+    }
+  ];
+
+  yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
+
+  for (let test of TEST_CASES) {
+    yield testView(test);
+  }
+});
+
+function* testView({viewName, initializer}) {
+  info("Testing " + viewName);
+
+  let {inspector, view} = yield initializer();
+  yield selectNode("div", inspector);
+
+  testIsColorValueNode(view);
+  testIsColorPopupOnAllNodes(view);
+  yield clearCurrentNodeSelection(inspector);
+}
+
+/**
+ * A function testing that isColorValueNode correctly detects nodes part of
+ * color values.
+ */
+function testIsColorValueNode(view) {
+  info("Testing that child nodes of color nodes are detected.");
+  let root = rootElement(view);
+  let colorNode = root.querySelector(COLOR_SELECTOR);
+
+  ok(colorNode, "Color node found");
+  for (let node of iterateNodes(colorNode)) {
+    ok(isColorValueNode(node), "Node is part of color value.");
+  }
+}
+
+/**
+ * A function testing that _isColorPopup returns a correct value for all nodes
+ * in the view.
+ */
+function testIsColorPopupOnAllNodes(view) {
+  let root = rootElement(view);
+  for (let node of iterateNodes(root)) {
+    testIsColorPopupOnNode(view, node);
+  }
+}
+
+/**
+ * Test result of _isColorPopup with given node.
+ * @param object view
+ *               A CSSRuleView or CssHtmlTree instance.
+ * @param Node node
+ *             A node to check.
+ */
+function testIsColorPopupOnNode(view, node) {
+  info("Testing node " + node);
+  if (view.doc) {
+    view.doc.popupNode = node;
+  }
+  else {
+    view.popupNode = node;
+  }
+  view._colorToCopy = "";
+
+  let result = view._isColorPopup();
+  let correct = isColorValueNode(node);
+
+  is(result, correct, "_isColorPopup returned the expected value " + correct);
+  is(view._colorToCopy, (correct) ? TEST_COLOR : "",
+     "_colorToCopy was set to the expected value");
+}
+
+/**
+ * Check if a node is part of color value i.e. it has parent with a 'data-color'
+ * attribute.
+ */
+function isColorValueNode(node) {
+  let container = (node.nodeType == node.TEXT_NODE) ?
+                   node.parentElement : node;
+
+  let isColorNode = el => el.dataset && "color" in el.dataset;
+
+  while (!isColorNode(container)) {
+    container = container.parentNode;
+    if (!container) {
+      info("No color. Node is not part of color value.");
+      return false;
+    }
+  }
+
+  info("Found a color. Node is part of color value.");
+
+  return true;
+}
+
+/**
+ * A generator that iterates recursively trough all child nodes of baseNode.
+ */
+function* iterateNodes(baseNode) {
+  yield baseNode;
+
+  for (let child of baseNode.childNodes) {
+    yield* iterateNodes(child);
+  }
+}
+
+/**
+ * Returns the root element for the given view, rule or computed.
+ */
+let rootElement = view => (view.element) ? view.element : view.styleDocument;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -0,0 +1,99 @@
+/* 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/ */
+"use strict";
+
+// Test "Copy color" item of the context menu #2: Test that correct color is
+// copied if the color changes.
+
+const TEST_COLOR = "#123ABC";
+
+let test = asyncTest(function* () {
+  const PAGE_CONTENT = [
+    '<style type="text/css">',
+    '  div {',
+    '    color: ' + TEST_COLOR + ';',
+    '  }',
+    '</style>',
+    '<div>Testing the color picker tooltip!</div>'
+  ].join("\n");
+
+  yield addTab("data:text/html;charset=utf8,Test context menu Copy color");
+  content.document.body.innerHTML = PAGE_CONTENT;
+
+  let {inspector, view} = yield openRuleView();
+  yield testCopyToClipboard(inspector, view);
+  yield testManualEdit(inspector, view);
+  yield testColorPickerEdit(inspector, view);
+});
+
+function* testCopyToClipboard(inspector, view) {
+  info("Testing that color is copied to clipboard");
+
+  yield selectNode("div", inspector);
+
+  let win = view.doc.defaultView;
+  let element = getRuleViewProperty(view, "div", "color").valueSpan
+    .querySelector(".ruleview-colorswatch");
+
+  let popup = once(view._contextmenu, "popupshown");
+  EventUtils.synthesizeMouseAtCenter(element, {button: 2, type: "contextmenu"}, win);
+  yield popup;
+
+  ok(!view.menuitemCopyColor.hidden, "Copy color is visible");
+
+  yield waitForClipboard(() => view.menuitemCopyColor.click(), TEST_COLOR);
+  view._contextmenu.hidePopup();
+}
+
+function* testManualEdit(inspector, view) {
+  info("Testing manually edited colors");
+  yield selectNode("div", inspector);
+
+  let {valueSpan} = getRuleViewProperty(view, "div", "color");
+
+  let newColor = "#C9184E"
+  let editor = yield focusEditableField(valueSpan);
+
+  info("Typing new value");
+  let input = editor.input;
+  let onBlur = once(input, "blur");
+  for (let ch of newColor + ";"){
+    EventUtils.sendChar(ch, view.doc.defaultView);
+  }
+
+  yield onBlur;
+  yield wait(1);
+
+  let colorValue = getRuleViewProperty(view, "div", "color").valueSpan.firstChild;
+  is(colorValue.dataset.color, newColor, "data-color was updated");
+
+  view.doc.popupNode = colorValue;
+  view._isColorPopup();
+
+  is(view._colorToCopy, newColor, "_colorToCopy has the new value");
+}
+
+function* testColorPickerEdit(inspector, view) {
+  info("Testing colors edited via color picker");
+  yield selectNode("div", inspector);
+
+  let swatch = getRuleViewProperty(view, "div", "color").valueSpan
+    .querySelector(".ruleview-colorswatch");
+
+  info("Opening the color picker");
+  let picker = view.colorPicker;
+  let onShown = picker.tooltip.once("shown");
+  swatch.click();
+  yield onShown;
+
+  let rgbaColor = [83, 183, 89, 1];
+  let rgbaColorText = "rgba(83, 183, 89, 1)";
+  yield simulateColorPickerChange(picker, rgbaColor);
+
+  is(swatch.parentNode.dataset.color, rgbaColorText, "data-color was updated");
+  view.doc.popupNode = swatch;
+  view._isColorPopup();
+
+  is(view._colorToCopy, rgbaColorText, "_colorToCopy has the new value");
+}
--- a/browser/devtools/styleinspector/test/browser_styleinspector_output-parser.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_output-parser.js
@@ -68,17 +68,17 @@ function test() {
       test: fragment => {
         is(countAll(fragment), 0);
       }
     },
     {
       name: "background-color",
       value: "transparent",
       test: fragment => {
-        is(countAll(fragment), 1);
+        is(countAll(fragment), 2);
         is(countColors(fragment), 1);
         is(fragment.textContent, "transparent");
       }
     },
     {
       name: "color",
       value: "red",
       test: fragment => {
@@ -93,17 +93,17 @@ function test() {
         is(countColors(fragment), 1);
         is(fragment.textContent, "#F06");
       }
     },
     {
       name: "border",
       value: "80em dotted pink",
       test: fragment => {
-        is(countAll(fragment), 1);
+        is(countAll(fragment), 2);
         is(countColors(fragment), 1);
         is(getColor(fragment), "pink");
       }
     },
     {
       name: "color",
       value: "red !important",
       test: fragment => {
@@ -114,28 +114,28 @@ function test() {
     {
       name: "background",
       value: "red url(test.png) repeat top left",
       test: fragment => {
         is(countColors(fragment), 1);
         is(countUrls(fragment), 1);
         is(getColor(fragment), "red");
         is(getUrl(fragment), "test.png");
-        is(countAll(fragment), 2);
+        is(countAll(fragment), 3);
       }
     },
     {
       name: "background",
       value: "blue url(test.png) repeat top left !important",
       test: fragment => {
         is(countColors(fragment), 1);
         is(countUrls(fragment), 1);
         is(getColor(fragment), "blue");
         is(getUrl(fragment), "test.png");
-        is(countAll(fragment), 2);
+        is(countAll(fragment), 3);
       }
     },
     {
       name: "list-style-image",
       value: "url(\"images/arrow.gif\")",
       test: fragment => {
         is(countAll(fragment), 1);
         is(getUrl(fragment), "images/arrow.gif");
@@ -158,42 +158,42 @@ function test() {
         is(countUrls(fragment), 1);
         is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid");
       }
     },
     {
       name: "background",
       value: "linear-gradient(to right, rgba(183,222,237,1) 0%, rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, #F06 75%, red 100%)",
       test: fragment => {
-        is(countAll(fragment), 5);
+        is(countAll(fragment), 10);
         let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
         is(allSwatches.length, 5);
         is(allSwatches[0].textContent, "rgba(183,222,237,1)");
         is(allSwatches[1].textContent, "rgba(33,180,226,1)");
         is(allSwatches[2].textContent, "rgba(31,170,217,.5)");
         is(allSwatches[3].textContent, "#F06");
         is(allSwatches[4].textContent, "red");
       }
     },
     {
       name: "background",
       value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)",
       test: fragment => {
-        is(countAll(fragment), 2);
+        is(countAll(fragment), 4);
         let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
         is(allSwatches.length, 2);
         is(allSwatches[0].textContent, "orange");
         is(allSwatches[1].textContent, "red");
       }
     },
     {
       name: "background",
       value: "white  url(http://test.com/wow_such_image.png) no-repeat top left",
       test: fragment => {
-        is(countAll(fragment), 2);
+        is(countAll(fragment), 3);
         is(countUrls(fragment), 1);
         is(countColors(fragment), 1);
       }
     },
     {
       name: "background",
       value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")",
       test: fragment => {
@@ -208,17 +208,17 @@ function test() {
         is(countAll(fragment), 1);
         is(getUrl(fragment), "this-is-an-incredible-image.jpeg");
       }
     },
     {
       name: "background",
       value: "red url(    \"http://wow.com/cool/../../../you're(doingit)wrong\"   ) repeat center",
       test: fragment => {
-        is(countAll(fragment), 2);
+        is(countAll(fragment), 3);
         is(countColors(fragment), 1);
         is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong");
       }
     },
     {
       name: "background-image",
       value: "url(../../../look/at/this/folder/structure/../../red.blue.green.svg   )",
       test: fragment => {
--- a/browser/devtools/webaudioeditor/webaudioeditor.xul
+++ b/browser/devtools/webaudioeditor/webaudioeditor.xul
@@ -51,21 +51,23 @@
          flex="1"
          hidden="true">
       <toolbar id="web-audio-toolbar" class="devtools-toolbar">
         <spacer flex="1"></spacer>
         <toolbarbutton id="inspector-pane-toggle" class="devtools-toolbarbutton"
                        tabindex="0"/>
       </toolbar>
       <splitter class="devtools-horizontal-splitter"/>
-      <box id="web-audio-content-pane" flex="1">
+      <box id="web-audio-content-pane"
+           class="devtools-responsive-container"
+           flex="1">
         <hbox flex="1">
-          <box id="web-audio-graph" class="devtools-responsive-container" flex="1">
+          <box id="web-audio-graph" flex="1">
             <vbox flex="1">
-              <svg id="graph-svg" flex="1" viewBox="0 0 1000 500"
+              <svg id="graph-svg"
                   xmlns="http://www.w3.org/2000/svg"
                   xmlns:xlink="http://www.w3.org/1999/xlink">
                 <g id="graph-target" transform="translate(20,20)"/>
               </svg>
             </vbox>
           </box>
         </hbox>
         <splitter class="devtools-side-splitter"/>
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -1391,12 +1391,12 @@ mediaEmulateDesc=Emulate a specified CSS
 mediaEmulateManual=View the document as if rendered on a device supporting the given media type, with the relevant CSS rules applied.
 mediaEmulateType=The media type to emulate
 mediaResetDesc=Stop emulating a CSS media type
 
 # LOCALIZATION NOTE (injectDesc, injectManual, injectLibraryDesc, injectLoaded,
 # injectFailed) These strings describe the 'inject' commands and all available
 # parameters.
 injectDesc=Inject common libraries into the page
-injectManual=Inject common libraries into the content of the page which can also be accessed from the Firefox console.
+injectManual2=Inject common libraries into the content of the page which can also be accessed from the console.
 injectLibraryDesc=Select the library to inject or enter a valid script URI to inject
 injectLoaded=%1$S loaded
 injectFailed=Failed to load %1$S - Invalid URI
--- a/browser/themes/osx/preferences/in-content/preferences.css
+++ b/browser/themes/osx/preferences/in-content/preferences.css
@@ -81,8 +81,16 @@ spinbuttons {
   -moz-margin-start: 2px;
   -moz-margin-end: 8px !important;
 }
 
 description {
   font-size: 1.25rem;
   line-height: 22px;
 }
+
+#popupPolicyRow {
+  /* Override styles from
+     browser/themes/osx/preferences/preferences.css */
+  margin-bottom: 0 !important;
+  padding-bottom: 0 !important;
+  border-bottom: none;
+}
--- a/browser/themes/osx/preferences/preferences.css
+++ b/browser/themes/osx/preferences/preferences.css
@@ -129,19 +129,17 @@ caption {
   padding-top: 8px;
 }
 
 #paneContent row {
   padding: 2px 4px;
   -moz-box-align: center;
 }
 
-#popupPolicyRow,
-#enableSoftwareInstallRow,
-#enableImagesRow {
+#popupPolicyRow {
   margin-bottom: 4px !important;
   padding-bottom: 4px !important;
   border-bottom: 1px solid #ccc;
 }
 
 #browserUseCurrent,
 #browserUseBookmark,
 #browserUseBlank {
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -134,17 +134,19 @@ panelmultiview[nosubviews=true] > .panel
 
 #PanelUI-popup .panel-subview-body {
   margin: -4px;
   padding: 4px 4px;
 }
 
 .panel-subview-header,
 .subviewbutton.panel-subview-footer {
-  padding: 12px;
+  box-sizing: border-box;
+  min-height: 41px;
+  padding: 11px 12px;
 }
 
 .panel-subview-header {
   margin: -4px -4px 4px;
   background-color: hsla(210,4%,10%,.05);
   box-shadow: 0 -1px 0 hsla(210,4%,10%,.05) inset;
   color: hsl(0,0%,50%);
 }
@@ -485,18 +487,19 @@ toolbarpaletteitem[place="palette"] > to
   margin: 0;
 }
 
 #PanelUI-help,
 #PanelUI-fxa-status,
 #PanelUI-customize,
 #PanelUI-quit {
   margin: 0;
-  padding: 10px 0;
-  min-height: 2em;
+  padding: 11px 0;
+  box-sizing: border-box;
+  min-height: 40px;
   -moz-appearance: none;
   box-shadow: none;
   border: none;  
   border-radius: 0;
   transition: background-color;
   -moz-box-orient: horizontal;
 }
 
--- a/browser/themes/shared/devtools/webaudioeditor.inc.css
+++ b/browser/themes/shared/devtools/webaudioeditor.inc.css
@@ -35,18 +35,18 @@
 }
 
 #requests-menu-reload-notice-button {
   min-height: 2em;
 }
 
 /* Context Graph */
 svg {
-  position: fixed;
   overflow: hidden;
+  -moz-box-flex: 1;
 }
 
 /* Edges in graph */
 .edgePath path {
   stroke-width: 1px;
   fill: none;
 }
 
@@ -106,22 +106,16 @@ text {
 .web-audio-inspector .error {
   background-image: url(alerticon-warning.png);
   background-size: 13px 12px;
   -moz-appearance: none;
   opacity: 0;
   transition: opacity .5s ease-out 0s;
 }
 
-@media (min-resolution: 2dppx) {
-  .web-audio-inspector .error {
-    background-image: url(alerticon-warning@2x.png);
-  }
-}
-
 #inspector-pane-toggle {
   background: none;
   box-shadow: none;
   border: none;
   list-style-image: url(debugger-collapse.png);
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
@@ -146,9 +140,33 @@ text {
 
   #inspector-pane-toggle[pane-collapsed] {
     list-style-image: url(debugger-expand@2x.png);
   }
 
   #inspector-pane-toggle:active {
     -moz-image-region: rect(0px,64px,32px,32px);
   }
-}
\ No newline at end of file
+
+  .web-audio-inspector .error {
+    background-image: url(alerticon-warning@2x.png);
+  }
+}
+
+/**
+ * Responsive Styles
+ * `.devtools-responsive-container` takes care of most of
+ * the changing of host types.
+ */
+@media (max-width: 700px) {
+  /**
+   * Override the inspector toggle so it's always open
+   * in the portrait view, with the toggle button hidden.
+   */
+  #inspector-pane-toggle {
+    display: none;
+  }
+
+  #web-audio-inspector {
+    margin-left: 0px !important;
+    margin-right: 0px !important;
+  }
+}
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -319,29 +319,71 @@ case "$target" in
     if test -d "$android_build_tools" -a -f "$android_build_tools/aapt"; then
         AC_MSG_RESULT([$android_build_tools])
     else
         AC_MSG_ERROR([not found. Please check your SDK for the subdirectory of build-tools. With the current configuration, it should be in $android_sdk_root/build_tools])
     fi
 
     ANDROID_SDK="${android_sdk}"
     ANDROID_SDK_ROOT="${android_sdk_root}"
-    if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
-        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
+
+    AC_MSG_CHECKING([for compat library dirs])
+    if test -e "${android_sdk_root}/extras/android/compatibility/v4/android-support-v4.jar" ; then
+        ANDROID_COMPAT_DIR_BASE="${android_sdk_root}/extras/android/compatibility";
     else
-        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
+        ANDROID_COMPAT_DIR_BASE="${android_sdk_root}/extras/android/support";
     fi
+    AC_MSG_RESULT([$ANDROID_COMPAT_DIR_BASE])
+
     ANDROID_TOOLS="${android_tools}"
     ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
     ANDROID_BUILD_TOOLS="${android_build_tools}"
     AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
+
+    ANDROID_COMPAT_LIB=$ANDROID_COMPAT_DIR_BASE/v4/android-support-v4.jar
+    AC_MSG_CHECKING([for v4 compat library])
     AC_SUBST(ANDROID_COMPAT_LIB)
     if ! test -e $ANDROID_COMPAT_LIB ; then
-        AC_MSG_ERROR([You must download the Android support library when targeting Android.   Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_COMPAT_LIB)])
+        AC_MSG_ERROR([You must download the Android v4 support library when targeting Android.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_COMPAT_LIB)])
+    fi
+    AC_MSG_RESULT([$ANDROID_COMPAT_LIB])
+
+    if test -n "$MOZ_NATIVE_DEVICES" ; then
+        AC_SUBST(MOZ_NATIVE_DEVICES)
+
+        AC_MSG_CHECKING([for google play services])
+        GOOGLE_PLAY_SERVICES_LIB="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/libs/google-play-services.jar"
+        GOOGLE_PLAY_SERVICES_RES="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/res"
+        AC_SUBST(GOOGLE_PLAY_SERVICES_LIB)
+        AC_SUBST(GOOGLE_PLAY_SERVICES_RES)
+        if ! test -e $GOOGLE_PLAY_SERVICES_LIB ; then
+            AC_MSG_ERROR([You must download Google Play Services to build with native video casting support enabled.  Run the Android SDK tool and install Google Play Services under Extras.  See http://developer.android.com/google/play-services/setup.html for more info. (looked for $GOOGLE_PLAY_SERVICES_LIB) ])
+        fi
+        AC_MSG_RESULT([$GOOGLE_PLAY_SERVICES_LIB])
+
+        ANDROID_APPCOMPAT_LIB="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/libs/android-support-v7-appcompat.jar"
+        ANDROID_APPCOMPAT_RES="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/res"
+        AC_MSG_CHECKING([for v7 appcompat library])
+        if ! test -e $ANDROID_APPCOMPAT_LIB ; then
+            AC_MSG_ERROR([You must download the v7 app compat Android support library when targeting Android with native video casting support enabled.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_APPCOMPAT_LIB)])
+        fi
+        AC_MSG_RESULT([$ANDROID_APPCOMPAT_LIB])
+        AC_SUBST(ANDROID_APPCOMPAT_LIB)
+        AC_SUBST(ANDROID_APPCOMPAT_RES)
+
+        ANDROID_MEDIAROUTER_LIB="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/libs/android-support-v7-mediarouter.jar"
+        ANDROID_MEDIAROUTER_RES="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/res"
+        AC_MSG_CHECKING([for v7 mediarouter library])
+        if ! test -e $ANDROID_MEDIAROUTER_LIB ; then
+            AC_MSG_ERROR([You must download the v7 media router Android support library when targeting Android with native video casting support enabled.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_MEDIAROUTER_LIB)])
+        fi
+        AC_MSG_RESULT([$ANDROID_MEDIAROUTER_LIB])
+        AC_SUBST(ANDROID_MEDIAROUTER_LIB)
+        AC_SUBST(ANDROID_MEDIAROUTER_RES)
     fi
 
     MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$ANDROID_TOOLS])
     MOZ_PATH_PROG(DX, dx, :, [$ANDROID_BUILD_TOOLS])
     MOZ_PATH_PROG(AAPT, aapt, :, [$ANDROID_BUILD_TOOLS])
     MOZ_PATH_PROG(AIDL, aidl, :, [$ANDROID_BUILD_TOOLS])
     MOZ_PATH_PROG(ADB, adb, :, [$ANDROID_PLATFORM_TOOLS])
 
--- a/build/mobile/robocop/Makefile.in
+++ b/build/mobile/robocop/Makefile.in
@@ -90,12 +90,12 @@ include $(topsrcdir)/config/rules.mk
 tools:: $(ANDROID_APK_NAME).apk
 
 GENERATED_DIRS += $(dir-tests)
 
 # The test APK needs to know the contents of the target APK while not
 # being linked against them.  This is a best effort to avoid getting
 # out of sync with base's build config.
 JARS_DIR := $(DEPTH)/mobile/android/base
-JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(JARS_DIR)/*.jar))
+JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(JARS_DIR)/*.jar)):$(ANDROID_COMPAT_LIB)
 # We also want to re-compile classes.dex when the associated base
 # content changes.
 classes.dex: $(wildcard $(JARS_DIR)/*.jar)
--- a/config/android-common.mk
+++ b/config/android-common.mk
@@ -1,39 +1,35 @@
 # 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/.
 
-# Ensure JAVA_CLASSPATH and ANDROID_SDK are defined before including this file.
+# Ensure ANDROID_SDK is defined before including this file.
 # We use common android defaults for boot class path and java version.
 ifndef ANDROID_SDK
   $(error ANDROID_SDK must be defined before including android-common.mk)
 endif
 
-ifndef JAVA_CLASSPATH
-  $(error JAVA_CLASSPATH must be defined before including android-common.mk)
-endif
-
 # DEBUG_JARSIGNER always debug signs.
 DEBUG_JARSIGNER=$(PYTHON) $(abspath $(topsrcdir)/mobile/android/debug_sign_tool.py) \
   --keytool=$(KEYTOOL) \
   --jarsigner=$(JARSIGNER) \
   $(NULL)
 
 # For Android, this defaults to $(ANDROID_SDK)/android.jar
 ifndef JAVA_BOOTCLASSPATH
-  JAVA_BOOTCLASSPATH = $(ANDROID_SDK)/android.jar:$(ANDROID_COMPAT_LIB)
+  JAVA_BOOTCLASSPATH = $(ANDROID_SDK)/android.jar
 endif
 
 # For Android, we default to 1.5
 ifndef JAVA_VERSION
   JAVA_VERSION = 1.5
 endif
 
 JAVAC_FLAGS = \
   -target $(JAVA_VERSION) \
   -source $(JAVA_VERSION) \
-  -classpath $(JAVA_CLASSPATH) \
+  $(if $(JAVA_CLASSPATH),-classpath $(JAVA_CLASSPATH),) \
   -bootclasspath $(JAVA_BOOTCLASSPATH) \
   -encoding UTF8 \
   -g:source,lines \
   -Werror \
   $(NULL)
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -189,16 +189,20 @@ pref("extensions.hideInstallButton", tru
 pref("extensions.showMismatchUI", false);
 pref("extensions.hideUpdateButton", false);
 pref("extensions.strictCompatibility", false);
 pref("extensions.minCompatibleAppVersion", "11.0");
 
 pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 
+pref("extensions.hotfix.id", "firefox-android-hotfix@mozilla.org");
+pref("extensions.hotfix.cert.checkAttributes", true);
+pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35:22:9E:11:C9:A7:31:04:49:A1:AA");
+
 /* preferences for the Get Add-ons pane */
 pref("extensions.getAddons.cache.enabled", true);
 pref("extensions.getAddons.maxResults", 15);
 pref("extensions.getAddons.recommended.browseURL", "https://addons.mozilla.org/%LOCALE%/android/recommended/");
 pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/list/featured/all/%MAX_RESULTS%/%OS%/%VERSION%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/android/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
 pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%");
 pref("extensions.getAddons.browseAddons", "https://addons.mozilla.org/%LOCALE%/android/");
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -78,16 +78,21 @@
 #if !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG)
                  android:debuggable="true">
 #else
                  android:debuggable="false">
 #endif
 
         <meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
 
+#ifdef GOOGLE_PLAY_SERVICES
+        <!-- This resources comes from Google Play Services. Required for casting support. -->
+        <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
+#endif
+
         <!-- If the windowSoftInputMode adjust* flag changes below, the
              setSoftInputMode call in BrowserSearch#onStop must also be updated. -->
         <activity android:name=".App"
                   android:label="@string/moz_app_displayname"
                   android:taskAffinity="@ANDROID_PACKAGE_NAME@.BROWSER"
                   android:alwaysRetainTaskState="true"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -482,17 +482,17 @@ abstract public class BrowserApp extends
         mBrowserToolbar.setProgressBar(mProgressView);
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             // Show the target URL immediately in the toolbar.
             mBrowserToolbar.setTitle(intent.getDataString());
 
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT);
         }
 
-        ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener());
+        ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
         ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
             @Override
             public boolean onInterceptMotionEvent(View view, MotionEvent event) {
                 // If we get a gamepad panning MotionEvent while the focus is not on the layerview,
                 // put the focus on the layerview and carry on
                 if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
                     if (mHomePager == null) {
                         return false;
@@ -1985,21 +1985,27 @@ abstract public class BrowserApp extends
 
         mBrowserSearchContainer.setVisibility(View.INVISIBLE);
 
         getSupportFragmentManager().beginTransaction()
                 .remove(mBrowserSearch).commitAllowingStateLoss();
         mBrowserSearch.setUserVisibleHint(false);
     }
 
-    private class HideTabsTouchListener implements TouchEventInterceptor {
+    /**
+     * Hides certain UI elements (e.g. button toast, tabs tray) when the
+     * user touches the main layout.
+     */
+    private class HideOnTouchListener implements TouchEventInterceptor {
         private boolean mIsHidingTabs = false;
 
         @Override
         public boolean onInterceptTouchEvent(View view, MotionEvent event) {
+            getButtonToast().hide(false, ButtonToast.ReasonHidden.TOUCH_OUTSIDE);
+
             // We need to account for scroll state for the touched view otherwise
             // tapping on an "empty" part of the view will still be considered a
             // valid touch event.
             if (view.getScrollX() != 0 || view.getScrollY() != 0) {
                 Rect rect = new Rect();
                 view.getHitRect(rect);
                 rect.offset(-view.getScrollX(), -view.getScrollY());
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -54,17 +54,33 @@ GARBAGE += \
   javah.out \
   jni-stubs.inc \
   GeneratedJNIWrappers.cpp \
   GeneratedJNIWrappers.h \
   $(NULL)
 
 GARBAGE_DIRS += classes db jars res sync services generated
 
-JAVA_CLASSPATH = $(ANDROID_SDK)/android.jar
+JAVA_BOOTCLASSPATH = \
+    $(ANDROID_SDK)/android.jar \
+    $(ANDROID_COMPAT_LIB) \
+    $(NULL)
+
+JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH)))
+
+# If native devices are enabled, add Google Play Services and some of the v7 compat libraries
+ifdef MOZ_NATIVE_DEVICES
+    JAVA_CLASSPATH += \
+        $(GOOGLE_PLAY_SERVICES_LIB) \
+        $(ANDROID_MEDIAROUTER_LIB) \
+        $(ANDROID_APPCOMPAT_LIB) \
+        $(NULL)
+endif
+
+JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
 
 ALL_JARS = \
   gecko-R.jar \
   gecko-browser.jar \
   gecko-mozglue.jar \
   gecko-util.jar \
   squareup-picasso.jar \
   sync-thirdparty.jar \
@@ -78,19 +94,26 @@ endif
 include $(topsrcdir)/config/config.mk
 
 # Note that we're going to set up a dependency directly between embed_android.dex and the java files
 # Instead of on the .class files, since more than one .class file might be produced per .java file
 # Sync dependencies are provided in a single jar. Sync classes themselves are delivered as source,
 # because Android resource classes must be compiled together in order to avoid overlapping resource
 # indices.
 
+library_jars = \
+    $(JAVA_CLASSPATH) \
+    $(JAVA_BOOTCLASSPATH) \
+    $(NULL)
+
+library_jars := $(subst $(NULL) ,:,$(strip $(library_jars)))
+
 classes.dex: .proguard.deps
 	$(REPORT_BUILD)
-	$(DX) --dex --output=classes.dex jars-proguarded $(ANDROID_COMPAT_LIB)
+	$(DX) --dex --output=classes.dex jars-proguarded $(subst :, ,$(ANDROID_COMPAT_LIB):$(JAVA_CLASSPATH))
 
 ifdef MOZ_DISABLE_PROGUARD
   PROGUARD_PASSES=0
 else
   ifdef MOZ_DEBUG
     PROGUARD_PASSES=1
   else
     ifndef MOZILLA_OFFICIAL
@@ -108,17 +131,17 @@ endif
 .proguard.deps: $(ALL_JARS)
 	$(REPORT_BUILD)
 	@$(TOUCH) $@
 	java -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
 		@$(topsrcdir)/mobile/android/config/proguard.cfg \
 		-optimizationpasses $(PROGUARD_PASSES) \
 		-injars $(subst ::,:,$(subst $(NULL) ,:,$(strip $(ALL_JARS)))) \
 		-outjars jars-proguarded \
-		-libraryjars $(ANDROID_SDK)/android.jar:$(ANDROID_COMPAT_LIB)
+		-libraryjars $(library_jars)
 
 CLASSES_WITH_JNI= \
     org.mozilla.gecko.ANRReporter \
     org.mozilla.gecko.GeckoAppShell \
     org.mozilla.gecko.GeckoJavaSampler \
     org.mozilla.gecko.gfx.NativePanZoomController \
     org.mozilla.gecko.util.NativeJSContainer \
     org.mozilla.gecko.util.NativeJSObject \
@@ -136,41 +159,40 @@ endif
 jni-stubs.inc: gecko-browser.jar gecko-mozglue.jar gecko-util.jar sync-thirdparty.jar
 	$(JAVAH) -o javah.out -bootclasspath $(JAVA_BOOTCLASSPATH) -classpath $(subst $(NULL) $(NULL),:,$^) $(CLASSES_WITH_JNI)
 	$(PYTHON) $(topsrcdir)/mobile/android/base/jni-generator.py javah.out $@
 
 ANNOTATION_PROCESSOR_JAR_FILES := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar
 
 GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES)
 GeneratedJNIWrappers.cpp: $(ALL_JARS)
-	$(JAVA) -classpath gecko-mozglue.jar:$(JAVA_BOOTCLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) org.mozilla.gecko.annotationProcessors.AnnotationProcessor $(ALL_JARS)
+	$(JAVA) -classpath gecko-mozglue.jar:$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) org.mozilla.gecko.annotationProcessors.AnnotationProcessor $(ALL_JARS)
 
-gecko_package_dir = generated/org/mozilla/gecko
 # Like generated/org/mozilla/fennec_$USERID.
 android_package_dir = $(addprefix generated/,$(subst .,/,$(ANDROID_PACKAGE_NAME)))
 
 # These _PP_JAVAFILES are specified in moz.build and defined in
 # backend.mk, which is included by config.mk.  Therefore this needs to
 # be defined after config.mk is included.
-PP_JAVAFILES := $(filter-out $(gecko_package_dir)/R.java,$(gecko-mozglue_PP_JAVAFILES) $(gecko-browser_PP_JAVAFILES))
+PP_JAVAFILES := $(filter-out generated/org/mozilla/gecko/R.java,$(gecko-mozglue_PP_JAVAFILES) $(gecko-browser_PP_JAVAFILES))
 
 manifest := \
   AndroidManifest.xml.in \
   WebappManifestFragment.xml.frag.in \
   $(NULL)
 
 PP_TARGETS += manifest
 
 # Certain source files need to be preprocessed.  This special rule
 # generates these files into generated/org/mozilla/gecko for
 # consumption by the build system and IDEs.
 
-preprocessed := $(addsuffix .in,$(subst $(gecko_package_dir)/,,$(filter $(gecko_package_dir)/%,$(PP_JAVAFILES))))
+preprocessed := $(addsuffix .in,$(subst generated/org/mozilla/gecko/,,$(filter generated/org/mozilla/gecko/%,$(PP_JAVAFILES))))
 
-preprocessed_PATH := $(gecko_package_dir)
+preprocessed_PATH := generated/org/mozilla/gecko
 preprocessed_KEEP_PATH := 1
 
 PP_TARGETS += preprocessed
 
 # Certain source files have Java package name @ANDROID_PACKAGE_NAME@.
 # We hate these files but they are necessary for backwards
 # compatibility.  These special rules generate these files into
 # generated/org/mozilla/{firefox,firefox_beta,fennec,fennec_$USER} for
@@ -217,16 +239,17 @@ android_res_files := $(filter-out $(not_
 # if any recipe command fails. It is crucial that the sub-Make touch
 # the target files (those depending on .locales.deps) only when there
 # contents have changed; otherwise, this will force rebuild them as
 # part of every build.
 .locales.deps: FORCE
 	$(TOUCH) $@
 	$(MAKE) -C locales
 
+
 # This .deps pattern saves an invocation of the sub-Make: the single
 # invocation generates both strings.xml and suggestedsites.json. The
 # trailing semi-colon defines an empty recipe: defining no recipe at
 # all causes Make to treat the target differently, in a way that
 # defeats our dependencies.
 res/values/strings.xml: .locales.deps ;
 res/raw/suggestedsites.json: .locales.deps ;
 
@@ -263,17 +286,34 @@ geckoview_resources.zip: $(all_resources
 	$(foreach dir,$(ANDROID_RES_DIRS),$(call zip_directory_with_relative_paths,$(CURDIR)/$@,$(dir)))
 
 # All of generated/org/mozilla/gecko/R.java, gecko.ap_, and R.txt are
 # produced by aapt; this saves aapt invocations.  The trailing
 # semi-colon defines an empty recipe; defining no recipe at all causes
 # Make to treat the target differently, in a way that defeats our
 # dependencies.
 
-$(gecko_package_dir)/R.java: .aapt.deps ;
+generated/org/mozilla/gecko/R.java: .aapt.deps ;
+
+# If native devices are enabled, add Google Play Services, build their resources
+generated/android/support/v7/appcompat/R.java: .aapt.deps ;
+generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
+generated/com/google/android/gms/R.java: .aapt.deps ;
+
+ifdef MOZ_NATIVE_DEVICES
+    extra_packages += android.support.v7.appcompat
+    extra_res_dirs += $(ANDROID_APPCOMPAT_RES)
+
+    extra_packages += android.support.v7.mediarouter
+    extra_res_dirs += $(ANDROID_MEDIAROUTER_RES)
+
+    extra_packages += com.google.android.gms
+    extra_res_dirs += $(GOOGLE_PLAY_SERVICES_RES)
+endif
+
 gecko.ap_: .aapt.deps ;
 R.txt: .aapt.deps ;
 
 # [Comment 2/3] This tom-foolery provides a target that forces a
 # rebuild of gecko.ap_.  This is used during packaging to ensure that
 # resources are fresh.  The alternative would be complicated; see
 # [Comment 1/3].
 
@@ -284,52 +324,59 @@ gecko-nodeps/R.txt: .aapt.nodeps ;
 # This ignores the default set of resources ignored by aapt, plus
 # files starting with '#'.  (Emacs produces temp files named #temp#.)
 # This doesn't actually set the environment variable; it's used as a
 # parameter in the aapt invocation below.  Consider updating
 # not_android_res_files as well.
 
 ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*:*.rej:*.orig
 
+extra_packages := $(subst $(NULL) ,:,$(strip $(extra_packages)))
+
 # 1: target file.
 # 2: dependencies.
 # 3: name of ap_ file to write.
 # 4: directory to write R.java into.
 # 5: directory to write R.txt into.
 # We touch the target file before invoking aapt so that aapt's outputs
 # are fresher than the target, preventing a subsequent invocation from
 # thinking aapt's outputs are stale.  This is safe because Make
 # removes the target file if any recipe command fails.
 define aapt_command
 $(1): $$(call mkdir_deps,$(filter-out ./,$(dir $(3) $(4) $(5)))) $(2)
 	@$$(TOUCH) $$@
-	$$(AAPT) package -f -M AndroidManifest.xml -I $$(ANDROID_SDK)/android.jar \
+	$$(AAPT) package -f -m \
+		-M AndroidManifest.xml \
+		-I $(ANDROID_SDK)/android.jar \
 		--auto-add-overlay \
 		$$(addprefix -S ,$$(ANDROID_RES_DIRS)) \
-		--custom-package org.mozilla.gecko --non-constant-id \
+		$(if $(extra_res_dirs),$$(addprefix -S ,$$(extra_res_dirs)),) \
+		$(if $(extra_packages),--extra-packages $$(extra_packages),) \
+		--custom-package org.mozilla.gecko \
+		--non-constant-id \
 		-F $(3) \
 		-J $(4) \
 		--output-text-symbols $(5) \
 		--ignore-assets "$$(ANDROID_AAPT_IGNORE)"
 endef
 
 # [Comment 3/3] The first of these rules is used during regular
 # builds.  The second writes an ap_ file that is only used during
 # packaging.  It doesn't write the normal ap_, or R.java, since we
 # don't want the packaging step to write anything that would make a
 # further no-op build do work.  See also
 # toolkit/mozapps/installer/packager.mk.
 
 # .aapt.deps: $(all_resources)
-$(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,$(gecko_package_dir)/,./))
+$(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,generated/,./))
 
 # .aapt.nodeps: $(CURDIR)/AndroidManifest.xml FORCE
 $(eval $(call aapt_command,.aapt.nodeps,$(CURDIR)/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
 
-fennec_ids.txt: $(gecko_package_dir)/R.java fennec-ids-generator.py
+fennec_ids.txt: generated/org/mozilla/gecko/R.java fennec-ids-generator.py
 	$(PYTHON) $(topsrcdir)/mobile/android/base/fennec-ids-generator.py -i $< -o $@
 
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
 libs:: geckoview_resources.zip classes.dex jni-stubs.inc GeneratedJNIWrappers.cpp fennec_ids.txt
 	$(INSTALL) geckoview_resources.zip $(FINAL_TARGET)
 	$(INSTALL) classes.dex $(FINAL_TARGET)
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -148,16 +148,20 @@ public class BrowserDB {
     public static void initialize(String profile) {
         sDb = new LocalBrowserDB(profile);
     }
 
     public static void setSuggestedSites(SuggestedSites suggestedSites) {
         sSuggestedSites = suggestedSites;
     }
 
+    public static boolean hideSuggestedSite(String url) {
+        return sSuggestedSites.hideSite(url);
+    }
+
     public static void invalidateCachedState() {
         sDb.invalidateCachedState();
     }
 
     @RobocopTarget
     public static Cursor filter(ContentResolver cr, CharSequence constraint, int limit) {
         return sDb.filter(cr, constraint, limit);
     }
--- a/mobile/android/base/db/SuggestedSites.java
+++ b/mobile/android/base/db/SuggestedSites.java
@@ -5,36 +5,40 @@
 
 package org.mozilla.gecko.db;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 import org.json.JSONArray;
-import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.RawResource;
+import org.mozilla.gecko.util.ThreadUtils;
 
 /**
  * {@code SuggestedSites} provides API to get a list of locale-specific
  * suggested sites to be used in Fennec's top sites panel. It provides
  * only a single method to fetch the list as a {@code Cursor}. This cursor
  * will then be wrapped by {@code TopSitesCursorWrapper} to blend top,
  * pinned, and suggested sites in the UI. The returned {@code Cursor}
  * uses its own schema defined in {@code BrowserContract.SuggestedSites}
@@ -42,21 +46,26 @@ import org.mozilla.gecko.util.RawResourc
  *
  * Under the hood, {@code SuggestedSites} keeps reference to the
  * parsed list of sites to avoid reparsing the JSON file on every
  * {@code get()} call.
  *
  * The default list of suggested sites is stored in a raw Android
  * resource ({@code R.raw.suggestedsites}) which is dynamically
  * generated at build time for each target locale.
+ *
+ * Changes to the list of suggested sites are saved in SharedPreferences.
  */
 @RobocopTarget
 public class SuggestedSites {
     private static final String LOGTAG = "GeckoSuggestedSites";
 
+    // SharedPreference key for suggested sites that should be hidden.
+    public static final String PREF_SUGGESTED_SITES_HIDDEN = "suggestedSites.hidden";
+
     private static final String[] COLUMNS = new String[] {
         BrowserContract.SuggestedSites._ID,
         BrowserContract.SuggestedSites.URL,
         BrowserContract.SuggestedSites.TITLE
     };
 
     private static final String JSON_KEY_URL = "url";
     private static final String JSON_KEY_TITLE = "title";
@@ -83,16 +92,17 @@ public class SuggestedSites {
                      "imageUrl = " + imageUrl + "\n" +
                      "bgColor = " + bgColor + " }";
         }
     }
 
     private final Context context;
     private Map<String, Site> cachedSites;
     private Locale cachedLocale;
+    private Set<String> cachedBlacklist;
 
     public SuggestedSites(Context appContext) {
         context = appContext;
     }
 
     private String loadFromFile() {
         // Do nothing for now
         return null;
@@ -150,17 +160,17 @@ public class SuggestedSites {
         cachedLocale = Locale.getDefault();
     }
 
     private boolean isEnabled() {
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
         return prefs.getBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, true);
     }
 
-    private Site getSiteForUrl(String url) {
+    private synchronized Site getSiteForUrl(String url) {
         if (cachedSites == null) {
             return null;
         }
 
         return cachedSites.get(url);
     }
 
     /**
@@ -194,17 +204,17 @@ public class SuggestedSites {
 
     /**
      * Returns a {@code Cursor} with the list of suggested websites.
      *
      * @param limit maximum number of suggested sites.
      * @param locale the target locale.
      * @param excludeUrls list of URLs to be excluded from the list.
      */
-    public Cursor get(int limit, Locale locale, List<String> excludeUrls) {
+    public synchronized Cursor get(int limit, Locale locale, List<String> excludeUrls) {
         final MatrixCursor cursor = new MatrixCursor(COLUMNS);
 
         // Return an empty cursor if suggested sites have been
         // disabled by the user.
         if (!isEnabled()) {
             return cursor;
         }
 
@@ -214,16 +224,18 @@ public class SuggestedSites {
         }
 
         // Return empty cursor if there was an error when
         // loading the suggested sites or the list is empty.
         if (cachedSites == null || cachedSites.isEmpty()) {
             return cursor;
         }
 
+        excludeUrls = includeBlacklist(excludeUrls);
+
         final int sitesCount = cachedSites.size();
         Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
 
         final int maxCount = Math.min(limit, sitesCount);
         for (Site site : cachedSites.values()) {
             if (cursor.getCount() == maxCount) {
                 break;
             }
@@ -252,9 +264,88 @@ public class SuggestedSites {
         final Site site = getSiteForUrl(url);
         return (site != null ? site.imageUrl : null);
     }
 
     public String getBackgroundColorForUrl(String url) {
         final Site site = getSiteForUrl(url);
         return (site != null ? site.bgColor : null);
     }
-}
\ No newline at end of file
+
+    private Set<String> loadBlacklist() {
+        Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences.");
+        final Set<String> blacklist = new HashSet<String>();
+
+        final SharedPreferences preferences = GeckoSharedPrefs.forProfile(context);
+        final String sitesString = preferences.getString(PREF_SUGGESTED_SITES_HIDDEN, null);
+
+        if (sitesString != null) {
+            for (String site : sitesString.trim().split(" ")) {
+                blacklist.add(Uri.decode(site));
+            }
+        }
+
+        return blacklist;
+    }
+
+    private List<String> includeBlacklist(List<String> originalList) {
+        if (cachedBlacklist == null) {
+            cachedBlacklist = loadBlacklist();
+        }
+
+        if (cachedBlacklist.isEmpty()) {
+            return originalList;
+        }
+
+        if (originalList == null) {
+            originalList = new ArrayList<String>();
+        }
+
+        originalList.addAll(cachedBlacklist);
+        return originalList;
+    }
+
+    /**
+     * Blacklist a suggested site so it will no longer be returned as a suggested site.
+     * This method should only be called from a background thread because it may write
+     * to SharedPreferences.
+     *
+     * Urls that are not Suggested Sites are ignored.
+     *
+     * @param url String url of site to blacklist
+     * @return true is blacklisted, false otherwise
+     */
+    public synchronized boolean hideSite(String url) {
+        ThreadUtils.assertNotOnUiThread();
+
+        if (cachedSites == null) {
+            refresh();
+            if (cachedSites == null) {
+                Log.w(LOGTAG, "Could not load suggested sites!");
+                return false;
+            }
+        }
+
+        if (cachedSites.containsKey(url)) {
+            if (cachedBlacklist == null) {
+                cachedBlacklist = loadBlacklist();
+            }
+
+            // Check if site has already been blacklisted, just in case.
+            if (!cachedBlacklist.contains(url)) {
+
+                saveToBlacklist(url);
+                cachedBlacklist.add(url);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void saveToBlacklist(String url) {
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
+        final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, "");
+        final String siteString = prefString.concat(" " + Uri.encode(url));
+        prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).commit();
+    }
+}
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -14,16 +14,17 @@ import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
 import org.mozilla.gecko.widget.ButtonToast;
 
@@ -328,16 +329,19 @@ abstract class HomeFragment extends Frag
         }
 
         @Override
         public Void doInBackground(Void... params) {
             ContentResolver cr = mContext.getContentResolver();
 
             if (mPosition > -1) {
                 BrowserDB.unpinSite(cr, mPosition);
+                if (BrowserDB.hideSuggestedSite(mUrl)) {
+                    cr.notifyChange(SuggestedSites.CONTENT_URI, null);
+                }
             }
 
             BrowserDB.removeBookmarksWithURL(cr, mUrl);
             BrowserDB.removeHistoryEntry(cr, mUrl);
 
             BrowserDB.removeReadingListItemWithURL(cr, mUrl);
 
             final JSONObject json = new JSONObject();
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -11,16 +11,23 @@ include('android-services.mozbuild')
 
 thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
 
 resjar = add_java_jar('gecko-R')
 resjar.sources = []
 resjar.generated_sources += [
     'org/mozilla/gecko/R.java',
 ]
+
+if CONFIG['MOZ_NATIVE_DEVICES']:
+    resjar.generated_sources += ['com/google/android/gms/R.java']
+    DEFINES["GOOGLE_PLAY_SERVICES"] = 1
+    resjar.generated_sources += ['android/support/v7/appcompat/R.java']
+    resjar.generated_sources += ['android/support/v7/mediarouter/R.java']
+
 resjar.javac_flags += ['-Xlint:all']
 
 mgjar = add_java_jar('gecko-mozglue')
 mgjar.sources += [
     'mozglue/ByteBufferInputStream.java',
     'mozglue/DirectBufferAllocator.java',
     'mozglue/generatorannotations/GeneratorOptions.java',
     'mozglue/generatorannotations/OptionalGeneratedParameter.java',
@@ -465,16 +472,22 @@ gbjar.generated_sources += sync_generate
 gbjar.extra_jars = [
     'gecko-R.jar',
     'gecko-mozglue.jar',
     'gecko-util.jar',
     'squareup-picasso.jar',
     'sync-thirdparty.jar',
     'websockets.jar',
 ]
+
+if CONFIG['MOZ_NATIVE_DEVICES']:
+    gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_LIB']]
+    gbjar.extra_jars += [CONFIG['ANDROID_MEDIAROUTER_LIB']]
+    gbjar.extra_jars += [CONFIG['GOOGLE_PLAY_SERVICES_LIB']]
+
 gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough']
 
 spjar = add_java_jar('squareup-picasso')
 spjar.sources += [ thirdparty_source_dir + f for f in [
     'com/squareup/picasso/Action.java',
     'com/squareup/picasso/AssetBitmapHunter.java',
     'com/squareup/picasso/BitmapHunter.java',
     'com/squareup/picasso/Cache.java',
--- a/mobile/android/base/resources/drawable/toast_background.xml
+++ b/mobile/android/base/resources/drawable/toast_background.xml
@@ -1,10 +1,10 @@
 <?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/. -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
-    <solid android:color="@color/toast_button_background" />
+    <solid android:color="@color/toast_background" />
     <corners android:radius="@dimen/toast_button_corner_radius" />
 </shape>
deleted file mode 100644
--- a/mobile/android/base/resources/values-v19/colors.xml
+++ /dev/null
@@ -1,10 +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>
-  <!-- Button toast colors. -->
-  <color name="toast_button_background">#FF656565</color>
-  <color name="toast_button_pressed">#FFB1B1B1</color>
-</resources>
--- a/mobile/android/base/resources/values-v19/styles.xml
+++ b/mobile/android/base/resources/values-v19/styles.xml
@@ -9,18 +9,18 @@
         <item name="android:layout_marginLeft">8dp</item>
         <item name="android:layout_marginRight">8dp</item>
     </style>
 
     <style name="ToastElementBase">
         <item name="android:background">@null</item>
         <item name="android:paddingLeft">24dp</item>
         <item name="android:paddingRight">24dp</item>
-        <item name="android:paddingTop">18dp</item>
-        <item name="android:paddingBottom">18dp</item>
+        <item name="android:paddingTop">11dp</item>
+        <item name="android:paddingBottom">11dp</item>
 	  </style>
 
     <style name="ToastDivider" parent="ToastDividerBase">
 	<item name="android:layout_marginTop">12dp</item>
         <item name="android:layout_marginBottom">12dp</item>
     </style>
 
     <style name="ToastMessage" parent="ToastMessageBase">
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -103,13 +103,14 @@
   <color name="swipe_refresh_orange3">#F57900</color>
   <color name="swipe_refresh_orange4">#FFB44C</color>
 
   <!-- Remote tabs setup -->
   <color name="remote_tabs_setup_button_background">#E66000</color>
   <color name="remote_tabs_setup_button_background_hit">#D95300</color>
 
   <!-- Button toast colors. -->
-  <color name="toast_button_background">#FF2A2A2A</color>
-  <color name="toast_button_pressed">#FF3E6784</color>
+  <color name="toast_background">#DD363B40</color>
+  <color name="toast_button_background">#00000000</color>
+  <color name="toast_button_pressed">#DD2C3136</color>
   <color name="toast_button_divider">#FFD1D5DA</color>
   <color name="toast_button_text">#FFFFFFFF</color>
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -606,18 +606,18 @@
         <item name="android:layout_marginLeft">16dp</item>
         <item name="android:layout_marginRight">16dp</item>
     </style>
 
     <style name="ToastElementBase">
         <item name="android:background">@null</item>
         <item name="android:paddingLeft">12dp</item>
         <item name="android:paddingRight">12dp</item>
-        <item name="android:paddingTop">20dp</item>
-        <item name="android:paddingBottom">20dp</item>
+        <item name="android:paddingTop">11dp</item>
+        <item name="android:paddingBottom">11dp</item>
     </style>
 
     <style name="ToastDividerBase">
         <item name="android:background">@color/toast_button_divider</item>
         <item name="android:layout_width">1dp</item>
         <item name="android:layout_height">match_parent</item>
     </style>
 
--- a/mobile/android/base/widget/ButtonToast.java
+++ b/mobile/android/base/widget/ButtonToast.java
@@ -37,16 +37,17 @@ public class ButtonToast {
     private final View mView;
     private final TextView mMessageView;
     private final Button mButton;
     private final Handler mHideHandler = new Handler();
     private Toast mCurrentToast;
 
     public enum ReasonHidden {
         CLICKED,
+        TOUCH_OUTSIDE,
         TIMEOUT,
         REPLACED,
         STARTUP
     }
 
     // State objects
     private static class Toast {
         public final CharSequence buttonMessage;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -116,34 +116,40 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
 ].forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
-  notifications.forEach(function (aNotification) {
-    Services.obs.addObserver(function(s, t, d) {
-        window[name].observe(s, t, d)
-    }, aNotification, false);
+  let observer = (s, t, d) => {
+    Services.obs.removeObserver(observer, t);
+    Services.obs.addObserver(window[name], t, false);
+    window[name].observe(s, t, d); // Explicitly notify new observer
+  };
+  notifications.forEach((notification) => {
+    Services.obs.addObserver(observer, notification, false);
   });
 });
 
 // Lazily-loaded JS modules that use observer notifications
 [
   ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
             "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
 ].forEach(module => {
   let [name, notifications, resource] = module;
   XPCOMUtils.defineLazyModuleGetter(this, name, resource);
+  let observer = (s, t, d) => {
+    Services.obs.removeObserver(observer, t);
+    Services.obs.addObserver(this[name], t, false);
+    this[name].observe(s, t, d); // Explicitly notify new observer
+  };
   notifications.forEach(notification => {
-    Services.obs.addObserver((s,t,d) => {
-      this[name].observe(s,t,d)
-    }, notification, false);
+    Services.obs.addObserver(observer, notification, false);
   });
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
   "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
 
 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
   "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
--- a/mobile/android/config/proguard.cfg
+++ b/mobile/android/config/proguard.cfg
@@ -189,13 +189,15 @@
 }
 
 -keep @interface org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI
 -keep @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class *
 -keepclassmembers @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class * {
     *;
 }
 
+-keep class **.R$*
+
 # Disable obfuscation because it makes exception stack traces more difficult to read.
 -dontobfuscate
 
 # Suppress warnings about missing descriptor classes.
 #-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -64,8 +64,11 @@ MOZ_SERVICES_FXACCOUNTS=1
 # Enable Wifi-AP/cell tower data reporting
 MOZ_DATA_REPORTING=1
 
 # Enable runtime locale switching.
 MOZ_LOCALE_SWITCHER=1
 
 # Enable second screen and casting support for external devices.
 MOZ_DEVICES=1
+
+# Enable second screen using native Android libraries
+MOZ_NATIVE_DEVICES=
--- a/mobile/android/tests/browser/junit3/src/tests/TestSuggestedSites.java
+++ b/mobile/android/tests/browser/junit3/src/tests/TestSuggestedSites.java
@@ -2,38 +2,37 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browser.tests;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.SharedPreferences;
 import android.database.Cursor;
+import android.net.Uri;
 import android.test.mock.MockResources;
 import android.test.RenamingDelegatingContext;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
 import org.json.JSONArray;
-import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.RawResource;
 
 public class TestSuggestedSites extends BrowserTestCase {
     private static class TestContext extends RenamingDelegatingContext {
         private static final String PREFIX = "TestSuggestedSites-";
 
         private final Resources resources;
         private final Set<String> usedPrefs;
 
@@ -187,16 +186,54 @@ public class TestSuggestedSites extends 
             String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
             assertFalse(excludedUrls.contains(url));
             assertTrue(includedUrls.contains(url));
         }
 
         c.close();
     }
 
+    public void testHiddenSites() {
+        resources.setSuggestedSitesResource(generateSites(6));
+
+        List<String> visibleUrls = new ArrayList<String>(3);
+        visibleUrls.add("url3");
+        visibleUrls.add("url4");
+        visibleUrls.add("url5");
+
+        List<String> hiddenUrls = new ArrayList<String>(3);
+        hiddenUrls.add("url0");
+        hiddenUrls.add("url1");
+        hiddenUrls.add("url2");
+
+        // Add mocked hidden sites to SharedPreferences.
+        StringBuilder hiddenUrlBuilder = new StringBuilder();
+        for (String s : hiddenUrls) {
+            hiddenUrlBuilder.append(" ");
+            hiddenUrlBuilder.append(Uri.encode(s));
+        }
+
+        final String hiddenPref = hiddenUrlBuilder.toString();
+        GeckoSharedPrefs.forProfile(context).edit()
+                                        .putString(SuggestedSites.PREF_SUGGESTED_SITES_HIDDEN, hiddenPref)
+                                        .commit();
+
+        Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+        assertEquals(Math.min(3, DEFAULT_LIMIT), c.getCount());
+
+        c.moveToPosition(-1);
+        while (c.moveToNext()) {
+            String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+            assertFalse(hiddenUrls.contains(url));
+            assertTrue(visibleUrls.contains(url));
+        }
+
+        c.close();
+    }
+
     public void testDisabledState() {
         resources.setSuggestedSitesResource(generateSites(3));
 
         Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
         assertEquals(3, c.getCount());
         c.close();
 
         // Disable suggested sites
--- a/testing/mozbase/docs/index.rst
+++ b/testing/mozbase/docs/index.rst
@@ -36,17 +36,17 @@ project_ wiki page with notes on develop
 .. _project: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase
 
 The documentation is organized by category, then by module. Figure out what you
 want to do then dive in!
 
 .. toctree::
    :maxdepth: 2
 
-   manifestdestiny
+   manifestparser
    gettinginfo
    setuprunning
    mozhttpd
    loggingreporting
    devicemanagement
 
 Indices and tables
 ==================
--- a/testing/mozbase/mozversion/mozversion/mozversion.py
+++ b/testing/mozbase/mozversion/mozversion/mozversion.py
@@ -28,16 +28,17 @@ class LocalAppNotFoundError(VersionError
         VersionError.__init__(
             self, 'No binary path or application.ini found in working '
             'directory. Specify a binary path or run from the directory '
             'containing the binary.')
 
 
 INI_DATA_MAPPING = (('application', 'App'), ('platform', 'Build'))
 
+
 class Version(mozlog.LoggingMixin):
 
     def __init__(self):
         self._info = {}
 
     def get_gecko_info(self, path):
         for type, section in INI_DATA_MAPPING:
             config_file = os.path.join(path, "%s.ini" % type)
@@ -57,32 +58,34 @@ class Version(mozlog.LoggingMixin):
             name = name_map.get(key, key).lower()
             self._info['%s_%s' % (type, name)] = config.has_option(
                 section, key) and config.get(section, key) or None
 
         if not self._info.get('application_display_name'):
             self._info['application_display_name'] = \
                 self._info.get('application_name')
 
+
 class LocalFennecVersion(Version):
 
     def __init__(self, path, **kwargs):
         Version.__init__(self, **kwargs)
         self.get_gecko_info(path)
 
     def get_gecko_info(self, path):
         archive = zipfile.ZipFile(path, 'r')
         for type, section in INI_DATA_MAPPING:
             filename = "%s.ini" % type
             if filename in archive.namelist():
                 self._parse_ini_file(archive.open(filename), type,
                                      section)
             else:
                 self.warn('Unable to find %s' % filename)
 
+
 class LocalVersion(Version):
 
     def __init__(self, binary, **kwargs):
         Version.__init__(self, **kwargs)
         path = None
 
         if binary:
             if not os.path.exists(binary):
@@ -163,21 +166,22 @@ class LocalB2GVersion(B2GVersion):
             with open(zip_path, 'rb') as zip_file:
                 self.get_gaia_info(zip_file)
         else:
             self.warn('Error pulling gaia file')
 
 
 class RemoteB2GVersion(B2GVersion):
 
-    def __init__(self, sources=None, dm_type='adb', host=None, **kwargs):
+    def __init__(self, sources=None, dm_type='adb', host=None,
+                 device_serial=None, **kwargs):
         B2GVersion.__init__(self, sources, **kwargs)
 
         if dm_type == 'adb':
-            dm = mozdevice.DeviceManagerADB()
+            dm = mozdevice.DeviceManagerADB(deviceSerial=device_serial)
         elif dm_type == 'sut':
             if not host:
                 raise Exception('A host for SUT must be supplied.')
             dm = mozdevice.DeviceManagerSUT(host=host)
         else:
             raise Exception('Unknown device manager type: %s' % dm_type)
 
         if not sources:
@@ -213,56 +217,63 @@ class RemoteB2GVersion(B2GVersion):
             'ro.product.device': 'device_id'}
         for line in build_props.split('\n'):
             if not line.strip().startswith('#') and '=' in line:
                 key, value = [s.strip() for s in line.split('=', 1)]
                 if key in desired_props.keys():
                     self._info[desired_props[key]] = value
 
 
-def get_version(binary=None, sources=None, dm_type=None, host=None):
+def get_version(binary=None, sources=None, dm_type=None, host=None,
+                device_serial=None):
     """
     Returns the application version information as a dict. You can specify
     a path to the binary of the application or an Android APK file (to get
     version information for Firefox for Android). If this is omitted then the
     current directory is checked for the existance of an application.ini
     file. If not found, then it is assumed the target application is a remote
     Firefox OS instance.
 
     :param binary: Path to the binary for the application or Android APK file
     :param sources: Path to the sources.xml file (Firefox OS)
     :param dm_type: Device manager type. Must be 'adb' or 'sut' (Firefox OS)
     :param host: Host address of remote Firefox OS instance (SUT)
+    :param device_serial: Serial identifier of Firefox OS device (ADB)
     """
     try:
         if binary and zipfile.is_zipfile(binary) and 'AndroidManifest.xml' in \
            zipfile.ZipFile(binary, 'r').namelist():
             version = LocalFennecVersion(binary)
         else:
             version = LocalVersion(binary)
             if version._info.get('application_name') == 'B2G':
                 version = LocalB2GVersion(binary, sources=sources)
     except LocalAppNotFoundError:
-        version = RemoteB2GVersion(sources=sources, dm_type=dm_type, host=host)
+        version = RemoteB2GVersion(sources=sources, dm_type=dm_type, host=host,
+                                   device_serial=device_serial)
     return version._info
 
 
 def cli(args=sys.argv[1:]):
     parser = OptionParser()
     parser.add_option('--binary',
                       dest='binary',
                       help='path to application binary or apk')
     parser.add_option('--sources',
                       dest='sources',
                       help='path to sources.xml (Firefox OS only)')
+    parser.add_option('--device',
+                      help='serial identifier of device to target (Firefox OS '
+                           'only)')
     (options, args) = parser.parse_args(args)
 
     dm_type = os.environ.get('DM_TRANS', 'adb')
     host = os.environ.get('TEST_DEVICE')
 
     version = get_version(binary=options.binary, sources=options.sources,
-                          dm_type=dm_type, host=host)
+                          dm_type=dm_type, host=host,
+                          device_serial=options.device)
     for (key, value) in sorted(version.items()):
         if value:
             print '%s: %s' % (key, value)
 
 if __name__ == '__main__':
     cli()
--- a/toolkit/content/widgets/scrollbox.xml
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -553,16 +553,18 @@
             return;
         }
 
         this.setAttribute("notoverflowing", "true");
 
         try {
           // See bug 341047 and comments in overflow handler as to why 
           // try..catch is needed here
+          this._updateScrollButtonsDisabledState();
+
           let childNodes = this._getScrollableElements();
           if (childNodes && childNodes.length)
             this.ensureElementIsVisible(childNodes[0], false);
         }
         catch(e) {
           this.removeAttribute("notoverflowing");
         }
       ]]></handler>
--- a/toolkit/devtools/gcli/commands/inject.js
+++ b/toolkit/devtools/gcli/commands/inject.js
@@ -7,17 +7,17 @@
 const { Services } = require("resource://gre/modules/Services.jsm");
 const { listenOnce } = require("devtools/async-utils");
 const gcli = require("gcli/index");
 
 exports.items = [
   {
     name: "inject",
     description: gcli.lookup("injectDesc"),
-    manual: gcli.lookup("injectManual"),
+    manual: gcli.lookup("injectManual2"),
     params: [{
       name: 'library',
       type: {
         name: "union",
         types: [
           {
             name: "selection",
             lookup: [
--- a/toolkit/devtools/output-parser.js
+++ b/toolkit/devtools/output-parser.js
@@ -323,28 +323,39 @@ OutputParser.prototype = {
    * @returns {Boolean}
    *          true if the color passed in was valid, false otherwise. Special
    *          values such as transparent also return false.
    */
   _appendColor: function(color, options={}) {
     let colorObj = new colorUtils.CssColor(color);
 
     if (this._isValidColor(colorObj)) {
+      let container = this._createNode("span", {
+         "data-color": color
+      });
+
       if (options.colorSwatchClass) {
-        this._appendNode("span", {
+        let swatch = this._createNode("span", {
           class: options.colorSwatchClass,
           style: "background-color:" + color
         });
+        container.appendChild(swatch);
       }
+
       if (options.defaultColorType) {
         color = colorObj.toString();
+        container.dataset["color"] = color;
       }
-      this._appendNode("span", {
+
+      let value = this._createNode("span", {
         class: options.colorClass
       }, color);
+
+      container.appendChild(value);
+      this.parsed.push(container);
       return true;
     }
     return false;
   },
 
    /**
     * Append a URL to the output.
     *
@@ -375,43 +386,60 @@ OutputParser.prototype = {
 
       this._appendTextNode("')");
     } else {
       this._appendTextNode("url('" + url + "')");
     }
   },
 
   /**
-   * Append a node to the output.
+   * Create a node.
    *
    * @param  {String} tagName
    *         Tag type e.g. "div"
    * @param  {Object} attributes
    *         e.g. {class: "someClass", style: "cursor:pointer"};
    * @param  {String} [value]
    *         If a value is included it will be appended as a text node inside
    *         the tag. This is useful e.g. for span tags.
+   * @return {Node} Newly created Node.
    */
-  _appendNode: function(tagName, attributes, value="") {
+  _createNode: function(tagName, attributes, value="") {
     let win = Services.appShell.hiddenDOMWindow;
     let doc = win.document;
     let node = doc.createElementNS(HTML_NS, tagName);
     let attrs = Object.getOwnPropertyNames(attributes);
 
     for (let attr of attrs) {
       if (attributes[attr]) {
         node.setAttribute(attr, attributes[attr]);
       }
     }
 
     if (value) {
       let textNode = doc.createTextNode(value);
       node.appendChild(textNode);
     }
 
+    return node;
+  },
+
+  /**
+   * Append a node to the output.
+   *
+   * @param  {String} tagName
+   *         Tag type e.g. "div"
+   * @param  {Object} attributes
+   *         e.g. {class: "someClass", style: "cursor:pointer"};
+   * @param  {String} [value]
+   *         If a value is included it will be appended as a text node inside
+   *         the tag. This is useful e.g. for span tags.
+   */
+  _appendNode: function(tagName, attributes, value="") {
+    let node = this._createNode(tagName, attributes, value);
     this.parsed.push(node);
   },
 
   /**
    * Append a text node to the output. If the previously output item was a text
    * node then we append the text to that node.
    *
    * @param  {String} text
--- a/toolkit/devtools/server/actors/framerate.js
+++ b/toolkit/devtools/server/actors/framerate.js
@@ -23,71 +23,87 @@ exports.unregister = function(handle) {
 /**
  * A very simple utility for monitoring framerate.
  */
 let FramerateActor = exports.FramerateActor = protocol.ActorClass({
   typeName: "framerate",
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
-    this._contentWin = tabActor.window;
+    this._chromeWin = getChromeWin(tabActor.window);
     this._onRefreshDriverTick = this._onRefreshDriverTick.bind(this);
   },
   destroy: function(conn) {
     protocol.Actor.prototype.destroy.call(this, conn);
-    this.finalize();
+    this.stopRecording();
   },
 
   /**
    * Starts monitoring framerate, storing the frames per second.
    */
   startRecording: method(function() {
     if (this._recording) {
       return;
     }
     this._recording = true;
     this._ticks = [];
 
-    this._startTime = this._contentWin.performance.now();
-    this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
+    this._startTime = this._chromeWin.performance.now();
+    this._chromeWin.requestAnimationFrame(this._onRefreshDriverTick);
   }, {
   }),
 
   /**
    * Stops monitoring framerate, returning the recorded values.
    */
   stopRecording: method(function(beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
     if (!this._recording) {
       return [];
     }
     this._recording = false;
 
     // We don't need to store the ticks array for future use, release it.
-    let ticks = this._ticks.filter(e => e >= beginAt && e <= endAt);
+    let ticks = this.getPendingTicks(beginAt, endAt);
     this._ticks = null;
     return ticks;
   }, {
     request: {
       beginAt: Arg(0, "nullable:number"),
       endAt: Arg(1, "nullable:number")
     },
     response: { ticks: RetVal("array:number") }
   }),
 
   /**
+   * Gets the refresh driver ticks recorded so far.
+   */
+  getPendingTicks: method(function(beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
+    if (!this._ticks) {
+      return [];
+    }
+    return this._ticks.filter(e => e >= beginAt && e <= endAt);
+  }, {
+    request: {
+      beginAt: Arg(0, "nullable:number"),
+      endAt: Arg(1, "nullable:number")
+    },
+    response: { ticks: RetVal("array:number") }
+  }),
+
+  /**
    * Function invoked along with the refresh driver.
    */
   _onRefreshDriverTick: function() {
     if (!this._recording) {
       return;
     }
-    this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
+    this._chromeWin.requestAnimationFrame(this._onRefreshDriverTick);
 
     // Store the amount of time passed since the recording started.
-    let currentTime = this._contentWin.performance.now();
+    let currentTime = this._chromeWin.performance.now();
     let elapsedTime = currentTime - this._startTime;
     this._ticks.push(elapsedTime);
   }
 });
 
 /**
  * The corresponding Front object for the FramerateActor.
  */
@@ -139,8 +155,24 @@ let FramerateFront = exports.FramerateFr
 
       frameCount = 0;
       prevTime = currTime;
     }
 
     return timeline;
   }
 });
+
+
+/**
+ * Gets the top level browser window from a content window.
+ *
+ * @param nsIDOMWindow innerWin
+ *        The content window to query.
+ * @return nsIDOMWindow
+ *         The top level browser window.
+ */
+function getChromeWin(innerWin) {
+  return innerWin
+    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+    .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -17,16 +17,17 @@ support-files =
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
 [test_css-logic.html]
 [test_device.html]
 [test_framerate_01.html]
 [test_framerate_02.html]
 [test_framerate_03.html]
+[test_framerate_04.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_framerate_04.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1023018 - Tests if the framerate actor keeps recording after navigations.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  var {FramerateFront} = devtools.require("devtools/server/actors/framerate");
+  var TargetFactory = devtools.TargetFactory;
+
+  var url = document.getElementById("testContent").href;
+  attachURL(url, onTab);
+
+  function onTab(_, client, form, contentDoc) {
+    var contentWin = contentDoc.defaultView;
+    var chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+    var selectedTab = chromeWin.gBrowser.selectedTab;
+
+    var target = TargetFactory.forTab(selectedTab);
+    var front = FramerateFront(client, form);
+
+    front.startRecording().then(() => {
+      window.setTimeout(() => {
+        front.getPendingTicks().then(firstBatch => {
+          target.once("will-navigate", () => {
+            window.setTimeout(() => {
+              front.stopRecording().then(secondBatch => {
+                onRecordingStopped(client, firstBatch, secondBatch);
+              });
+            }, 1000);
+          });
+          contentWin.location.reload();
+        });
+      }, 1000);
+    });
+  }
+
+  function onRecordingStopped(client, firstBatch, secondBatch) {
+    ok(firstBatch, "There should be a first batch recording available.");
+    ok(secondBatch, "There should be a second batch recording available.");
+
+    var diff = secondBatch.length - firstBatch.length;
+    info("Difference in ticks: " + diff);
+    ok(diff > 0, "More ticks should be recorded in the second batch.");
+
+    ok(firstBatch.every((e) => secondBatch.indexOf(e) != -1),
+      "All the ticks in the first batch should be in the second batch as well.");
+    ok(secondBatch.every((e, i, array) => i < array.length - 1 ? e < array[i + 1] : true),
+      "All the ticks in the final batch should be ascending in value.");
+
+    client.close(() => {
+      DebuggerServer.destroy();
+      SimpleTest.finish()
+    });
+  }
+}
+</script>
+</pre>
+<a id="testContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
+</body>
+</html>
--- a/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
@@ -75,16 +75,24 @@ ruleView.contextmenu.selectAll.accessKey
 # LOCALIZATION NOTE (ruleView.contextmenu.copy): Text displayed in the rule view
 # context menu.
 ruleView.contextmenu.copy=Copy
 
 # LOCALIZATION NOTE (ruleView.contextmenu.copy.accessKey): Access key for
 # the rule view context menu "Select all" entry.
 ruleView.contextmenu.copy.accessKey=C
 
+# LOCALIZATION NOTE (ruleView.contextmenu.copyColor): Text displayed in the rule
+# and computed view context menu when a color value was clicked.
+ruleView.contextmenu.copyColor=Copy Color
+
+# LOCALIZATION NOTE (ruleView.contextmenu.copyColor.accessKey): Access key for
+# the rule and computed view context menu "Copy Color" entry.
+ruleView.contextmenu.copyColor.accessKey=L
+
 # LOCALIZATION NOTE (ruleView.contextmenu.showOrigSources): Text displayed in the rule view
 # context menu.
 ruleView.contextmenu.showOrigSources=Show original sources
 
 # LOCALIZATION NOTE (ruleView.contextmenu.showOrigSources.accessKey): Access key for
 # the rule view context menu "Show original sources" entry.
 ruleView.contextmenu.showOrigSources.accessKey=O
 
--- a/widget/android/NativeJSContainer.cpp
+++ b/widget/android/NativeJSContainer.cpp
@@ -128,17 +128,17 @@ public:
     }
 
     static jobject CloneInstance(JNIEnv* env, jobject instance) {
         NativeJSContainer* const container = FromInstance(env, instance);
         if (!container || !container->EnsureObject(env)) {
             return nullptr;
         }
         JSContext* const cx = container->mThreadContext;
-        JS::RootedObject object(cx, container->mJSObject);
+        JS::RootedObject object(cx, *container->mJSObject);
         MOZ_ASSERT(object);
 
         JSAutoStructuredCloneBuffer buffer;
         if (!buffer.write(cx, JS::RootedValue(cx, JS::ObjectValue(*object)))) {
             AndroidBridge::ThrowException(env,
                 "java/lang/UnsupportedOperationException",
                 "Cannot serialize object");
             return nullptr;
@@ -174,35 +174,36 @@ public:
             FromInstance(env, GetInstanceFromObject(env, object));
         if (!container ||
             !container->EnsureObject(env)) { // Do thread check
             return nullptr;
         }
         const jint index = env->GetIntField(object, jObjectIndex);
         if (index < 0) {
             // -1 for index field means it's the root object of the container
-            return container->mJSObject;
+            return *container->mJSObject;
         }
-        return container->mRootedObjects[index];
+        return *container->mRootedObjects[index];
     }
 
     static jobject CreateObjectInstance(JNIEnv* env, jobject object,
                                         JSContext* cx,
                                         JS::HandleObject jsObject) {
         MOZ_ASSERT(object);
         MOZ_ASSERT(jsObject);
         AutoLocalJNIFrame frame(env, 2);
 
         jobject instance = GetInstanceFromObject(env, object);
         NativeJSContainer* const container = FromInstance(env, instance);
         if (!container) {
             return nullptr;
         }
         size_t newIndex = container->mRootedObjects.length();
-        if (!container->mRootedObjects.append(jsObject)) {
+        PersistentObjectPtr rootedJSObject(new PersistentObject(cx, jsObject));
+        if (!container->mRootedObjects.append(Move(rootedJSObject))) {
             AndroidBridge::ThrowException(env,
                 "java/lang/OutOfMemoryError", "Cannot allocate object");
             return nullptr;
         }
         const jobject newObject =
             env->NewObject(jNativeJSObject, jObjectConstructor,
                            instance, newIndex);
         MOZ_ASSERT(newObject);
@@ -226,17 +227,17 @@ public:
                 "Not available for this thread");
             return false;
         }
 
         JS::RootedValue value(mThreadContext);
         MOZ_ASSERT(mBuffer.data());
         MOZ_ALWAYS_TRUE(mBuffer.read(mThreadContext, &value));
         if (value.isObject()) {
-            mJSObject = &value.toObject();
+            mJSObject = new PersistentObject(mThreadContext, &value.toObject());
         }
         if (!mJSObject) {
             AndroidBridge::ThrowException(env,
                 "java/lang/IllegalStateException", "Cannot deserialize data");
             return false;
         }
         mBuffer.clear();
         return true;
@@ -273,39 +274,43 @@ private:
             env->NewObject(jNativeJSContainer, jContainerConstructor,
                            static_cast<jlong>(
                            reinterpret_cast<uintptr_t>(nativeObject)));
         AndroidBridge::HandleUncaughtException(env);
         MOZ_ASSERT(newObject);
         return newObject;
     }
 
+    typedef JS::PersistentRooted<JSObject*>   PersistentObject;
+    typedef ScopedDeletePtr<PersistentObject> PersistentObjectPtr;
+
     // Thread that the object is valid on
     PRThread* mThread;
     // Context that the object is valid in
     JSContext* mThreadContext;
     // Deserialized object, or nullptr if object is in serialized form
-    JS::Heap<JSObject*> mJSObject;
+    PersistentObjectPtr mJSObject;
     // Serialized object, or empty if object is in deserialized form
     JSAutoStructuredCloneBuffer mBuffer;
     // Objects derived from mJSObject
-    Vector<JS::Heap<JSObject*>, 4> mRootedObjects;
+    Vector<PersistentObjectPtr, 0> mRootedObjects;
 
     // Create a new container containing the given deserialized object
     NativeJSContainer(JSContext* cx, JS::HandleObject object)
             : mThread(PR_GetCurrentThread())
             , mThreadContext(cx)
-            , mJSObject(object)
+            , mJSObject(new PersistentObject(cx, object))
     {
     }
 
     // Create a new container containing the given serialized object
     NativeJSContainer(JSContext* cx, JSAutoStructuredCloneBuffer&& buffer)
             : mThread(PR_GetCurrentThread())
             , mThreadContext(cx)
+            , mJSObject(nullptr)
             , mBuffer(Forward<JSAutoStructuredCloneBuffer>(buffer))
     {
     }
 
     bool SwitchContextToCurrentThread() {
         PRThread* const currentThread = PR_GetCurrentThread();
         if (currentThread == mThread) {
             return true;