Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 18 Jul 2014 18:31:46 -0700
changeset 195009 96a0be5c760b3298a9da679aac970b8819cc22a5
parent 194999 961c13085948f6baf16fd56749e199d8c3a931fd (current diff)
parent 195008 b69b7c847c5682780dd76c545b3d73b1785479ed (diff)
child 195010 1f124b3a13555227443b93b695f189ff6e789bf0
child 195022 54755983f855342fa664f24f88f7b192443868fd
child 195070 d407b2eb63213272418b49bfee87631d60f213df
child 195118 0ce8308e5147fde778a64f18ddc9225feeb48000
push id27162
push userkwierso@gmail.com
push dateSat, 19 Jul 2014 01:36:10 +0000
treeherdermozilla-central@96a0be5c760b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
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 a=merge
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -426,11 +426,12 @@ skip-if = (os == "win" && !debug) || e10
 skip-if = e10s # Bug ?????? - test directly manipulates content (content.document.getElementById)
 [browser_zbug569342.js]
 skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
 [browser_registerProtocolHandler_notification.js]
 skip-if = e10s # Bug 940206 - nsIWebContentHandlerRegistrar::registerProtocolHandler doesn't work in e10s
 [browser_no_mcb_on_http_site.js]
 skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
 [browser_bug1003461-switchtab-override.js]
+[browser_bug1024133-switchtab-override-keynav.js]
 [browser_bug1025195_switchToTabHavingURI_ignoreFragment.js]
 [browser_addCertException.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (content.document.getElementById)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1024133-switchtab-override-keynav.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+add_task(function* test_switchtab_override_keynav() {
+  let testURL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+  info("Opening first tab");
+  let tab = gBrowser.addTab(testURL);
+  let tabLoadDeferred = Promise.defer();
+  whenTabLoaded(tab, tabLoadDeferred.resolve);
+  yield tabLoadDeferred.promise;
+
+  info("Opening and selecting second tab");
+  let secondTab = gBrowser.selectedTab = gBrowser.addTab();
+  registerCleanupFunction(() => {
+    try {
+      gBrowser.removeTab(tab);
+      gBrowser.removeTab(secondTab);
+    } catch(ex) { /* tabs may have already been closed in case of failure */ }
+    return promiseClearHistory();
+  });
+
+  info("Wait for autocomplete")
+  let searchDeferred = Promise.defer();
+  let onSearchComplete = gURLBar.onSearchComplete;
+  registerCleanupFunction(() => {
+    gURLBar.onSearchComplete = onSearchComplete;
+  });
+  gURLBar.onSearchComplete = function () {
+    ok(gURLBar.popupOpen, "The autocomplete popup is correctly open");
+    onSearchComplete.apply(gURLBar);
+    searchDeferred.resolve();
+  }
+
+  gURLBar.focus();
+  gURLBar.value = "dummy_pag";
+  EventUtils.synthesizeKey("e" , {});
+  yield searchDeferred.promise;
+
+  info("Select first autocomplete popup entry");
+  EventUtils.synthesizeKey("VK_DOWN" , {});
+  ok(/moz-action:switchtab/.test(gURLBar.value), "switch to tab entry found");
+
+  info("Shift+left on switch-to-tab entry");
+
+  EventUtils.synthesizeKey("VK_SHIFT" , { type: "keydown" });
+  EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+  EventUtils.synthesizeKey("VK_SHIFT" , { type: "keyup" });
+
+  ok(!/moz-action:switchtab/.test(gURLBar.inputField.value), "switch to tab should be hidden");
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -141,20 +141,23 @@
         It should return the value that the setter should use.
       -->
       <method name="onBeforeValueSet">
         <parameter name="aValue"/>
         <body><![CDATA[
           this._value = aValue;
           var returnValue = aValue;
           var action = this._parseActionUrl(aValue);
-          // Don't put back the action if we are invoked while override actions
-          // is active.
-          if (action && this._numNoActionsKeys <= 0) {
+
+          if (action) {
             returnValue = action.param;
+          }
+
+          // Set the actiontype only if the user is not overriding actions.
+          if (action && this._noActionsKeys.size == 0) {
             this.setAttribute("actiontype", action.type);
           } else {
             this.removeAttribute("actiontype");
           }
           return returnValue;
         ]]></body>
       </method>
 
@@ -712,24 +715,24 @@
             return null;
 
           // url is in the format moz-action:ACTION,PARAM
           let [, action, param] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
           return {type: action, param: param};
         ]]></body>
       </method>
 
-      <field name="_numNoActionsKeys"><![CDATA[
-        0
+      <field name="_noActionsKeys"><![CDATA[
+        new Set();
       ]]></field>
 
       <method name="_clearNoActions">
         <parameter name="aURL"/>
         <body><![CDATA[
-          this._numNoActionsKeys = 0;
+          this._noActionsKeys.clear();
           this.popup.removeAttribute("noactions");
           let action = this._parseActionUrl(this._value);
           if (action)
             this.setAttribute("actiontype", action.type);
         ]]></body>
       </method>
 
       <method name="selectTextRange">
@@ -741,29 +744,32 @@
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="keydown"><![CDATA[
         if ((event.keyCode === KeyEvent.DOM_VK_ALT ||
              event.keyCode === KeyEvent.DOM_VK_SHIFT) &&
-            this.popup.selectedIndex >= 0) {
-          this._numNoActionsKeys++;
-          this.popup.setAttribute("noactions", "true");
-          this.removeAttribute("actiontype");
+            this.popup.selectedIndex >= 0 &&
+            !this._noActionsKeys.has(event.keyCode)) {
+          if (this._noActionsKeys.size == 0) {
+            this.popup.setAttribute("noactions", "true");
+            this.removeAttribute("actiontype");
+          }
+          this._noActionsKeys.add(event.keyCode);
         }
       ]]></handler>
 
       <handler event="keyup"><![CDATA[
         if ((event.keyCode === KeyEvent.DOM_VK_ALT ||
              event.keyCode === KeyEvent.DOM_VK_SHIFT) &&
-            this._numNoActionsKeys > 0) {
-          this._numNoActionsKeys--;
-          if (this._numNoActionsKeys == 0)
+            this._noActionsKeys.has(event.keyCode)) {
+          this._noActionsKeys.delete(event.keyCode);
+          if (this._noActionsKeys.size == 0)
             this._clearNoActions();
         }
       ]]></handler>
 
       <handler event="blur"><![CDATA[
         this._clearNoActions();
         this.formatValue();
       ]]></handler>
--- a/dom/webidl/InspectorUtils.webidl
+++ b/dom/webidl/InspectorUtils.webidl
@@ -9,8 +9,21 @@ dictionary InspectorRGBTriple {
    * they can be outside the 0-255 range, but for backwards-compatible
    * named colors (which is what we use this dictionary for) the 0-255
    * assumption is fine.
    */
   octet r = 0;
   octet g = 0;
   octet b = 0;
 };
+
+dictionary InspectorRGBATuple {
+  /*
+   * NOTE: This tuple is in the normal 0-255-sized RGB space but can be
+   * fractional and may extend outside the 0-255 range.
+   *
+   * a is in the range 0 - 1.
+   */
+  double r = 0;
+  double g = 0;
+  double b = 0;
+  double a = 1;
+};
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -34,19 +34,22 @@
 #include "nsContentList.h"
 #include "mozilla/CSSStyleSheet.h"
 #include "mozilla/dom/Element.h"
 #include "nsRuleWalker.h"
 #include "nsRuleProcessorData.h"
 #include "nsCSSRuleProcessor.h"
 #include "mozilla/dom/InspectorUtilsBinding.h"
 #include "mozilla/dom/ToJSValue.h"
+#include "nsCSSParser.h"
 #include "nsCSSProps.h"
+#include "nsCSSValue.h"
 #include "nsColor.h"
 #include "nsStyleSet.h"
+#include "nsStyleUtil.h"
 
 using namespace mozilla;
 using namespace mozilla::css;
 using namespace mozilla::dom;
 
 ///////////////////////////////////////////////////////////////////////////////
 
 inDOMUtils::inDOMUtils()
@@ -791,16 +794,76 @@ inDOMUtils::RgbToColorName(uint8_t aR, u
     return NS_ERROR_INVALID_ARG;
   }
 
   aColorName.AssignASCII(color);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+inDOMUtils::ColorToRGBA(const nsAString& aColorString, JSContext* aCx,
+                        JS::MutableHandle<JS::Value> aValue)
+{
+  nscolor color = 0;
+  nsCSSParser cssParser;
+  nsCSSValue cssValue;
+
+  bool isColor = cssParser.ParseColorString(aColorString, nullptr, 0,
+                                            cssValue, true);
+
+  if (!isColor) {
+    aValue.setNull();
+    return NS_OK;
+  }
+
+  nsRuleNode::ComputeColor(cssValue, nullptr, nullptr, color);
+
+  InspectorRGBATuple tuple;
+  tuple.mR = NS_GET_R(color);
+  tuple.mG = NS_GET_G(color);
+  tuple.mB = NS_GET_B(color);
+  tuple.mA = nsStyleUtil::ColorComponentToFloat(NS_GET_A(color));
+
+  if (!ToJSValue(aCx, tuple, aValue)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+inDOMUtils::IsValidCSSColor(const nsAString& aColorString, bool *_retval)
+{
+  nsCSSParser cssParser;
+  nsCSSValue cssValue;
+  *_retval = cssParser.ParseColorString(aColorString, nullptr, 0, cssValue, true);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+inDOMUtils::CssPropertyIsValid(const nsAString& aPropertyName,
+                               const nsAString& aPropertyValue,
+                               bool *_retval)
+{
+  nsCSSProperty propertyID =
+    nsCSSProps::LookupProperty(aPropertyName, nsCSSProps::eIgnoreEnabledState);
+
+  if (propertyID == eCSSProperty_UNKNOWN) {
+    *_retval = false;
+    return NS_OK;
+  }
+
+  // Get a parser, parse the property.
+  nsCSSParser parser;
+  *_retval = parser.IsValueValidForProperty(propertyID, aPropertyValue);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 inDOMUtils::GetBindingURLs(nsIDOMElement *aElement, nsIArray **_retval)
 {
   NS_ENSURE_ARG_POINTER(aElement);
 
   *_retval = nullptr;
 
   nsCOMPtr<nsIMutableArray> urls = do_CreateInstance(NS_ARRAY_CONTRACTID);
   if (!urls)
--- a/layout/inspector/inIDOMUtils.idl
+++ b/layout/inspector/inIDOMUtils.idl
@@ -12,17 +12,17 @@ interface nsIDOMDocument;
 interface nsIDOMCSSRule;
 interface nsIDOMCSSStyleRule;
 interface nsIDOMNode;
 interface nsIDOMNodeList;
 interface nsIDOMFontFaceList;
 interface nsIDOMRange;
 interface nsIDOMCSSStyleSheet;
 
-[scriptable, uuid(fd529e53-f734-4d15-83ce-d545a631d668)]
+[scriptable, uuid(bd6b3dee-b8dd-40c7-a40a-ad8455b49917)]
 interface inIDOMUtils : nsISupports
 {
   // CSS utilities
   void getAllStyleSheets (in nsIDOMDocument aDoc,
                           [optional] out unsigned long aLength,
                           [array, size_is (aLength), retval] out nsISupports aSheets);
   nsISupportsArray getCSSStyleRules(in nsIDOMElement aElement, [optional] in DOMString aPseudo);
   unsigned long getRuleLine(in nsIDOMCSSRule aRule);
@@ -64,18 +64,35 @@ interface inIDOMUtils : nsISupports
                                [optional] out unsigned long aLength,
                                [array, size_is(aLength), retval] out wstring aValues);
 
   // Utilities for working with CSS colors
   [implicit_jscontext]
   jsval colorNameToRGB(in DOMString aColorName);
   AString rgbToColorName(in octet aR, in octet aG, in octet aB);
 
+  // Convert a given CSS color string to rgba. Returns null on failure or an
+  // InspectorRGBATuple on success.
+  //
+  // NOTE: Converting a color to RGBA may be lossy when converting from some
+  // formats e.g. CMYK.
+  [implicit_jscontext]
+  jsval colorToRGBA(in DOMString aColorString);
+
+  // Check whether a given color is a valid CSS color.
+  bool isValidCSSColor(in AString aColorString);
+
   // Utilities for obtaining information about a CSS property.
 
+  // Check whether a CSS property and value are a valid combination. If the
+  // property is pref-disabled it will still be processed.
+  // aPropertyName: A property name e.g. "color"
+  // aPropertyValue: A property value e.g. "red" or "red !important"
+  bool cssPropertyIsValid(in AString aPropertyName, in AString aPropertyValue);
+
   // Get a list of the longhands corresponding to the given CSS property.  If
   // the property is a longhand already, just returns the property itself.
   // Throws on unsupported property names.
   void getSubpropertiesForCSSProperty(in AString aProperty,
                                       [optional] out unsigned long aLength,
                                       [array, size_is(aLength), retval] out wstring aValues);
   // Check whether a given CSS property is a shorthand.  Throws on unsupported
   // property names.
--- a/layout/inspector/tests/mochitest.ini
+++ b/layout/inspector/tests/mochitest.ini
@@ -7,10 +7,13 @@ support-files = bug856317.css
 [test_bug536379-2.html]
 [test_bug536379.html]
 [test_bug557726.html]
 [test_bug609549.xhtml]
 [test_bug806192.html]
 [test_bug856317.html]
 [test_bug877690.html]
 [test_bug1006595.html]
+[test_color_to_rgba.html]
+[test_is_valid_css_color.html]
+[test_css_property_is_valid.html]
 [test_get_all_style_sheets.html]
 [test_isinheritableproperty.html]
new file mode 100644
--- /dev/null
+++ b/layout/inspector/tests/test_color_to_rgba.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test inDOMUtils::ColorToRGBA</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8">
+  let utils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]
+                           .getService(SpecialPowers.Ci.inIDOMUtils);
+
+  testColor("red", {r:255, g:0, b:0, a:1});
+  testColor("#f00", {r:255, g:0, b:0, a:1});
+  testColor("#ff0000", {r:255, g:0, b:0, a:1});
+  testColor("ff0000", null);
+  testColor("rgb(255,0,0)", {r:255, g:0, b:0, a:1});
+  testColor("rgba(255,0,0,0.7)", {r:255, g:0, b:0, a:0.7});
+  testColor("rgb(255,0,0,0.7)", null);
+  testColor("rgb(50%,75%,60%)", {r:128, g:191, b:153, a:1});
+  testColor("rgba(100%,50%,25%,0.7)", {r:255, g:128, b:64, a:0.7});
+  testColor("hsl(320,30%,10%)", {r:33, g:17, b:28, a:1});
+  testColor("hsla(170,60%,40%,0.9)", {r:40, g:163, b:142, a:0.9});
+
+  function testColor(color, expected) {
+    let rgb = utils.colorToRGBA(color);
+
+    if (rgb === null) {
+      ok(expected === null, "color: " + color + " returns null");
+      return;
+    }
+
+    let {r, g, b, a} = rgb;
+
+    is(r, expected.r, "color: " + color + ", red component is converted correctly");
+    is(g, expected.g, "color: " + color + ", green component is converted correctly");
+    is(b, expected.b, "color: " + color + ", blue component is converted correctly");
+    is(Math.round(a * 10) / 10, expected.a, "color: " + color + ", alpha component is a converted correctly");
+  }
+  </script>
+</head>
+<body>
+<h1>Test inDOMUtils::ColorToRGBA</h1>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/inspector/tests/test_css_property_is_valid.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test inDOMUtils::CssPropertyIsValid</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8">
+  let utils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]
+                           .getService(SpecialPowers.Ci.inIDOMUtils);
+
+  let tests = [
+    {
+      property: "color",
+      value: "red",
+      expected: true
+    },
+    {
+      property: "display",
+      value: "none",
+      expected: true
+    },
+    {
+      property: "display",
+      value: "red",
+      expected: false
+    },
+    {
+      property: "displayx",
+      value: "none",
+      expected: false
+    },
+    {
+      property: "border",
+      value: "1px solid blue",
+      expected: true
+    },
+    {
+      property: "border",
+      value: "1 solid blue",
+      expected: false
+    },
+    {
+      property: "border",
+      value: "1px underline blue",
+      expected: false
+    },
+    {
+      property: "border",
+      value: "1px solid",
+      expected: true
+    },
+    {
+      property: "color",
+      value: "blue !important",
+      expected: true
+    },
+    {
+      property: "color",
+      value: "blue ! important",
+      expected: true
+    },
+    {
+      property: "color",
+      value: "blue !impoxxxrtant",
+      expected: false
+    },
+    {
+      property: "color",
+      value: "red; background:green;",
+      expected: false
+    },
+    {
+      property: "content",
+      value: "\"hello\"",
+      expected: true
+    }
+  ];
+
+  for (let {property, value, expected} of tests) {
+    let valid = utils.cssPropertyIsValid(property, value);
+
+    if (expected) {
+      ok(valid, property + ":" + value + " is valid");
+    } else {
+      ok(!valid, property + ":" + value + " is not valid");
+    }
+  }
+  </script>
+</head>
+<body>
+<h1>Test inDOMUtils::CssPropertyIsValid</h1>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/inspector/tests/test_is_valid_css_color.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test inDOMUtils::isValidCSSColor</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8">
+  let utils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]
+                           .getService(SpecialPowers.Ci.inIDOMUtils);
+
+  // Color names
+  let colors = utils.getCSSValuesForProperty("color");
+  let notColor = ["hsl", "hsla", "inherit", "initial", "rgb", "rgba", "unset"];
+  for (let color of colors) {
+    if (notColor.indexOf(color) !== -1) {
+      continue;
+    }
+    ok(utils.isValidCSSColor(color), color + " is a valid color");
+    ok(!utils.isValidCSSColor("xxx" + color), "xxx" + color + " is not a valid color");
+  }
+
+  // rgb(a)
+  for (let i = 0; i <= 265; i++) {
+    ok(utils.isValidCSSColor("rgb(" + i + ",0,0)"), "rgb(" + i + ",0,0) is a valid color");
+    ok(utils.isValidCSSColor("rgb(0," + i + ",0)"), "rgb(0," + i + ",0) is a valid color");
+    ok(utils.isValidCSSColor("rgb(0,0," + i + ")"), "rgb(0,0," + i + ") is a valid color");
+    ok(utils.isValidCSSColor("rgba(" + i + ",0,0,0.2)"), "rgba(" + i + ",0,0,0.2) is a valid color");
+    ok(utils.isValidCSSColor("rgba(0," + i + ",0,0.5)"), "rgba(0," + i + ",0,0.5) is a valid color");
+    ok(utils.isValidCSSColor("rgba(0,0," + i + ",0.7)"), "rgba(0,0," + i + ",0.7) is a valid color");
+
+    ok(!utils.isValidCSSColor("rgbxxx(" + i + ",0,0)"), "rgbxxx(" + i + ",0,0) is not a valid color");
+    ok(!utils.isValidCSSColor("rgbxxx(0," + i + ",0)"), "rgbxxx(0," + i + ",0) is not a valid color");
+    ok(!utils.isValidCSSColor("rgbxxx(0,0," + i + ")"), "rgbxxx(0,0," + i + ") is not a valid color");
+  }
+
+  // rgb(a) (%)
+  for (let i = 0; i <= 110; i++) {
+    ok(utils.isValidCSSColor("rgb(" + i + "%,0%,0%)"), "rgb(" + i + "%,0%,0%) is a valid color");
+    ok(utils.isValidCSSColor("rgb(0%," + i + "%,0%)"), "rgb(0%," + i + "%,0%) is a valid color");
+    ok(utils.isValidCSSColor("rgb(0%,0%," + i + "%)"), "rgb(0%,0%," + i + "%) is a valid color");
+    ok(utils.isValidCSSColor("rgba(" + i + "%,0%,0%,0.2)"), "rgba(" + i + "%,0%,0%,0.2) is a valid color");
+    ok(utils.isValidCSSColor("rgba(0%," + i + "%,0%,0.5)"), "rgba(0%," + i + "%,0%,0.5) is a valid color");
+    ok(utils.isValidCSSColor("rgba(0%,0%," + i + "%,0.7)"), "rgba(0%,0%," + i + "%,0.7) is a valid color");
+
+    ok(!utils.isValidCSSColor("rgbaxxx(" + i + "%,0%,0%,0.2)"), "rgbaxxx(" + i + "%,0%,0%,0.2) is not a valid color");
+    ok(!utils.isValidCSSColor("rgbaxxx(0%," + i + "%,0%,0.5)"), "rgbaxxx(0%," + i + "%,0%,0.5) is not a valid color");
+    ok(!utils.isValidCSSColor("rgbaxxx(0%,0%," + i + "%,0.7)"), "rgbaxxx(0%,0%," + i + "%,0.7) is not a valid color");
+  }
+
+  // hsl(a)
+  for (let i = 0; i <= 370; i++) {
+    ok(utils.isValidCSSColor("hsl(" + i + ",30%,10%)"), "rgb(" + i + ",30%,10%) is a valid color");
+    ok(utils.isValidCSSColor("hsla(" + i + ",60%,70%,0.2)"), "rgba(" + i + ",60%,70%,0.2) is a valid color");
+  }
+  for (let i = 0; i <= 110; i++) {
+    ok(utils.isValidCSSColor("hsl(100," + i + "%,20%)"), "hsl(100," + i + "%,20%) is a valid color");
+    ok(utils.isValidCSSColor("hsla(100,20%," + i + "%,0.6)"), "hsla(100,20%," + i + "%,0.6) is a valid color");
+  }
+
+  // hex
+  for (let i = 0; i <= 255; i++) {
+    let hex = (i).toString(16);
+    if (hex.length === 1) {
+      hex = 0 + hex;
+    }
+    ok(utils.isValidCSSColor("#" + hex + "7777"), "#" + hex + "7777 is a valid color");
+    ok(utils.isValidCSSColor("#77" + hex + "77"), "#77" + hex + "77 is a valid color");
+    ok(utils.isValidCSSColor("#7777" + hex), "#7777" + hex + " is a valid color");
+  }
+  ok(!utils.isValidCSSColor("#kkkkkk"), "#kkkkkk is not a valid color");
+
+  // short hex
+  for (let i = 0; i <= 16; i++) {
+    let hex = (i).toString(16);
+    ok(utils.isValidCSSColor("#" + hex + hex + hex), "#" + hex + hex + hex + " is a valid color");
+  }
+  ok(!utils.isValidCSSColor("#ggg"), "#ggg is not a valid color");
+  </script>
+</head>
+<body>
+<h1>Test inDOMUtils::isValidCSSColor</h1>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -172,17 +172,18 @@ public:
   bool ParseFontFamilyListString(const nsSubstring& aBuffer,
                                  nsIURI* aURL, // for error reporting
                                  uint32_t aLineNumber, // for error reporting
                                  nsCSSValue& aValue);
 
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI* aURL, // for error reporting
                         uint32_t aLineNumber, // for error reporting
-                        nsCSSValue& aValue);
+                        nsCSSValue& aValue,
+                        bool aSuppressErrors /* false */);
 
   nsresult ParseSelectorString(const nsSubstring& aSelectorString,
                                nsIURI* aURL, // for error reporting
                                uint32_t aLineNumber, // for error reporting
                                nsCSSSelectorList **aSelectorList);
 
   already_AddRefed<nsCSSKeyframeRule>
   ParseKeyframeRule(const nsSubstring& aBuffer,
@@ -211,16 +212,18 @@ public:
 
   bool ParseCounterDescriptor(nsCSSCounterDesc aDescID,
                               const nsAString& aBuffer,
                               nsIURI* aSheetURL,
                               nsIURI* aBaseURL,
                               nsIPrincipal* aSheetPrincipal,
                               nsCSSValue& aValue);
 
+  bool IsValueValidForProperty(const nsCSSProperty aPropID,
+                               const nsAString& aPropValue);
 
   typedef nsCSSParser::VariableEnumFunc VariableEnumFunc;
 
   /**
    * Parses a CSS token stream value and invokes a callback function for each
    * variable reference that is encountered.
    *
    * @param aPropertyValue The CSS token stream value.
@@ -1702,25 +1705,34 @@ CSSParserImpl::ParseSourceSizeList(const
 
   return !hitError;
 }
 
 bool
 CSSParserImpl::ParseColorString(const nsSubstring& aBuffer,
                                 nsIURI* aURI, // for error reporting
                                 uint32_t aLineNumber, // for error reporting
-                                nsCSSValue& aValue)
+                                nsCSSValue& aValue,
+                                bool aSuppressErrors /* false */)
 {
   nsCSSScanner scanner(aBuffer, aLineNumber);
   css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aURI);
   InitScanner(scanner, reporter, aURI, aURI, nullptr);
 
+  nsAutoSuppressErrors suppressErrors(this, aSuppressErrors);
+
   // Parse a color, and check that there's nothing else after it.
   bool colorParsed = ParseColor(aValue) && !GetToken(true);
-  OUTPUT_ERROR();
+
+  if (aSuppressErrors) {
+    CLEAR_ERROR();
+  } else {
+    OUTPUT_ERROR();
+  }
+
   ReleaseScanner();
   return colorParsed;
 }
 
 bool
 CSSParserImpl::ParseFontFamilyListString(const nsSubstring& aBuffer,
                                          nsIURI* aURI, // for error reporting
                                          uint32_t aLineNumber, // for error reporting
@@ -14663,16 +14675,57 @@ CSSParserImpl::ParseValueWithVariables(C
   while (i--) {
     aImpliedCharacters.Append(stack[i]);
   }
 
   *aType = type;
   return true;
 }
 
+bool
+CSSParserImpl::IsValueValidForProperty(const nsCSSProperty aPropID,
+                                       const nsAString& aPropValue)
+{
+  mData.AssertInitialState();
+  mTempData.AssertInitialState();
+
+  nsCSSScanner scanner(aPropValue, 0);
+  css::ErrorReporter reporter(scanner, mSheet, mChildLoader, nullptr);
+  InitScanner(scanner, reporter, nullptr, nullptr, nullptr);
+
+  nsAutoSuppressErrors suppressErrors(this);
+
+  mSection = eCSSSection_General;
+  scanner.SetSVGMode(false);
+
+  // Check for unknown properties
+  if (eCSSProperty_UNKNOWN == aPropID) {
+    ReleaseScanner();
+    return false;
+  }
+
+  // Check that the property and value parse successfully
+  bool parsedOK = ParseProperty(aPropID);
+
+  // Check for priority
+  parsedOK = parsedOK && ParsePriority() != ePriority_Error;
+
+  // We should now be at EOF
+  parsedOK = parsedOK && !GetToken(true);
+
+  mTempData.ClearProperty(aPropID);
+  mTempData.AssertInitialState();
+  mData.AssertInitialState();
+
+  CLEAR_ERROR();
+  ReleaseScanner();
+
+  return parsedOK;
+}
+
 } // anonymous namespace
 
 // Recycling of parser implementation objects
 
 static CSSParserImpl* gFreeList = nullptr;
 
 nsCSSParser::nsCSSParser(mozilla::css::Loader* aLoader,
                          CSSStyleSheet* aSheet)
@@ -14855,20 +14908,21 @@ nsCSSParser::ParseFontFamilyListString(c
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseFontFamilyListString(aBuffer, aURI, aLineNumber, aValue);
 }
 
 bool
 nsCSSParser::ParseColorString(const nsSubstring& aBuffer,
                               nsIURI*            aURI,
                               uint32_t           aLineNumber,
-                              nsCSSValue&        aValue)
+                              nsCSSValue&        aValue,
+                              bool               aSuppressErrors /* false */)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
-    ParseColorString(aBuffer, aURI, aLineNumber, aValue);
+    ParseColorString(aBuffer, aURI, aLineNumber, aValue, aSuppressErrors);
 }
 
 nsresult
 nsCSSParser::ParseSelectorString(const nsSubstring&  aSelectorString,
                                  nsIURI*             aURI,
                                  uint32_t            aLineNumber,
                                  nsCSSSelectorList** aSelectorList)
 {
@@ -14976,8 +15030,17 @@ nsCSSParser::ParseCounterDescriptor(nsCS
                                     nsIURI* aBaseURL,
                                     nsIPrincipal* aSheetPrincipal,
                                     nsCSSValue& aValue)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseCounterDescriptor(aDescID, aBuffer,
                            aSheetURL, aBaseURL, aSheetPrincipal, aValue);
 }
+
+bool
+nsCSSParser::IsValueValidForProperty(const nsCSSProperty aPropID,
+                                     const nsAString&    aPropValue)
+{
+  return static_cast<CSSParserImpl*>(mImpl)->
+    IsValueValidForProperty(aPropID, aPropValue);
+}
+
--- a/layout/style/nsCSSParser.h
+++ b/layout/style/nsCSSParser.h
@@ -186,17 +186,18 @@ public:
    * Parse aBuffer into a nsCSSValue |aValue|. Will return false
    * if aBuffer is not a valid CSS color specification.
    * One can use nsRuleNode::ComputeColor to compute an nscolor from
    * the returned nsCSSValue.
    */
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI*            aURL,
                         uint32_t           aLineNumber,
-                        nsCSSValue&        aValue);
+                        nsCSSValue&        aValue,
+                        bool               aSuppressErrors = false);
 
   /**
    * Parse aBuffer into a selector list.  On success, caller must
    * delete *aSelectorList when done with it.
    */
   nsresult ParseSelectorString(const nsSubstring&  aSelectorString,
                                nsIURI*             aURL,
                                uint32_t            aLineNumber,
@@ -291,16 +292,20 @@ public:
 
   bool ParseCounterDescriptor(nsCSSCounterDesc aDescID,
                               const nsAString& aBuffer,
                               nsIURI* aSheetURL,
                               nsIURI* aBaseURL,
                               nsIPrincipal* aSheetPrincipal,
                               nsCSSValue& aValue);
 
+  // Check whether a given value can be applied to a property.
+  bool IsValueValidForProperty(const nsCSSProperty aPropID,
+                               const nsAString&    aPropValue);
+
 protected:
   // This is a CSSParserImpl*, but if we expose that type name in this
   // header, we can't put the type definition (in nsCSSParser.cpp) in
   // the anonymous namespace.
   void* mImpl;
 };
 
 #endif /* nsCSSParser_h___ */
--- a/mobile/android/base/tests/testSimpleDiscovery.js
+++ b/mobile/android/base/tests/testSimpleDiscovery.js
@@ -21,17 +21,19 @@ function discovery_observer(subject, top
   do_check_eq(service.manufacturer, "Copy Cat Inc.");
   do_check_eq(service.modelName, "Eureka Dongle");
 
   run_next_test();
 };
 
 var testTarget = {
   target: "test:service",
-  factory: function(service) { /* dummy */  }
+  factory: function(service) { /* dummy */  },
+  types: ["video/mp4"],
+  extensions: ["mp4"]
 };
 
 add_test(function test_default() {
   do_register_cleanup(function cleanup() {
     SimpleServiceDiscovery.unregisterTarget(testTarget);
     Services.obs.removeObserver(discovery_observer, "ssdp-service-found");
   });
 
--- a/mobile/android/base/tests/testVideoDiscovery.js
+++ b/mobile/android/base/tests/testVideoDiscovery.js
@@ -41,18 +41,20 @@ add_test(function setup_browser() {
     Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
   }, true);
 });
 
 let videoDiscoveryTests = [
   { id: "simple-mp4", source: "http://mochi.test:8888/simple.mp4", poster: "http://mochi.test:8888/simple.png", text: "simple video with mp4 src" },
   { id: "simple-fail", pass: false, text: "simple video with no mp4 src" },
   { id: "with-sources-mp4", source: "http://mochi.test:8888/simple.mp4", text: "video with mp4 extension source child" },
+  { id: "with-sources-webm", source: "http://mochi.test:8888/simple.webm", text: "video with webm extension source child" },
   { id: "with-sources-fail", pass: false, text: "video with no mp4 extension source child" },
-  { id: "with-sources-mimetype", source: "http://mochi.test:8888/simple-video-mp4", text: "video with mp4 mimetype source child" },
+  { id: "with-sources-mimetype-mp4", source: "http://mochi.test:8888/simple-video-mp4", text: "video with mp4 mimetype source child" },
+  { id: "with-sources-mimetype-webm", source: "http://mochi.test:8888/simple-video-webm", text: "video with webm mimetype source child" },
   { id: "video-overlay", source: "http://mochi.test:8888/simple.mp4", text: "div overlay covering a simple video with mp4 src" }
 ];
 
 function execute_video_test(test) {
   let element = browser.contentDocument.getElementById(test.id);
   if (element) {
     let [x, y] = middle(element);
     let video = chromeWin.CastingApps.getVideo(element, x, y);
--- a/mobile/android/base/tests/video_discovery.html
+++ b/mobile/android/base/tests/video_discovery.html
@@ -26,28 +26,39 @@
     <video id="simple-fail" src="/simple.ogg"></video>
 
     <!-- PASS: source list uses a mp4 extension -->
     <video id="with-sources-mp4">
       <source src="/simple.ogg">
       <source src="/simple.mp4">
     </video>
 
-    <!-- FAIL: source list uses a mp4 extension -->
-    <video id="with-sources-fail">
+    <!-- PASS: source list uses a webm extension -->
+    <video id="with-sources-webm">
       <source src="/simple.ogg">
       <source src="/simple.webm">
     </video>
 
+    <!-- FAIL: source list has no mp4 or webm extension -->
+    <video id="with-sources-fail">
+      <source src="/simple.ogg">
+    </video>
+
     <!-- PASS: source list uses a mp4 mimetype -->
-    <video id="with-sources-mimetype">
+    <video id="with-sources-mimetype-mp4">
       <source src="/simple-video-ogg" type="video/ogg">
       <source src="/simple-video-mp4" type="video/mp4">
     </video>
 
+    <!-- PASS: source list uses a webm mimetype -->
+    <video id="with-sources-mimetype-webm">
+      <source src="/simple-video-ogg" type="video/ogg">
+      <source src="/simple-video-webm" type="video/webm">
+    </video>
+
     <!-- PASS: source list uses a mp4 mimetype and extra data -->
     <video id="with-sources-mimetype-plus">
       <source src="/simple-video-ogg" type="video/ogg">
       <source src="/simple-video-mp4" type="video/mp4; codecs='avc1.42E01E, mp4a.40.2'">
     </video>
 
     <!-- PASS: div overlay covers a video with mp4 src -->
     <div id="video-box">
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -1,41 +1,48 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 /* 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";
 
 // Define service targets. We should consider moving these to their respective
 // JSM files, but we left them here to allow for better lazy JSM loading.
 var rokuTarget = {
   target: "roku:ecp",
   factory: function(aService) {
     Cu.import("resource://gre/modules/RokuApp.jsm");
     return new RokuApp(aService);
-  }
+  },
+  types: ["video/mp4"],
+  extensions: ["mp4"]
 };
 
 var fireflyTarget = {
   target: "urn:dial-multiscreen-org:service:dial:1",
   filters: {
     server: null,
     modelName: "Eureka Dongle"
   },
   factory: function(aService) {
     Cu.import("resource://gre/modules/FireflyApp.jsm");
     return new FireflyApp(aService);
-  }
+  },
+  types: ["video/mp4", "video/webm"],
+  extensions: ["mp4", "webm"]
 };
 
 var mediaPlayerTarget = {
   target: "media:router",
   factory: function(aService) {
     Cu.import("resource://gre/modules/MediaPlayerApp.jsm");
     return new MediaPlayerApp(aService);
-  }
+  },
+  types: ["video/mp4", "video/webm", "application/x-mpegurl"],
+  extensions: ["mp4", "webm", "m3u", "m3u8"]
 };
 
 var CastingApps = {
   _castMenuId: -1,
 
   init: function ca_init() {
     if (!this.isEnabled()) {
       return;
@@ -168,86 +175,82 @@ var CastingApps = {
     this.openExternal(video, 0, 0);
   },
 
   makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
     return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
   },
 
   getVideo: function(aElement, aX, aY) {
+    let extensions = SimpleServiceDiscovery.getSupportedExtensions();
+    let types = SimpleServiceDiscovery.getSupportedMimeTypes();
+
     // Fast path: Is the given element a video element
-    let video = this._getVideo(aElement);
+    let video = this._getVideo(aElement, types, extensions);
     if (video) {
       return video;
     }
 
     // The context menu system will keep walking up the DOM giving us a chance
     // to find an element we match. When it hits <html> things can go BOOM.
     try {
       // Maybe this is an overlay, with the video element under it
       // Use the (x, y) location to guess at a <video> element
       let elements = aElement.ownerDocument.querySelectorAll("video");
       for (let element of elements) {
         // Look for a video element contained in the overlay bounds
         let rect = element.getBoundingClientRect();
         if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
-          video = this._getVideo(element);
+          video = this._getVideo(element, types, extensions);
           if (video) {
             break;
           }
         }
       }
     } catch(e) {}
 
     // Could be null
     return video;
   },
 
-  _getVideo: function(aElement) {
+  _getVideo: function(aElement, aTypes, aExtensions) {
     if (!(aElement instanceof HTMLVideoElement)) {
       return null;
     }
 
-    // Given the hardware support for H264, let's only look for 'mp4' sources
-    function allowableExtension(aURI) {
-      if (aURI && aURI instanceof Ci.nsIURL) {
-        return (aURI.fileExtension == "mp4");
-      }
-      return false;
-    }
 
     // Grab the poster attribute from the <video>
     let posterURL = aElement.poster;
 
     // First, look to see if the <video> has a src attribute
     let sourceURL = aElement.src;
 
     // If empty, try the currentSrc
     if (!sourceURL) {
       sourceURL = aElement.currentSrc;
     }
 
     if (sourceURL) {
       // Use the file extension to guess the mime type
       let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
-      if (allowableExtension(sourceURI)) {
-        return { element: aElement, source: sourceURI.spec, poster: posterURL };
+      if (this.allowableExtension(sourceURI, aExtensions)) {
+        return { element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI};
       }
     }
 
     // Next, look to see if there is a <source> child element that meets
     // our needs
     let sourceNodes = aElement.getElementsByTagName("source");
     for (let sourceNode of sourceNodes) {
       let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI));
 
       // Using the type attribute is our ideal way to guess the mime type. Otherwise,
       // fallback to using the file extension to guess the mime type
-      if (sourceNode.type == "video/mp4" || allowableExtension(sourceURI)) {
-        return { element: aElement, source: sourceURI.spec, poster: posterURL };
+      if (this.allowableMimeType(sourceNode.type, aTypes) || this.allowableExtension(sourceURI, aExtensions)) {
+        return { element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type };
       }
     }
 
     return null;
   },
 
   filterCast: {
     matches: function(aElement, aX, aY) {
@@ -356,31 +359,35 @@ var CastingApps = {
         title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
         icon: "drawable://casting",
         clickCallback: this.pageAction.click,
         important: true
       });
     }
   },
 
-  prompt: function(aCallback) {
+  prompt: function(aCallback, aFilterFunc) {
     let items = [];
+    let filteredServices = [];
     SimpleServiceDiscovery.services.forEach(function(aService) {
       let item = {
         label: aService.friendlyName,
         selected: false
       };
-      items.push(item);
+      if (!aFilterFunc || aFilterFunc(aService)) {
+        filteredServices.push(aService);
+        items.push(item);
+      }
     });
 
     let prompt = new Prompt({
       title: Strings.browser.GetStringFromName("casting.prompt")
     }).setSingleChoiceItems(items).show(function(data) {
       let selected = data.button;
-      let service = selected == -1 ? null : SimpleServiceDiscovery.services[selected];
+      let service = selected == -1 ? null : filteredServices[selected];
       if (aCallback)
         aCallback(service);
     });
   },
 
   handleContextMenu: function(aElement, aX, aY) {
     UITelemetry.addEvent("action.1", "contextmenu", null, "web_cast");
     UITelemetry.addEvent("cast.1", "contextmenu", null);
@@ -389,16 +396,20 @@ var CastingApps = {
 
   openExternal: function(aElement, aX, aY) {
     // Start a second screen media service
     let video = this.getVideo(aElement, aX, aY);
     if (!video) {
       return;
     }
 
+    function filterFunc(service) {
+      return this.allowableExtension(video.sourceURI, service.extensions) || this.allowableMimeType(video.type, service.types);
+    }
+
     this.prompt(function(aService) {
       if (!aService)
         return;
 
       // Make sure we have a player app for the given service
       let app = SimpleServiceDiscovery.findAppForService(aService);
       if (!app)
         return;
@@ -433,17 +444,17 @@ var CastingApps = {
                 source: video.source,
                 poster: video.poster
               },
               videoRef: Cu.getWeakReference(video.element)
             };
           }.bind(this), this);
         }.bind(this));
       }.bind(this));
-    }.bind(this));
+    }.bind(this), filterFunc.bind(this));
   },
 
   closeExternal: function() {
     if (!this.session) {
       return;
     }
 
     this.session.remoteMedia.shutdown();
@@ -482,10 +493,26 @@ var CastingApps = {
     if (!this.session) {
       return;
     }
 
     let status = aRemoteMedia.status;
     if (status == "completed") {
       this.closeExternal();
     }
+  },
+
+  allowableExtension: function(aURI, aExtensions) {
+    if (aURI && aURI instanceof Ci.nsIURL) {
+      for (let x in aExtensions) {
+        if (aURI.fileExtension == aExtensions[x]) return true;
+      }
+    }
+    return false;
+  },
+
+  allowableMimeType: function(aType, aTypes) {
+    for (let x in aTypes) {
+      if (aType == aTypes[x]) return true;
+    }
+    return false;
   }
 };
--- a/mobile/android/modules/SimpleServiceDiscovery.jsm
+++ b/mobile/android/modules/SimpleServiceDiscovery.jsm
@@ -1,9 +1,9 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 /* 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";
 
 this.EXPORTED_SYMBOLS = ["SimpleServiceDiscovery"];
 
@@ -238,16 +238,36 @@ var SimpleServiceDiscovery = {
         if (service.lastPing != this._searchTimestamp) {
           Services.obs.notifyObservers(null, EVENT_SERVICE_LOST, service.uuid);
           this._services.delete(service.uuid);
         }
       }
     }
   },
 
+  getSupportedExtensions: function() {
+    let extensions = [];
+    this._targets.forEach(function(target) {
+        extensions = extensions.concat(target.extensions);
+      }, this);
+    return extensions.filter(function(extension, pos) {
+      return extensions.indexOf(extension) == pos;
+    });
+  },
+
+  getSupportedMimeTypes: function() {
+    let types = [];
+    this._targets.forEach(function(target) {
+        types = types.concat(target.types);
+      }, this);
+    return types.filter(function(type, pos) {
+      return types.indexOf(type) == pos;
+    });
+  },
+
   registerTarget: function registerTarget(aTarget) {
     // We must have "target" and "factory" defined
     if (!("target" in aTarget) || !("factory" in aTarget)) {
       // Fatal for registration
       throw "Registration requires a target and a location";
     }
 
     // Only add if we don't already know about this target
@@ -286,16 +306,19 @@ var SimpleServiceDiscovery = {
     }
     return null;
   },
 
   // Returns an array copy of the active services
   get services() {
     let array = [];
     for (let [key, service] of this._services) {
+      let target = this._targets.get(service.target);
+      service.extensions = target.extensions;
+      service.types = target.types;
       array.push(service);
     }
     return array;
   },
 
   // Returns false if the service does not match the target's filters
   _filterService: function _filterService(aService) {
     let target = this._targets.get(aService.target);
--- a/toolkit/devtools/discovery/discovery.js
+++ b/toolkit/devtools/discovery/discovery.js
@@ -129,22 +129,121 @@ Transport.prototype = {
     }
     this.emit("message", object);
   },
 
   onStopListening: function() {}
 
 };
 
+/**
+ * Manages the local device's name.  The name can be generated in serveral
+ * platform-specific ways (see |_generate|).  The aim is for each device on the
+ * same local network to have a unique name.  If the Settings API is available,
+ * the name is saved there to persist across reboots.
+ */
+function LocalDevice() {
+  this._name = LocalDevice.UNKNOWN;
+  if ("@mozilla.org/settingsService;1" in Cc) {
+    this._settings =
+      Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
+    Services.obs.addObserver(this, "mozsettings-changed", false);
+  }
+  this._get(); // Trigger |_get| to load name eagerly
+}
+
+LocalDevice.SETTING = "devtools.discovery.device";
+LocalDevice.UNKNOWN = "unknown";
+
+LocalDevice.prototype = {
+
+  _get: function() {
+    if (!this._settings) {
+      // Without Settings API, just generate a name and stop, since the value
+      // can't be persisted.
+      this._generate();
+      return;
+    }
+    // Initial read of setting value
+    this._settings.createLock().get(LocalDevice.SETTING, {
+      handle: (_, name) => {
+        if (name && name !== LocalDevice.UNKNOWN) {
+          this._name = name;
+          log("Device: " + this._name);
+          return;
+        }
+        // No existing name saved, so generate one.
+        this._generate();
+      },
+      handleError: () => log("Failed to get device name setting")
+    });
+  },
+
+  /**
+   * Generate a new device name from various platform-specific properties.
+   * Triggers the |name| setter to persist if needed.
+   */
+  _generate: function() {
+    if (Services.appinfo.widgetToolkit == "gonk") {
+      // For Gonk devices, create one from the device name plus a little
+      // randomness.  The goal is just to distinguish devices in an office
+      // environment where many people may have the same device model for
+      // testing purposes (which would otherwise all report the same name).
+      let name = libcutils.property_get("ro.product.device");
+      // Pick a random number from [0, 2^32)
+      let randomID = Math.floor(Math.random() * Math.pow(2, 32));
+      // To hex and zero pad
+      randomID = ("00000000" + randomID.toString(16)).slice(-8);
+      this.name = name + "-" + randomID;
+    } else {
+      this.name = sysInfo.get("host");
+    }
+  },
+
+  /**
+   * Observe any changes that might be made via the Settings app
+   */
+  observe: function(subject, topic, data) {
+    if (topic !== "mozsettings-changed") {
+      return;
+    }
+    let setting = JSON.parse(data);
+    if (setting.key !== LocalDevice.SETTING) {
+      return;
+    }
+    this._name = setting.value;
+    log("Device: " + this._name);
+  },
+
+  get name() {
+    return this._name;
+  },
+
+  set name(name) {
+    if (!this._settings) {
+      this._name = name;
+      log("Device: " + this._name);
+      return;
+    }
+    // Persist to Settings API
+    // The new value will be seen and stored by the observer above
+    this._settings.createLock().set(LocalDevice.SETTING, name, {
+      handle: () => {},
+      handleError: () => log("Failed to set device name setting")
+    });
+  }
+
+};
+
 function Discovery() {
   EventEmitter.decorate(this);
 
   this.localServices = {};
   this.remoteServices = {};
-  this.device = { name: "unknown" };
+  this.device = new LocalDevice();
   this.replyTimeout = REPLY_TIMEOUT;
 
   // Defaulted to Transport, but can be altered by tests
   this._factories = { Transport: Transport };
 
   this._transports = {
     scan: null,
     update: null
@@ -153,18 +252,16 @@ function Discovery() {
     from: new Set()
   };
 
   this._onRemoteScan = this._onRemoteScan.bind(this);
   this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
   this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
 
   Services.obs.addObserver(this, "network-active-changed", false);
-
-  this._getSystemInfo();
 }
 
 Discovery.prototype = {
 
   /**
    * Add a new service offered by this device.
    * @param service string
    *        Name of the service
@@ -233,34 +330,16 @@ Discovery.prototype = {
 
   _waitForReplies: function() {
     clearTimeout(this._expectingReplies.timer);
     this._expectingReplies.from = new Set(this.getRemoteDevices());
     this._expectingReplies.timer =
       setTimeout(this._purgeMissingDevices, this.replyTimeout);
   },
 
-  /**
-   * Determine a unique name to identify the current device.
-   */
-  _getSystemInfo: function() {
-    // TODO Bug 1027787: Uniquify device name somehow?
-    try {
-      if (Services.appinfo.widgetToolkit == "gonk") {
-        this.device.name = libcutils.property_get("ro.product.device");
-      } else {
-        this.device.name = sysInfo.get("host");
-      }
-      log("Device: " + this.device.name);
-    } catch(e) {
-      log("Failed to get system info");
-      this.device.name = "unknown";
-    }
-  },
-
   get Transport() {
     return this._factories.Transport;
   },
 
   _startListeningForScan: function() {
     if (this._transports.scan) {
       return; // Already listening
     }
--- a/toolkit/devtools/discovery/tests/unit/test_discovery.js
+++ b/toolkit/devtools/discovery/tests/unit/test_discovery.js
@@ -64,17 +64,23 @@ TestTransport.prototype = {
   },
 
   onStopListening: function(socket, status) {}
 
 };
 
 // Use TestTransport instead of the usual Transport
 discovery._factories.Transport = TestTransport;
-discovery.device.name = "test-device";
+
+// Ignore name generation on b2g and force a fixed value
+Object.defineProperty(discovery.device, "name", {
+  get: function() {
+    return "test-device";
+  }
+});
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function*() {
   // At startup, no remote devices are known
   deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
--- a/toolkit/devtools/discovery/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/discovery/tests/unit/xpcshell.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 head =
 tail =
 
 [test_discovery.js]
+skip-if = toolkit == 'gonk' && debug # Debug doesn't like settings read