Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 11 Oct 2013 17:03:36 -0400
changeset 164360 1a71e45031aaf01e86a8eda986e3c8d2ac9e7844
parent 164359 c71abced5149f0527f0377130a4fe5d2c5048658 (current diff)
parent 164327 558cf02e6b9b66bfc365cf7eaad195ecac393e25 (diff)
child 164361 0f70219ff1516cf97eafb7b04f17a64d3142def9
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone27.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 m-c to inbound.
browser/devtools/shared/touch-events.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -13,9 +13,9 @@
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
-Bug 922461 needs a clobber to regenerate code and survive bug 925243's FAIL_ON_WARNINGS annotation.
+Bug 915002 - Clobber needed for webidl updates for AppNotificationServiceOptions. One more time.
\ No newline at end of file
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -882,28 +882,30 @@ var AlertsHelper = {
   receiveMessage: function alert_receiveMessage(aMessage) {
     if (!aMessage.target.assertAppHasPermission("desktop-notification")) {
       Cu.reportError("Desktop-notification message " + aMessage.name +
                      " from a content process with no desktop-notification privileges.");
       return;
     }
 
     let data = aMessage.data;
+    let details = data.details;
     let listener = {
       mm: aMessage.target,
       title: data.title,
       text: data.text,
-      manifestURL: data.manifestURL,
+      manifestURL: details.manifestURL,
       imageURL: data.imageURL
-    }
+    };
     this.registerAppListener(data.uid, listener);
 
     this.showNotification(data.imageURL, data.title, data.text,
-                          data.textClickable, null,
-                          data.uid, null, null, data.manifestURL);
+                          details.textClickable, null,
+                          data.uid, details.dir,
+                          details.lang, details.manifestURL);
   },
 }
 
 var WebappsHelper = {
   _installers: {},
   _count: 0,
 
   init: function webapps_init() {
--- a/b2g/components/AlertsService.js
+++ b/b2g/components/AlertsService.js
@@ -60,37 +60,35 @@ AlertsService.prototype = {
     let browser = Services.wm.getMostRecentWindow("navigator:browser");
     browser.AlertsHelper.closeAlert(aName);
   },
 
   // nsIAppNotificationService
   showAppNotification: function showAppNotification(aImageURL,
                                                     aTitle,
                                                     aText,
-                                                    aTextClickable,
-                                                    aManifestURL,
                                                     aAlertListener,
-                                                    aId) {
-    let uid = (aId == "") ? "app-notif-" + uuidGenerator.generateUUID() : aId;
+                                                    aDetails) {
+    let uid = (aDetails.id == "") ?
+          "app-notif-" + uuidGenerator.generateUUID() : aDetails.id;
 
     this._listeners[uid] = {
       observer: aAlertListener,
       title: aTitle,
       text: aText,
-      manifestURL: aManifestURL,
+      manifestURL: aDetails.manifestURL,
       imageURL: aImageURL
     };
 
     cpmm.sendAsyncMessage("app-notification-send", {
       imageURL: aImageURL,
       title: aTitle,
       text: aText,
-      textClickable: aTextClickable,
-      manifestURL: aManifestURL,
-      uid: uid
+      uid: uid,
+      details: aDetails
     });
   },
 
   // AlertsService.js custom implementation
   _listeners: [],
 
   receiveMessage: function receiveMessage(aMessage) {
     let data = aMessage.data;
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "c7c750d66279bf2b7f9164d8b57d425a028e1fd9", 
+    "revision": "e2429ca17a0b77ddffdd69a96fd413449f71256e", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4110,16 +4110,71 @@
 
         // See hack note in the tabbrowser-close-tab-button binding
         if (!this._blockDblClick)
           BrowserOpenTab();
 
         event.preventDefault();
       ]]></handler>
 
+      <handler event="click" button="0" phase="capturing"><![CDATA[
+        /* Catches extra clicks meant for the in-tab close button.
+         * Placed here to avoid leaking (a temporary handler added from the
+         * in-tab close button binding would close over the tab and leak it
+         * until the handler itself was removed). (bug 897751)
+         *
+         * The only sequence in which a second click event (i.e. dblclik)
+         * can be dispatched on an in-tab close button is when it is shown
+         * after the first click (i.e. the first click event was dispatched
+         * on the tab). This happens when we show the close button only on
+         * the active tab. (bug 352021)
+         * The only sequence in which a third click event can be dispatched
+         * on an in-tab close button is when the tab was opened with a
+         * double click on the tabbar. (bug 378344)
+         * In both cases, it is most likely that the close button area has
+         * been accidentally clicked, therefore we do not close the tab.
+         *
+         * We don't want to ignore processing of more than one click event,
+         * though, since the user might actually be repeatedly clicking to
+         * close many tabs at once.
+         */
+        let target = event.originalTarget;
+        if (target.className == 'tab-close-button') {
+          // We preemptively set this to allow the closing-multiple-tabs-
+          // in-a-row case.
+          if (this._blockDblClick) {
+            target._ignoredCloseButtonClicks = true;
+          } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
+            target._ignoredCloseButtonClicks = true;
+            event.stopPropagation();
+            return;
+          } else {
+            // Reset the "ignored click" flag
+            target._ignoredCloseButtonClicks = false;
+          }
+        }
+
+        /* Protects from close-tab-button errant doubleclick:
+         * Since we're removing the event target, if the user
+         * double-clicks the button, the dblclick event will be dispatched
+         * with the tabbar as its event target (and explicit/originalTarget),
+         * which treats that as a mouse gesture for opening a new tab.
+         * In this context, we're manually blocking the dblclick event
+         * (see tabbrowser-close-tab-button dblclick handler).
+         */
+        if (this._blockDblClick) {
+          if (!("_clickedTabBarOnce" in this)) {
+            this._clickedTabBarOnce = true;
+            return;
+          }
+          delete this._clickedTabBarOnce;
+          this._blockDblClick = false;
+        }
+      ]]></handler>
+
       <handler event="click"><![CDATA[
         if (event.button != 1)
           return;
 
         if (event.target.localName == "tab") {
           if (this.childNodes.length > 1 || !this._closeWindowWithLastTab)
             this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true});
         } else if (event.originalTarget.localName == "box") {
@@ -4489,63 +4544,20 @@
        element (in both cases, when they are anonymous nodes of <tabbrowser>).
   -->
   <binding id="tabbrowser-close-tab-button"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image">
     <handlers>
       <handler event="click" button="0"><![CDATA[
         var bindingParent = document.getBindingParent(this);
         var tabContainer = bindingParent.parentNode;
-        /* The only sequence in which a second click event (i.e. dblclik)
-         * can be dispatched on an in-tab close button is when it is shown
-         * after the first click (i.e. the first click event was dispatched
-         * on the tab). This happens when we show the close button only on
-         * the active tab. (bug 352021)
-         * The only sequence in which a third click event can be dispatched
-         * on an in-tab close button is when the tab was opened with a
-         * double click on the tabbar. (bug 378344)
-         * In both cases, it is most likely that the close button area has
-         * been accidentally clicked, therefore we do not close the tab.
-         *
-         * We don't want to ignore processing of more than one click event,
-         * though, since the user might actually be repeatedly clicking to
-         * close many tabs at once.
-         */
-        if (event.detail > 1 && !this._ignoredClick) {
-          this._ignoredClick = true;
-          return;
-        }
-
-        // Reset the "ignored click" flag
-        this._ignoredClick = false;
-
         tabContainer.tabbrowser.removeTab(bindingParent, {animate: true, byMouse: true});
+        // This enables double-click protection for the tab container
+        // (see tabbrowser-tabs 'click' handler).
         tabContainer._blockDblClick = true;
-
-        /* XXXmano hack (see bug 343628):
-         * Since we're removing the event target, if the user
-         * double-clicks this button, the dblclick event will be dispatched
-         * with the tabbar as its event target (and explicit/originalTarget),
-         * which treats that as a mouse gesture for opening a new tab.
-         * In this context, we're manually blocking the dblclick event
-         * (see dblclick handler).
-         */
-        var clickedOnce = false;
-        function enableDblClick(event) {
-          var target = event.originalTarget;
-          if (target.className == 'tab-close-button')
-            target._ignoredClick = true;
-          if (!clickedOnce) {
-            clickedOnce = true;
-            return;
-          }
-          tabContainer._blockDblClick = false;
-          tabContainer.removeEventListener("click", enableDblClick, true);
-        }
-        tabContainer.addEventListener("click", enableDblClick, true);
       ]]></handler>
 
       <handler event="dblclick" button="0" phase="capturing">
         // for the one-close-button case
         event.stopPropagation();
       </handler>
 
       <handler event="dragstart">
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -254,17 +254,17 @@ DownloadElementShell.prototype = {
           this._metaData = null;
           this._updateDownloadStatusUI();
         }
         if (this._element.selected)
           goUpdateDownloadCommands();
       }.bind(this),
 
       function onFailure(aReason) {
-        if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) {
+        if (aReason instanceof OS.File.Error && aReason.becauseNoSuchFile) {
           this._targetFileInfoFetched = true;
           this._targetFileExists = false;
         }
         else {
           Cu.reportError("Could not fetch info for target file (reason: " +
                          aReason + ")");
         }
 
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -29,16 +29,19 @@ const LAYOUT_CHANGE_TIMER = 250;
  */
 function InspectorPanel(iframeWindow, toolbox) {
   this._toolbox = toolbox;
   this._target = toolbox._target;
   this.panelDoc = iframeWindow.document;
   this.panelWin = iframeWindow;
   this.panelWin.inspector = this;
 
+  this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
+  this._target.on("will-navigate", this._onBeforeNavigate);
+
   EventEmitter.decorate(this);
 }
 
 exports.InspectorPanel = InspectorPanel;
 
 InspectorPanel.prototype = {
   /**
    * open is effectively an asynchronous constructor
@@ -139,16 +142,23 @@ InspectorPanel.prototype = {
     }.bind(this));
 
     this.setupSearchBox();
     this.setupSidebar();
 
     return deferred.promise;
   },
 
+  _onBeforeNavigate: function() {
+    this._defaultNode = null;
+    this.selection.setNodeFront(null);
+    this._destroyMarkup();
+    this.isDirty = false;
+  },
+
   _getWalker: function() {
     let inspector = this.target.inspector;
     return inspector.getWalker().then(walker => {
       this.walker = walker;
       return inspector.getPageStyle();
     }).then(pageStyle => {
       this.pageStyle = pageStyle;
     });
@@ -468,16 +478,18 @@ InspectorPanel.prototype = {
     this.cancelUpdate();
     this.cancelLayoutChange();
 
     if (this.browser) {
       this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
       this.browser = null;
     }
 
+    this.target.off("will-navigate", this._onBeforeNavigate);
+
     this.target.off("thread-paused", this.updateDebuggerPausedWarning);
     this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
     this._toolbox.off("select", this.updateDebuggerPausedWarning);
 
     this._toolbox = null;
 
     this.sidebar.off("select", this._setDefaultSidebar);
     this.sidebar.destroy();
@@ -730,25 +742,28 @@ InspectorPanel.prototype = {
       this.walker.removeNode(this.selection.nodeFront);
     }
   },
 
   /**
    * Schedule a low-priority change event for things like paint
    * and resize.
    */
-  scheduleLayoutChange: function Inspector_scheduleLayoutChange()
+  scheduleLayoutChange: function Inspector_scheduleLayoutChange(event)
   {
-    if (this._timer) {
-      return null;
+    // Filter out non browser window resize events (i.e. triggered by iframes)
+    if (this.browser.contentWindow === event.target) {
+      if (this._timer) {
+        return null;
+      }
+      this._timer = this.panelWin.setTimeout(function() {
+        this.emit("layout-change");
+        this._timer = null;
+      }.bind(this), LAYOUT_CHANGE_TIMER);
     }
-    this._timer = this.panelWin.setTimeout(function() {
-      this.emit("layout-change");
-      this._timer = null;
-    }.bind(this), LAYOUT_CHANGE_TIMER);
   },
 
   /**
    * Cancel a pending low-priority change event if any is
    * scheduled.
    */
   cancelLayoutChange: function Inspector_cancelLayoutChange()
   {
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -39,8 +39,9 @@ support-files = head.js
 [browser_inspector_reload.js]
 [browser_inspector_scrolling.js]
 [browser_inspector_select_last_selected.html]
 [browser_inspector_select_last_selected.js]
 [browser_inspector_select_last_selected2.html]
 [browser_inspector_sidebarstate.js]
 [browser_inspector_bug_848731_reset_selection_on_delete.js]
 [browser_inspector_bug_848731_reset_selection_on_delete.html]
+[browser_inspector_bug_922125_destroy_on_navigate.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_922125_destroy_on_navigate.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Toolbox = devtools.Toolbox;
+let TargetFactory = devtools.TargetFactory;
+
+function test() {
+  waitForExplicitFinish();
+
+  const URL_1 = "data:text/html;charset=UTF-8,<div id='one' style='color:red;'>ONE</div>";
+  const URL_2 = "data:text/html;charset=UTF-8,<div id='two' style='color:green;'>TWO</div>";
+
+  let inspector;
+
+  // open tab, load URL_1, and wait for load to finish
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let browser = gBrowser.getBrowserForTab(tab);
+
+  function onPageOneLoad() {
+    browser.removeEventListener("load", onPageOneLoad, true);
+
+    gDevTools.showToolbox(target).then(aToolbox => {
+      return aToolbox.selectTool("inspector");
+    }).then(i => {
+      inspector = i;
+
+      // Verify we are on page one
+      let testNode = content.document.querySelector("#one");
+      ok(testNode, "We have the test node on page 1");
+
+      assertMarkupViewIsLoaded();
+
+      // Listen to will-navigate to check if the view is empty
+      target.on("will-navigate", () => {
+        info("Navigation to page 2 has started, the inspector should be empty");
+        assertMarkupViewIsEmpty();
+      });
+      inspector.once("markuploaded", () => {
+        info("Navigation to page 2 was done, the inspector should be back up");
+
+        // Verify we are on page one
+        let testNode = content.document.querySelector("#two");
+        ok(testNode, "We have the test node on page 2");
+
+        // On page 2 load, verify we have the right content
+        assertMarkupViewIsLoaded();
+        endTests();
+      });
+
+      // Navigate to page 2
+      browser.loadURI(URL_2);
+    });
+  }
+
+  // Navigate to page 1
+  browser.addEventListener("load", onPageOneLoad, true);
+  browser.loadURI(URL_1);
+
+  function assertMarkupViewIsLoaded() {
+    let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+    is(markupViewBox.childNodes.length, 1, "The markup-view is loaded");
+  }
+
+  function assertMarkupViewIsEmpty() {
+    let markupViewBox = inspector.panelDoc.getElementById("markup-box");
+    is(markupViewBox.childNodes.length, 0, "The markup-view is unloaded");
+  }
+
+  function endTests() {
+    target = browser = tab = inspector = TargetFactory = Toolbox = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/inspector/test/browser_inspector_changes.js
+++ b/browser/devtools/inspector/test/browser_inspector_changes.js
@@ -52,16 +52,20 @@ function test() {
     ok(computedview, "Style Panel has a cssHtmlTree");
 
     let propView = getInspectorProp("font-size");
     is(propView.value, "10px", "Style inspector should be showing the correct font size.");
 
     inspector.once("computed-view-refreshed", stylePanelAfterChange);
 
     testDiv.style.fontSize = "15px";
+
+    // FIXME: This shouldn't be needed but as long as we don't fix the bug
+    // where the rule/computed views are not updated when the selected node's
+    // styles change, it has to stay here
     inspector.emit("layout-change");
   }
 
   function stylePanelAfterChange()
   {
     let propView = getInspectorProp("font-size");
     is(propView.value, "15px", "Style inspector should be showing the new font size.");
 
@@ -72,16 +76,21 @@ function test() {
   {
     // Tests changes made while the style panel is not active.
     inspector.sidebar.select("ruleview");
 
     executeSoon(function() {
       inspector.once("computed-view-refreshed", stylePanelAfterSwitch);
       testDiv.style.fontSize = "20px";
       inspector.sidebar.select("computedview");
+
+      // FIXME: This shouldn't be needed but as long as we don't fix the bug
+      // where the rule/computed views are not updated when the selected node's
+      // styles change, it has to stay here
+      inspector.emit("layout-change");
     });
   }
 
   function stylePanelAfterSwitch()
   {
     let propView = getInspectorProp("font-size");
     is(propView.value, "20px", "Style inspector should be showing the newest font size.");
 
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -70,17 +70,17 @@ function MarkupView(aInspector, aFrame, 
     autoSelect: true,
     theme: "auto"
   };
   this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
 
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
-  this._containers = new WeakMap();
+  this._containers = new Map();
 
   this._boundMutationObserver = this._mutationObserver.bind(this);
   this.walker.on("mutations", this._boundMutationObserver);
 
   this._boundOnNewSelection = this._onNewSelection.bind(this);
   this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
   this._onNewSelection();
 
@@ -378,17 +378,16 @@ MarkupView.prototype = {
     this._containers.set(aNode, container);
     container.childrenDirty = true;
 
     this._updateChildren(container);
 
     return container;
   },
 
-
   /**
    * Mutation observer used for included nodes.
    */
   _mutationObserver: function(aMutations) {
     for (let mutation of aMutations) {
       let type = mutation.type;
       let target = mutation.target;
 
@@ -800,16 +799,19 @@ MarkupView.prototype = {
     this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
     delete this._boundOnNewSelection;
 
     this.walker.off("mutations", this._boundMutationObserver)
     delete this._boundMutationObserver;
 
     delete this._elt;
 
+    for ([key, container] of this._containers) {
+      container.destroy();
+    }
     delete this._containers;
   },
 
   /**
    * Initialize the preview panel.
    */
   _initPreview: function() {
     if (!Services.prefs.getBoolPref("devtools.inspector.markupPreview")) {
@@ -940,17 +942,18 @@ function MarkupContainer(aMarkupView, aN
   this.elt.addEventListener("mouseover", this._onMouseOver, false);
 
   this._onMouseOut = this._onMouseOut.bind(this);
   this.elt.addEventListener("mouseout", this._onMouseOut, false);
 
   // Appending the editor element and attaching event listeners
   this.tagLine.appendChild(this.editor.elt);
 
-  this.elt.addEventListener("mousedown", this._onMouseDown.bind(this), false);
+  this._onMouseDown = this._onMouseDown.bind(this);
+  this.elt.addEventListener("mousedown", this._onMouseDown, false);
 }
 
 MarkupContainer.prototype = {
   toString: function() {
     return "[MarkupContainer for " + this.node + "]";
   },
 
   /**
@@ -1159,16 +1162,39 @@ MarkupContainer.prototype = {
   /**
    * Try to put keyboard focus on the current editor.
    */
   focus: function() {
     let focusable = this.editor.elt.querySelector("[tabindex]");
     if (focusable) {
       focusable.focus();
     }
+  },
+
+  /**
+   * Get rid of event listeners and references, when the container is no longer
+   * needed
+   */
+  destroy: function() {
+    // Recursively destroy children containers
+    let firstChild;
+    while (firstChild = this.children.firstChild) {
+      firstChild.container.destroy();
+      this.children.removeChild(firstChild);
+    }
+
+    // Remove event listeners
+    this.elt.removeEventListener("dblclick", this._onToggle, false);
+    this.elt.removeEventListener("mouseover", this._onMouseOver, false);
+    this.elt.removeEventListener("mouseout", this._onMouseOut, false);
+    this.elt.removeEventListener("mousedown", this._onMouseDown, false);
+    this.expander.removeEventListener("click", this._onToggle, false);
+
+    // Destroy my editor
+    this.editor.destroy();
   }
 };
 
 
 /**
  * Dummy container node used for the root document element.
  */
 function RootContainer(aMarkupView, aNode) {
@@ -1178,43 +1204,52 @@ function RootContainer(aMarkupView, aNod
   this.children = this.elt;
   this.node = aNode;
   this.toString = function() { return "[root container]"}
 }
 
 RootContainer.prototype = {
   hasChildren: true,
   expanded: true,
-  update: function() {}
+  update: function() {},
+  destroy: function() {}
 };
 
 /**
  * Creates an editor for simple nodes.
  */
 function GenericEditor(aContainer, aNode) {
   this.elt = aContainer.doc.createElement("span");
   this.elt.className = "editor";
   this.elt.textContent = aNode.nodeName;
 }
 
+GenericEditor.prototype = {
+  destroy: function() {}
+};
+
 /**
  * Creates an editor for a DOCTYPE node.
  *
  * @param MarkupContainer aContainer The container owning this editor.
  * @param DOMNode aNode The node being edited.
  */
 function DoctypeEditor(aContainer, aNode) {
   this.elt = aContainer.doc.createElement("span");
   this.elt.className = "editor comment";
   this.elt.textContent = '<!DOCTYPE ' + aNode.name +
      (aNode.publicId ? ' PUBLIC "' +  aNode.publicId + '"': '') +
      (aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
      '>';
 }
 
+DoctypeEditor.prototype = {
+  destroy: function() {}
+};
+
 /**
  * Creates a simple text editor node, used for TEXT and COMMENT
  * nodes.
  *
  * @param MarkupContainer aContainer The container owning this editor.
  * @param DOMNode aNode The node being edited.
  * @param string aTemplate The template id to use to build the editor.
  */
@@ -1279,17 +1314,19 @@ TextEditor.prototype = {
         return longstr.string();
       }).then(str => {
         longstr.release().then(null, console.error);
         if (this.selected) {
           this.value.textContent = str;
         }
       }).then(null, console.error);
     }
-  }
+  },
+
+  destroy: function() {}
 };
 
 /**
  * Creates an editor for an Element node.
  *
  * @param MarkupContainer aContainer The container owning this editor.
  * @param Element aNode The node being edited.
  */
@@ -1586,17 +1623,19 @@ ElementEditor.prototype = {
       }, () => {
         swapNodes(newElt, this.rawNode);
         this.markup.setNodeExpanded(this.node, newContainer.expanded);
         if (newContainer.selected) {
           this.markup.navigate(this.container);
         }
       });
     }).then(null, console.error);
-  }
+  },
+
+  destroy: function() {}
 };
 
 function nodeDocument(node) {
   return node.ownerDocument ||
     (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
 }
 
 function truncateString(str, maxLength) {
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -10,17 +10,17 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
 var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 let Telemetry = require("devtools/shared/telemetry");
-let {TouchEventHandler} = require("devtools/shared/touch-events");
+let {TouchEventHandler} = require("devtools/touch-events");
 
 this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
 
 const MIN_WIDTH = 50;
 const MIN_HEIGHT = 50;
 
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
deleted file mode 100644
--- a/browser/devtools/shared/touch-events.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-let {CC, Cc, Ci, Cu, Cr} = require('chrome');
-
-Cu.import('resource://gre/modules/Services.jsm');
-
-let handlerCount = 0;
-
-let orig_w3c_touch_events = Services.prefs.getIntPref('dom.w3c_touch_events.enabled');
-
-// =================== Touch ====================
-// Simulate touch events on desktop
-function TouchEventHandler (window) {
-  let contextMenuTimeout = 0;
-
-  // This guard is used to not re-enter the events processing loop for
-  // self dispatched events
-  let ignoreEvents = false;
-
-  let threshold = 25;
-  try {
-    threshold = Services.prefs.getIntPref('ui.dragThresholdX');
-  } catch(e) {}
-
-  let delay = 500;
-  try {
-    delay = Services.prefs.getIntPref('ui.click_hold_context_menus.delay');
-  } catch(e) {}
-
-  let TouchEventHandler = {
-    enabled: false,
-    events: ['mousedown', 'mousemove', 'mouseup', 'click'],
-    start: function teh_start() {
-      let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1;
-      handlerCount++;
-      Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1);
-      this.enabled = true;
-      this.events.forEach((function(evt) {
-        window.addEventListener(evt, this, true);
-      }).bind(this));
-      return isReloadNeeded;
-    },
-    stop: function teh_stop() {
-      handlerCount--;
-      if (handlerCount == 0)
-        Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events);
-      this.enabled = false;
-      this.events.forEach((function(evt) {
-        window.removeEventListener(evt, this, true);
-      }).bind(this));
-    },
-    handleEvent: function teh_handleEvent(evt) {
-      if (evt.button || ignoreEvents ||
-          evt.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN)
-        return;
-
-      // The gaia system window use an hybrid system even on the device which is
-      // a mix of mouse/touch events. So let's not cancel *all* mouse events
-      // if it is the current target.
-      let content = evt.target.ownerDocument.defaultView;
-      let isSystemWindow = content.location.toString().indexOf("system.gaiamobile.org") != -1;
-
-      let eventTarget = this.target;
-      let type = '';
-      switch (evt.type) {
-        case 'mousedown':
-          this.target = evt.target;
-
-          contextMenuTimeout =
-            this.sendContextMenu(evt.target, evt.pageX, evt.pageY, delay);
-
-          this.cancelClick = false;
-          this.startX = evt.pageX;
-          this.startY = evt.pageY;
-
-          // Capture events so if a different window show up the events
-          // won't be dispatched to something else.
-          evt.target.setCapture(false);
-
-          type = 'touchstart';
-          break;
-
-        case 'mousemove':
-          if (!eventTarget)
-            return;
-
-          if (!this.cancelClick) {
-            if (Math.abs(this.startX - evt.pageX) > threshold ||
-                Math.abs(this.startY - evt.pageY) > threshold) {
-              this.cancelClick = true;
-              content.clearTimeout(contextMenuTimeout);
-            }
-          }
-
-          type = 'touchmove';
-          break;
-
-        case 'mouseup':
-          if (!eventTarget)
-            return;
-          this.target = null;
-
-          content.clearTimeout(contextMenuTimeout);
-          type = 'touchend';
-          break;
-
-        case 'click':
-          // Mouse events has been cancelled so dispatch a sequence
-          // of events to where touchend has been fired
-          evt.preventDefault();
-          evt.stopImmediatePropagation();
-
-          if (this.cancelClick)
-            return;
-
-          ignoreEvents = true;
-          content.setTimeout(function dispatchMouseEvents(self) {
-            self.fireMouseEvent('mousedown', evt);
-            self.fireMouseEvent('mousemove', evt);
-            self.fireMouseEvent('mouseup', evt);
-            ignoreEvents = false;
-         }, 0, this);
-
-          return;
-      }
-
-      let target = eventTarget || this.target;
-      if (target && type) {
-        this.sendTouchEvent(evt, target, type);
-      }
-
-      if (!isSystemWindow) {
-        evt.preventDefault();
-        evt.stopImmediatePropagation();
-      }
-    },
-    fireMouseEvent: function teh_fireMouseEvent(type, evt)  {
-      let content = evt.target.ownerDocument.defaultView;
-      var utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-      utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true);
-    },
-    sendContextMenu: function teh_sendContextMenu(target, x, y, delay) {
-      let doc = target.ownerDocument;
-      let evt = doc.createEvent('MouseEvent');
-      evt.initMouseEvent('contextmenu', true, true, doc.defaultView,
-                         0, x, y, x, y, false, false, false, false,
-                         0, null);
-
-      let content = target.ownerDocument.defaultView;
-      let timeout = content.setTimeout((function contextMenu() {
-        target.dispatchEvent(evt);
-        this.cancelClick = true;
-      }).bind(this), delay);
-
-      return timeout;
-    },
-    sendTouchEvent: function teh_sendTouchEvent(evt, target, name) {
-      let document = target.ownerDocument;
-      let content = document.defaultView;
-
-      let touchEvent = document.createEvent('touchevent');
-      let point = document.createTouch(content, target, 0,
-                                       evt.pageX, evt.pageY,
-                                       evt.screenX, evt.screenY,
-                                       evt.clientX, evt.clientY,
-                                       1, 1, 0, 0);
-      let touches = document.createTouchList(point);
-      let targetTouches = touches;
-      let changedTouches = touches;
-      touchEvent.initTouchEvent(name, true, true, content, 0,
-                                false, false, false, false,
-                                touches, targetTouches, changedTouches);
-      target.dispatchEvent(touchEvent);
-      return touchEvent;
-    }
-  };
-
-  return TouchEventHandler;
-}
-
-exports.TouchEventHandler = TouchEventHandler;
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -265,29 +265,36 @@ CssHtmlTree.prototype = {
    * Update the highlighted element. The CssHtmlTree panel will show the style
    * information for the given element.
    * @param {nsIDOMElement} aElement The highlighted node to get styles for.
    *
    * @returns a promise that will be resolved when highlighting is complete.
    */
   highlight: function(aElement) {
     if (!aElement) {
+      this.viewedElement = null;
+      this.noResults.hidden = false;
+
       if (this._refreshProcess) {
         this._refreshProcess.cancel();
       }
+      // Hiding all properties
+      for (let propView of this.propertyViews) {
+        propView.refresh();
+      }
       return promise.resolve(undefined);
     }
 
     if (aElement === this.viewedElement) {
       return promise.resolve(undefined);
     }
 
     this.viewedElement = aElement;
+    this.refreshSourceFilter();
 
-    this.refreshSourceFilter();
     return this.refreshPanel();
   },
 
   _createPropertyViews: function()
   {
     if (this._createViewsPromise) {
       return this._createViewsPromise;
     }
@@ -326,16 +333,20 @@ CssHtmlTree.prototype = {
     return deferred.promise;
   },
 
   /**
    * Refresh the panel content.
    */
   refreshPanel: function CssHtmlTree_refreshPanel()
   {
+    if (!this.viewedElement) {
+      return promise.resolve();
+    }
+
     return promise.all([
       this._createPropertyViews(),
       this.pageStyle.getComputed(this.viewedElement, {
         filter: this._sourceFilter,
         onlyMatched: !this.includeBrowserStyles,
         markMatched: true
       })
     ]).then(([createViews, computed]) => {
@@ -354,18 +365,16 @@ CssHtmlTree.prototype = {
       this.noResults.hidden = true;
 
       // Reset visible property count
       this.numVisibleProperties = 0;
 
       // Reset zebra striping.
       this._darkStripe = true;
 
-      let display = this.propertyContainer.style.display;
-
       let deferred = promise.defer();
       this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
         onItem: (aPropView) => {
           aPropView.refresh();
         },
         onCancel: () => {
           deferred.reject("refresh cancelled");
         },
@@ -642,22 +651,26 @@ CssHtmlTree.prototype = {
     // Nodes used in templating
     delete this.root;
     delete this.propertyContainer;
     delete this.panel;
 
     // The document in which we display the results (csshtmltree.xul).
     delete this.styleDocument;
 
+    for (let propView of this.propertyViews)  {
+      propView.destroy();
+    }
+
     // The element that we're inspecting, and the document that it comes from.
     delete this.propertyViews;
     delete this.styleWindow;
     delete this.styleDocument;
     delete this.styleInspector;
-  },
+  }
 };
 
 function PropertyInfo(aTree, aName) {
   this.tree = aTree;
   this.name = aName;
 }
 PropertyInfo.prototype = {
   get value() {
@@ -756,16 +769,20 @@ PropertyView.prototype = {
     return this.tree.matchedProperties.has(this.name);
   },
 
   /**
    * Should this property be visible?
    */
   get visible()
   {
+    if (!this.tree.viewedElement) {
+      return false;
+    }
+
     if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
       return false;
     }
 
     let searchTerm = this.tree.searchField.value.toLowerCase();
     if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
       this.value.toLowerCase().indexOf(searchTerm) == -1) {
       return false;
@@ -804,63 +821,65 @@ PropertyView.prototype = {
   /**
    * Build the markup for on computed style
    * @return Element
    */
   buildMain: function PropertyView_buildMain()
   {
     let doc = this.tree.styleDocument;
 
+    this.onMatchedToggle = this.onMatchedToggle.bind(this);
+
     // Build the container element
     this.element = doc.createElementNS(HTML_NS, "div");
     this.element.setAttribute("class", this.propertyHeaderClassName);
-    this.element.addEventListener("dblclick",
-      this.onMatchedToggle.bind(this), false);
+    this.element.addEventListener("dblclick", this.onMatchedToggle, false);
 
     // Make it keyboard navigable
     this.element.setAttribute("tabindex", "0");
-    this.element.addEventListener("keydown", (aEvent) => {
+    this.onKeyDown = (aEvent) => {
       let keyEvent = Ci.nsIDOMKeyEvent;
       if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
         this.mdnLinkClick();
       }
       if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
         aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
         this.onMatchedToggle(aEvent);
       }
-    }, false);
+    };
+    this.element.addEventListener("keydown", this.onKeyDown, false);
 
     // Build the twisty expand/collapse
     this.matchedExpander = doc.createElementNS(HTML_NS, "div");
     this.matchedExpander.className = "expander theme-twisty";
-    this.matchedExpander.addEventListener("click",
-      this.onMatchedToggle.bind(this), false);
+    this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
     this.element.appendChild(this.matchedExpander);
 
     // Build the style name element
     this.nameNode = doc.createElementNS(HTML_NS, "div");
     this.nameNode.setAttribute("class", "property-name theme-fg-color5");
     // Reset its tabindex attribute otherwise, if an ellipsis is applied
     // it will be reachable via TABing
     this.nameNode.setAttribute("tabindex", "");
     this.nameNode.textContent = this.nameNode.title = this.name;
     // Make it hand over the focus to the container
-    this.nameNode.addEventListener("click", () => this.element.focus(), false);
+    this.onFocus = () => this.element.focus();
+    this.nameNode.addEventListener("click", this.onFocus, false);
     this.element.appendChild(this.nameNode);
 
     // Build the style value element
     this.valueNode = doc.createElementNS(HTML_NS, "div");
     this.valueNode.setAttribute("class", "property-value theme-fg-color1");
     // Reset its tabindex attribute otherwise, if an ellipsis is applied
     // it will be reachable via TABing
     this.valueNode.setAttribute("tabindex", "");
     this.valueNode.setAttribute("dir", "ltr");
     this.valueNode.textContent = this.valueNode.title = this.value;
     // Make it hand over the focus to the container
-    this.valueNode.addEventListener("click", () => this.element.focus(), false);
+    this.valueNode.addEventListener("click", this.onFocus, false);
     this.element.appendChild(this.valueNode);
 
     return this.element;
   },
 
   buildSelectorContainer: function PropertyView_buildSelectorContainer()
   {
     let doc = this.tree.styleDocument;
@@ -976,16 +995,34 @@ PropertyView.prototype = {
     let inspector = this.tree.styleInspector.inspector;
 
     if (inspector.target.tab) {
       let browserWin = inspector.target.tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(this.link, "tab");
     }
     aEvent.preventDefault();
   },
+
+  /**
+   * Destroy this property view, removing event listeners
+   */
+  destroy: function PropertyView_destroy() {
+    this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
+    this.element.removeEventListener("keydown", this.onKeyDown, false);
+    this.element = null;
+
+    this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
+    this.matchedExpander = null;
+
+    this.nameNode.removeEventListener("click", this.onFocus, false);
+    this.nameNode = null;
+
+    this.valueNode.removeEventListener("click", this.onFocus, false);
+    this.valueNode = null;
+  }
 };
 
 /**
  * A container to view us easy access to display data from a CssRule
  * @param CssHtmlTree aTree, the owning CssHtmlTree
  * @param aSelectorInfo
  */
 function SelectorView(aTree, aSelectorInfo)
--- a/browser/devtools/styleinspector/style-inspector.js
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -71,34 +71,28 @@ function RuleViewTool(aInspector, aWindo
                                      this._cssLinkHandler);
 
   this._onSelect = this.onSelect.bind(this);
   this.inspector.selection.on("detached", this._onSelect);
   this.inspector.selection.on("new-node-front", this._onSelect);
   this.refresh = this.refresh.bind(this);
   this.inspector.on("layout-change", this.refresh);
 
-  this.panelSelected = this.panelSelected.bind(this);
-  this.inspector.sidebar.on("ruleview-selected", this.panelSelected);
   this.inspector.selection.on("pseudoclass", this.refresh);
   if (this.inspector.highlighter) {
     this.inspector.highlighter.on("locked", this._onSelect);
   }
 
   this.onSelect();
 }
 
 exports.RuleViewTool = RuleViewTool;
 
 RuleViewTool.prototype = {
   onSelect: function RVT_onSelect(aEvent) {
-    if (!this.isActive()) {
-      // We'll update when the panel is selected.
-      return;
-    }
     this.view.setPageStyle(this.inspector.pageStyle);
 
     if (!this.inspector.selection.isConnected() ||
         !this.inspector.selection.isElementNode()) {
       this.view.highlight(null);
       return;
     }
 
@@ -112,37 +106,22 @@ RuleViewTool.prototype = {
     }
 
     if (aEvent == "locked") {
       let done = this.inspector.updating("rule-view");
       this.view.highlight(this.inspector.selection.nodeFront).then(done, done);
     }
   },
 
-  isActive: function RVT_isActive() {
-    return this.inspector.sidebar.getCurrentTabID() == "ruleview";
-  },
-
   refresh: function RVT_refresh() {
-    if (this.isActive()) {
-      this.view.nodeChanged();
-    }
-  },
-
-  panelSelected: function() {
-    if (this.inspector.selection.nodeFront === this.view.viewedElement) {
-      this.view.nodeChanged();
-    } else {
-      this.onSelect();
-    }
+    this.view.nodeChanged();
   },
 
   destroy: function RVT_destroy() {
     this.inspector.off("layout-change", this.refresh);
-    this.inspector.sidebar.off("ruleview-selected", this.refresh);
     this.inspector.selection.off("pseudoclass", this.refresh);
     this.inspector.selection.off("new-node-front", this._onSelect);
     if (this.inspector.highlighter) {
       this.inspector.highlighter.off("locked", this._onSelect);
     }
 
     this.view.element.removeEventListener("CssRuleViewCSSLinkClicked",
       this._cssLinkHandler);
@@ -176,34 +155,27 @@ function ComputedViewTool(aInspector, aW
   this.inspector.selection.on("detached", this._onSelect);
   this.inspector.selection.on("new-node-front", this._onSelect);
   if (this.inspector.highlighter) {
     this.inspector.highlighter.on("locked", this._onSelect);
   }
   this.refresh = this.refresh.bind(this);
   this.inspector.on("layout-change", this.refresh);
   this.inspector.selection.on("pseudoclass", this.refresh);
-  this.panelSelected = this.panelSelected.bind(this);
-  this.inspector.sidebar.on("computedview-selected", this.panelSelected);
 
   this.view.highlight(null);
 
   this.onSelect();
 }
 
 exports.ComputedViewTool = ComputedViewTool;
 
 ComputedViewTool.prototype = {
   onSelect: function CVT_onSelect(aEvent)
   {
-    if (!this.isActive()) {
-      // We'll try again when we're selected.
-      return;
-    }
-
     this.view.setPageStyle(this.inspector.pageStyle);
 
     if (!this.inspector.selection.isConnected() ||
         !this.inspector.selection.isElementNode()) {
       this.view.highlight(null);
       return;
     }
 
@@ -221,32 +193,18 @@ ComputedViewTool.prototype = {
     if (aEvent == "locked" && this.inspector.selection.nodeFront != this.view.viewedElement) {
       let done = this.inspector.updating("computed-view");
       this.view.highlight(this.inspector.selection.nodeFront).then(() => {
         done();
       });
     }
   },
 
-  isActive: function CVT_isActive() {
-    return this.inspector.sidebar.getCurrentTabID() == "computedview";
-  },
-
   refresh: function CVT_refresh() {
-    if (this.isActive()) {
-      this.view.refreshPanel();
-    }
-  },
-
-  panelSelected: function() {
-    if (this.inspector.selection.nodeFront === this.view.viewedElement) {
-      this.view.refreshPanel();
-    } else {
-      this.onSelect();
-    }
+    this.view.refreshPanel();
   },
 
   destroy: function CVT_destroy(aContext)
   {
     this.inspector.off("layout-change", this.refresh);
     this.inspector.sidebar.off("computedview-selected", this.refresh);
     this.inspector.selection.off("pseudoclass", this.refresh);
     this.inspector.selection.off("new-node-front", this._onSelect);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -98,17 +98,18 @@ function performWebConsoleTests(hud)
   function onNodeUpdate(node)
   {
     isnot(node.textContent.indexOf("bug653531"), -1,
           "correct output for $0.textContent");
     let inspector = gDevTools.getToolbox(target).getPanel("inspector");
     is(inspector.selection.node.textContent, "bug653531",
        "node successfully updated");
 
-    executeSoon(finishTest);
+    gBrowser.removeCurrentTab();
+    finishTest();
   }
 }
 
 function test()
 {
   waitForExplicitFinish();
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -781,16 +781,17 @@ bin/libfreebl_32int64_3.so
 @BINPATH@/webapprt/components/DirectoryProvider.js
 @BINPATH@/webapprt/components/PaymentUIGlue.js
 @BINPATH@/webapprt/components/components.manifest
 @BINPATH@/webapprt/defaults/preferences/prefs.js
 @BINPATH@/webapprt/modules/Startup.jsm
 @BINPATH@/webapprt/modules/WebappRT.jsm
 @BINPATH@/webapprt/modules/WebappsHandler.jsm
 @BINPATH@/webapprt/modules/RemoteDebugger.jsm
+@BINPATH@/webapprt/modules/WebRTCHandler.jsm
 #endif
 
 #ifdef MOZ_METRO
 @BINPATH@/components/MetroUIUtils.js
 @BINPATH@/components/MetroUIUtils.manifest
 [metro]
 ; gre resources
 @BINPATH@/CommandExecuteHandler@BIN_SUFFIX@
--- a/dom/interfaces/notification/nsIDOMDesktopNotification.idl
+++ b/dom/interfaces/notification/nsIDOMDesktopNotification.idl
@@ -2,19 +2,19 @@
  * 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/. */
 
 #include "domstubs.idl"
 
 interface nsIObserver;
 
 // Notification service that also provides the manifest URL
-[scriptable, uuid(61c4adf4-187d-4d18-937c-4df17bc01073)]
+[scriptable, uuid(50cb17d2-dc8a-4aa6-bcd3-94d76af14e20)]
 interface nsIAppNotificationService : nsISupports
 {
     void showAppNotification(in AString  imageUrl,
                              in AString  title,
                              in AString  text,
-                             [optional] in boolean textClickable,
-                             [optional] in AString manifestURL,
-                             [optional] in nsIObserver alertListener,
-                             [optional] in AString id);
+                             in nsIObserver alertListener,
+                             // details should be a WebIDL
+                             // AppNotificationServiceOptions Dictionary object
+                             in jsval    details);
 };
--- a/dom/src/notification/DesktopNotification.cpp
+++ b/dom/src/notification/DesktopNotification.cpp
@@ -1,13 +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/. */
 #include "mozilla/dom/DesktopNotification.h"
 #include "mozilla/dom/DesktopNotificationBinding.h"
+#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
 #include "nsContentPermissionHelper.h"
 #include "nsXULAppAPI.h"
 #include "mozilla/dom/PBrowserChild.h"
 #include "nsIDOMDesktopNotification.h"
 #include "TabChild.h"
 #include "mozilla/Preferences.h"
 #include "nsGlobalWindow.h"
 #include "nsIAppsService.h"
@@ -86,21 +87,28 @@ DesktopNotification::PostDesktopNotifica
   if (appNotifier) {
     nsCOMPtr<nsPIDOMWindow> window = GetOwner();
     uint32_t appId = (window.get())->GetDoc()->NodePrincipal()->GetAppId();
 
     if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) {
       nsCOMPtr<nsIAppsService> appsService = do_GetService("@mozilla.org/AppsService;1");
       nsString manifestUrl = EmptyString();
       appsService->GetManifestURLByLocalId(appId, manifestUrl);
+      mozilla::AutoSafeJSContext cx;
+      JS::RootedValue val(cx);
+      AppNotificationServiceOptions ops;
+      ops.mTextClickable = true;
+      ops.mManifestURL = manifestUrl;
+
+      if (!ops.ToObject(cx, JS::NullPtr(), &val)) {
+        return NS_ERROR_FAILURE;
+      }
+
       return appNotifier->ShowAppNotification(mIconURL, mTitle, mDescription,
-                                              true,
-                                              manifestUrl,
-                                              mObserver,
-                                              EmptyString());
+                                              mObserver, val);
     }
   }
 #endif
 
   nsCOMPtr<nsIAlertsService> alerts = do_GetService("@mozilla.org/alerts-service;1");
   if (!alerts) {
     return NS_ERROR_NOT_IMPLEMENTED;
   }
--- a/dom/src/notification/Notification.cpp
+++ b/dom/src/notification/Notification.cpp
@@ -1,14 +1,15 @@
 /* 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/. */
 
 #include "PCOMContentPermissionRequestChild.h"
 #include "mozilla/dom/Notification.h"
+#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
 #include "mozilla/dom/OwningNonNull.h"
 #include "mozilla/Preferences.h"
 #include "TabChild.h"
 #include "nsContentUtils.h"
 #include "nsDOMEvent.h"
 #include "nsIAlertsService.h"
 #include "nsIContentPermissionPrompt.h"
 #include "nsIDocument.h"
@@ -370,21 +371,32 @@ Notification::ShowInternal()
   if (appNotifier) {
     nsCOMPtr<nsPIDOMWindow> window = GetOwner();
     uint32_t appId = (window.get())->GetDoc()->NodePrincipal()->GetAppId();
 
     if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) {
       nsCOMPtr<nsIAppsService> appsService = do_GetService("@mozilla.org/AppsService;1");
       nsString manifestUrl = EmptyString();
       appsService->GetManifestURLByLocalId(appId, manifestUrl);
+      mozilla::AutoSafeJSContext cx;
+      JS::RootedValue val(cx);
+      AppNotificationServiceOptions ops;
+      ops.mTextClickable = true;
+      ops.mManifestURL = manifestUrl;
+      ops.mId = alertName;
+      ops.mDir = DirectionToString(mDir);
+      ops.mLang = mLang;
+
+      if (!ops.ToObject(cx, JS::NullPtr(), &val)) {
+        NS_WARNING("Converting dict to object failed!");
+        return NS_ERROR_FAILURE;
+      }
+
       return appNotifier->ShowAppNotification(mIconUrl, mTitle, mBody,
-                                              true,
-                                              manifestUrl,
-                                              observer,
-                                              alertName);
+                                              observer, val);
     }
   }
 #endif
 
   // In the case of IPC, the parent process uses the cookie to map to
   // nsIObserver. Thus the cookie must be unique to differentiate observers.
   nsString uniqueCookie = NS_LITERAL_STRING("notification:");
   uniqueCookie.AppendInt(sCount++);
--- a/dom/tests/mochitest/notification/notification_common.js
+++ b/dom/tests/mochitest/notification/notification_common.js
@@ -4,30 +4,30 @@ const ALERTS_SERVICE_CONTRACT_ID = "@moz
 const MOCK_SYSTEM_ALERTS_CID = SpecialPowers.wrap(SpecialPowers.Components).ID("{e86d888c-e41b-4b78-9104-2f2742a532de}");
 const SYSTEM_ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/system-alerts-service;1";
 
 var registrar = SpecialPowers.wrap(SpecialPowers.Components).manager.
   QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar);
 
 var mockAlertsService = {
   showAlertNotification: function(imageUrl, title, text, textClickable,
-                                  cookie, alertListener, name) {
+                                  cookie, alertListener, name, bidi, lang) {
     // probably should do this async....
     SpecialPowers.wrap(alertListener).observe(null, "alertshow", cookie);
 
     if (SpecialPowers.getBoolPref("notification.prompt.testing.click_on_notification") == true) {
        SpecialPowers.wrap(alertListener).observe(null, "alertclickcallback", cookie);
     }
 
     SpecialPowers.wrap(alertListener).observe(null, "alertfinished", cookie);
   },
 
-  showAppNotification: function(imageUrl, title, text, textClickable,
-                                manifestURL, alertListener) {
-    this.showAlertNotification(imageUrl, title, text, textClickable, "", alertListener, "");
+  showAppNotification: function(imageUrl, title, text, alertListener, details) {
+    this.showAlertNotification(imageUrl, title, text, details.textClickable, "",
+                               alertListener, details.name, details.dir, details.lang);
   },
 
   QueryInterface: function(aIID) {
     if (SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsISupports) ||
         SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsIAlertsService) ||
         SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsIAppNotificationService)) {
       return this;
     }
new file mode 100644
--- /dev/null
+++ b/dom/webidl/AppNotificationServiceOptions.webidl
@@ -0,0 +1,15 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+interface MozObserver;
+
+dictionary AppNotificationServiceOptions {
+  boolean textClickable = false;
+  DOMString manifestURL = "";
+  DOMString id = "";
+  DOMString dir = "";
+  DOMString lang = "";
+};
--- a/dom/webidl/DummyBinding.webidl
+++ b/dom/webidl/DummyBinding.webidl
@@ -22,13 +22,14 @@ interface DummyInterface : EventTarget {
   void frameRequestCallback(FrameRequestCallback arg);
   void MmsParameters(optional MmsParameters arg);
   void MmsAttachment(optional MmsAttachment arg);
   void AsyncScrollEventDetail(optional AsyncScrollEventDetail arg);
   void OpenWindowEventDetail(optional OpenWindowEventDetail arg);
   void DOMWindowResizeEventDetail(optional DOMWindowResizeEventDetail arg);
   void WifiOptions(optional WifiCommandOptions arg1,
                    optional WifiResultOptions arg2);
+  void AppNotificationServiceOptions(optional AppNotificationServiceOptions arg);
 };
 
 interface DummyInterfaceWorkers {
   BlobPropertyBag blobBag();
 };
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -14,16 +14,17 @@ PREPROCESSED_WEBIDL_FILES = [
     'Crypto.webidl',
     'Navigator.webidl',
 ]
 
 WEBIDL_FILES = [
     'AbstractWorker.webidl',
     'AnalyserNode.webidl',
     'AnimationEvent.webidl',
+    'AppNotificationServiceOptions.webidl',
     'ArchiveReader.webidl',
     'ArchiveRequest.webidl',
     'Attr.webidl',
     'AudioBuffer.webidl',
     'AudioBufferSourceNode.webidl',
     'AudioContext.webidl',
     'AudioDestinationNode.webidl',
     'AudioListener.webidl',
new file mode 100644
--- /dev/null
+++ b/dom/wifi/WifiCommand.jsm
@@ -0,0 +1,425 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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 = ["WifiCommand"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/systemlibs.js");
+
+const SUPP_PROP = "init.svc.wpa_supplicant";
+const WPA_SUPPLICANT = "wpa_supplicant";
+
+this.WifiCommand = function(controlMessage) {
+  var command = {};
+
+  //-------------------------------------------------
+  // General commands.
+  //-------------------------------------------------
+
+  command.loadDriver = function (callback) {
+    voidControlMessage("load_driver", function(status) {
+      callback(status);
+    });
+  };
+
+  command.unloadDriver = function (callback) {
+    voidControlMessage("unload_driver", function(status) {
+      callback(status);
+    });
+  };
+
+  command.startSupplicant = function (callback) {
+    voidControlMessage("start_supplicant", callback);
+  };
+
+  command.killSupplicant = function (callback) {
+    // It is interesting to note that this function does exactly what
+    // wifi_stop_supplicant does. Unforunately, on the Galaxy S2, Samsung
+    // changed that function in a way that means that it doesn't recognize
+    // wpa_supplicant as already running. Therefore, we have to roll our own
+    // version here.
+    stopProcess(SUPP_PROP, WPA_SUPPLICANT, callback);
+  };
+
+  command.terminateSupplicant = function (callback) {
+    doBooleanCommand("TERMINATE", "OK", callback);
+  };
+
+  command.stopSupplicant = function (callback) {
+    voidControlMessage("stop_supplicant", callback);
+  };
+
+  command.listNetworks = function (callback) {
+    doStringCommand("LIST_NETWORKS", callback);
+  };
+
+  command.addNetwork = function (callback) {
+    doIntCommand("ADD_NETWORK", callback);
+  };
+
+  command.setNetworkVariable = function (netId, name, value, callback) {
+    doBooleanCommand("SET_NETWORK " + netId + " " + name + " " +
+                      value, "OK", callback);
+  };
+
+  command.getNetworkVariable = function (netId, name, callback) {
+    doStringCommand("GET_NETWORK " + netId + " " + name, callback);
+  };
+
+  command.removeNetwork = function (netId, callback) {
+    doBooleanCommand("REMOVE_NETWORK " + netId, "OK", callback);
+  };
+
+  command.enableNetwork = function (netId, disableOthers, callback) {
+    doBooleanCommand((disableOthers ? "SELECT_NETWORK " : "ENABLE_NETWORK ") +
+                     netId, "OK", callback);
+  };
+
+  command.disableNetwork = function (netId, callback) {
+    doBooleanCommand("DISABLE_NETWORK " + netId, "OK", callback);
+  };
+
+  command.status = function (callback) {
+    doStringCommand("STATUS", callback);
+  };
+
+  command.ping = function (callback) {
+    doBooleanCommand("PING", "PONG", callback);
+  };
+
+  command.scanResults = function (callback) {
+    doStringCommand("SCAN_RESULTS", callback);
+  };
+
+  command.disconnect = function (callback) {
+    doBooleanCommand("DISCONNECT", "OK", callback);
+  };
+
+  command.reconnect = function (callback) {
+    doBooleanCommand("RECONNECT", "OK", callback);
+  };
+
+  command.reassociate = function (callback) {
+    doBooleanCommand("REASSOCIATE", "OK", callback);
+  };
+
+  command.setBackgroundScan = function (enable, callback) {
+    doBooleanCommand("SET pno " + (enable ? "1" : "0"),
+                     "OK",
+                     function(ok) {
+                       callback(true, ok);
+                     });
+  };
+
+  command.doSetScanMode = function (setActive, callback) {
+    doBooleanCommand(setActive ?
+                     "DRIVER SCAN-ACTIVE" :
+                     "DRIVER SCAN-PASSIVE", "OK", callback);
+  };
+
+  command.scan = function (callback) {
+    doBooleanCommand("SCAN", "OK", callback);
+  };
+
+  command.setLogLevel = function (level, callback) {
+    doBooleanCommand("LOG_LEVEL " + level, "OK", callback);
+  };
+
+  command.getLogLevel = function (callback) {
+    doStringCommand("LOG_LEVEL", callback);
+  };
+
+  command.wpsPbc = function (callback) {
+    doBooleanCommand("WPS_PBC", "OK", callback);
+  };
+
+  command.wpsPin = function (detail, callback) {
+    doStringCommand("WPS_PIN " +
+                    (detail.bssid === undefined ? "any" : detail.bssid) +
+                    (detail.pin === undefined ? "" : (" " + detail.pin)),
+                    callback);
+  };
+
+  command.wpsCancel = function (callback) {
+    doBooleanCommand("WPS_CANCEL", "OK", callback);
+  };
+
+  command.startDriver = function (callback) {
+    doBooleanCommand("DRIVER START", "OK");
+  };
+
+  command.stopDriver = function (callback) {
+    doBooleanCommand("DRIVER STOP", "OK");
+  };
+
+  command.startPacketFiltering = function (callback) {
+    var commandChain = ["DRIVER RXFILTER-ADD 0",
+                        "DRIVER RXFILTER-ADD 1",
+                        "DRIVER RXFILTER-ADD 3",
+                        "DRIVER RXFILTER-START"];
+
+    doBooleanCommandChain(commandChain, callback);
+  };
+
+  command.stopPacketFiltering = function (callback) {
+    var commandChain = ["DRIVER RXFILTER-STOP",
+                        "DRIVER RXFILTER-REMOVE 3",
+                        "DRIVER RXFILTER-REMOVE 1",
+                        "DRIVER RXFILTER-REMOVE 0"];
+
+    doBooleanCommandChain(commandChain, callback);
+  };
+
+  command.doGetRssi = function (cmd, callback) {
+    doCommand(cmd, function(data) {
+      var rssi = -200;
+
+      if (!data.status) {
+        // If we are associating, the reply is "OK".
+        var reply = data.reply;
+        if (reply !== "OK") {
+          // Format is: <SSID> rssi XX". SSID can contain spaces.
+          var offset = reply.lastIndexOf("rssi ");
+          if (offset !== -1) {
+            rssi = reply.substr(offset + 5) | 0;
+          }
+        }
+      }
+      callback(rssi);
+    });
+  };
+
+  command.getRssi = function (callback) {
+    command.doGetRssi("DRIVER RSSI", callback);
+  };
+
+  command.getRssiApprox = function (callback) {
+    command.doGetRssi("DRIVER RSSI-APPROX", callback);
+  };
+
+  command.getLinkSpeed = function (callback) {
+    doStringCommand("DRIVER LINKSPEED", function(reply) {
+      if (reply) {
+        reply = reply.split(" ")[1] | 0; // Format: LinkSpeed XX
+      }
+      callback(reply);
+    });
+  };
+
+  command.getConnectionInfoICS = function (callback) {
+    doStringCommand("SIGNAL_POLL", function(reply) {
+      if (!reply) {
+        callback(null);
+        return;
+      }
+
+      let rval = {};
+      var lines = reply.split("\n");
+      for (let i = 0; i < lines.length; ++i) {
+        let [key, value] = lines[i].split("=");
+        switch (key.toUpperCase()) {
+          case "RSSI":
+            rval.rssi = value | 0;
+            break;
+          case "LINKSPEED":
+            rval.linkspeed = value | 0;
+            break;
+          default:
+            // Ignore.
+        }
+      }
+
+      callback(rval);
+    });
+  };
+
+  command.getMacAddress = function (callback) {
+    doStringCommand("DRIVER MACADDR", function(reply) {
+      if (reply) {
+        reply = reply.split(" ")[2]; // Format: Macaddr = XX.XX.XX.XX.XX.XX
+      }
+      callback(reply);
+    });
+  };
+
+  command.setPowerModeICS = function (mode, callback) {
+    doBooleanCommand("DRIVER POWERMODE " + (mode === "AUTO" ? 0 : 1), "OK", callback);
+  };
+
+  command.setPowerModeJB = function (mode, callback) {
+    doBooleanCommand("SET ps " + (mode === "AUTO" ? 1 : 0), "OK", callback);
+  };
+
+  command.getPowerMode = function (callback) {
+    doStringCommand("DRIVER GETPOWER", function(reply) {
+      if (reply) {
+        reply = (reply.split()[2]|0); // Format: powermode = XX
+      }
+      callback(reply);
+    });
+  };
+
+  command.setNumAllowedChannels = function (numChannels, callback) {
+    doBooleanCommand("DRIVER SCAN-CHANNELS " + numChannels, "OK", callback);
+  };
+
+  command.getNumAllowedChannels = function (callback) {
+    doStringCommand("DRIVER SCAN-CHANNELS", function(reply) {
+      if (reply) {
+        reply = (reply.split()[2]|0); // Format: Scan-Channels = X
+      }
+      callback(reply);
+    });
+  };
+
+  command.setBluetoothCoexistenceMode = function (mode, callback) {
+    doBooleanCommand("DRIVER BTCOEXMODE " + mode, "OK", callback);
+  };
+
+  command.setBluetoothCoexistenceScanMode = function (mode, callback) {
+    doBooleanCommand("DRIVER BTCOEXSCAN-" + (mode ? "START" : "STOP"),
+                     "OK", callback);
+  };
+
+  command.saveConfig = function (callback) {
+    // Make sure we never write out a value for AP_SCAN other than 1.
+    doBooleanCommand("AP_SCAN 1", "OK", function(ok) {
+      doBooleanCommand("SAVE_CONFIG", "OK", callback);
+    });
+  };
+
+  command.reloadConfig = function (callback) {
+    doBooleanCommand("RECONFIGURE", "OK", callback);
+  };
+
+  command.setScanResultHandling = function (mode, callback) {
+    doBooleanCommand("AP_SCAN " + mode, "OK", callback);
+  };
+
+  command.addToBlacklist = function (bssid, callback) {
+    doBooleanCommand("BLACKLIST " + bssid, "OK", callback);
+  };
+
+  command.clearBlacklist = function (callback) {
+    doBooleanCommand("BLACKLIST clear", "OK", callback);
+  };
+
+  command.setSuspendOptimizations = function (enabled, callback) {
+    doBooleanCommand("DRIVER SETSUSPENDOPT " + (enabled ? 0 : 1),
+                     "OK", callback);
+  };
+
+  command.connectToSupplicant = function(callback) {
+    voidControlMessage("connect_to_supplicant", callback);
+  };
+
+  command.closeSupplicantConnection = function(callback) {
+    voidControlMessage("close_supplicant_connection", callback);
+  };
+
+  command.getMacAddress = function(callback) {
+    doStringCommand("DRIVER MACADDR", function(reply) {
+      if (reply) {
+        reply = reply.split(" ")[2]; // Format: Macaddr = XX.XX.XX.XX.XX.XX
+      }
+      callback(reply);
+    });
+  };
+
+  //--------------------------------------------------
+  // Helper functions.
+  //--------------------------------------------------
+
+  function voidControlMessage(cmd, callback) {
+    controlMessage({ cmd: cmd }, function (data) {
+      callback(data.status);
+    });
+  }
+
+  function doCommand(request, callback) {
+    var msg = { cmd:     "command",
+                request: request };
+
+    controlMessage(msg, callback);
+  }
+
+  function doIntCommand(request, callback) {
+    doCommand(request, function(data) {
+      callback(data.status ? -1 : (data.reply|0));
+    });
+  }
+
+  function doBooleanCommand(request, expected, callback) {
+    doCommand(request, function(data) {
+      callback(data.status ? false : (data.reply === expected));
+    });
+  }
+
+  function doStringCommand(request, callback) {
+    doCommand(request, function(data) {
+      callback(data.status ? null : data.reply);
+    });
+  }
+
+  function doBooleanCommandChain(commandChain, callback, i) {
+    if (undefined === i) {
+      i = 0;
+    }
+
+    doBooleanCommand(commandChain[i], "OK", function(ok) {
+      if (!ok) {
+        return callback(false);
+      }
+      i++;
+      if (i === commandChain.length || !commandChain[i]) {
+        // Reach the end or empty command.
+        return callback(true);
+      }
+      doBooleanCommandChain(commandChain, callback, i);
+    });
+  }
+
+  function stopProcess(service, process, callback) {
+    var count = 0;
+    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    function tick() {
+      let result = libcutils.property_get(service);
+      if (result === null) {
+        callback();
+        return;
+      }
+      if (result === "stopped" || ++count >= 5) {
+        // Either we succeeded or ran out of time.
+        timer = null;
+        callback();
+        return;
+      }
+
+      // Else it's still running, continue waiting.
+      timer.initWithCallback(tick, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
+    }
+
+    setProperty("ctl.stop", process, tick);
+  }
+
+  // Wrapper around libcutils.property_set that returns true if setting the
+  // value was successful.
+  // Note that the callback is not called asynchronously.
+  function setProperty(key, value, callback) {
+    let ok = true;
+    try {
+      libcutils.property_set(key, value);
+    } catch(e) {
+      ok = false;
+    }
+    callback(ok);
+  }
+
+  return command;
+};
new file mode 100644
--- /dev/null
+++ b/dom/wifi/WifiNetUtil.jsm
@@ -0,0 +1,211 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/systemlibs.js");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager",
+                                   "@mozilla.org/network/manager;1",
+                                   "nsINetworkManager");
+
+this.EXPORTED_SYMBOLS = ["WifiNetUtil"];
+
+const DHCP_PROP = "init.svc.dhcpcd";
+const DHCP      = "dhcpcd";
+
+this.WifiNetUtil = function(controlMessage) {
+  var util = {};
+
+  util.configureInterface = function(cfg, callback) {
+    let message = { cmd:     "ifc_configure",
+                    ifname:  cfg.ifname,
+                    ipaddr:  cfg.ipaddr,
+                    mask:    cfg.mask,
+                    gateway: cfg.gateway,
+                    dns1:    cfg.dns1,
+                    dns2:    cfg.dns2 };
+
+    controlMessage(message, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.runDhcp = function (ifname, callback) {
+    controlMessage({ cmd: "dhcp_do_request", ifname: ifname }, function(data) {
+      var dhcpInfo = data.status ? null : data;
+      util.runIpConfig(ifname, dhcpInfo, callback);
+    });
+  };
+
+  util.stopDhcp = function (ifname, callback) {
+    // This function does exactly what dhcp_stop does. Unforunately, if we call
+    // this function twice before the previous callback is returned. We may block
+    // our self waiting for the callback. It slows down the wifi startup procedure.
+    // Therefore, we have to roll our own version here.
+    let dhcpService = DHCP_PROP + "_" + ifname;
+    let suffix = (ifname.substr(0, 3) === "p2p") ? "p2p" : ifname;
+    let processName = DHCP + "_" + suffix;
+    stopProcess(dhcpService, processName, callback);
+  };
+
+  util.enableInterface = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_enable", ifname: ifname }, function (data) {
+      callback(!data.status);
+    });
+  };
+
+  util.disableInterface = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_disable", ifname: ifname }, function (data) {
+      callback(!data.status);
+    });
+  };
+
+  util.startDhcpServer = function (range, callback) {
+    gNetworkManager.setDhcpServer(true, range, function (error) {
+      callback(!error);
+    });
+  };
+
+  util.stopDhcpServer = function (callback) {
+    gNetworkManager.setDhcpServer(false, null, function (error) {
+      callback(!error);
+    });
+  };
+
+  util.addHostRoute = function (ifname, route, callback) {
+    controlMessage({ cmd: "ifc_add_host_route", ifname: ifname, route: route }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.removeHostRoutes = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_remove_host_routes", ifname: ifname }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.setDefaultRoute = function (ifname, route, callback) {
+    controlMessage({ cmd: "ifc_set_default_route", ifname: ifname, route: route }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.getDefaultRoute = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_get_default_route", ifname: ifname }, function(data) {
+      callback(!data.route);
+    });
+  };
+
+  util.removeDefaultRoute = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_remove_default_route", ifname: ifname }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.resetConnections = function (ifname, callback) {
+    controlMessage({ cmd: "ifc_reset_connections", ifname: ifname }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.releaseDhcpLease = function (ifname, callback) {
+    controlMessage({ cmd: "dhcp_release_lease", ifname: ifname }, function(data) {
+      callback(!data.status);
+    });
+  };
+
+  util.getDhcpError = function (callback) {
+    controlMessage({ cmd: "dhcp_get_errmsg" }, function(data) {
+      callback(data.error);
+    });
+  };
+
+  util.runDhcpRenew = function (ifname, callback) {
+    controlMessage({ cmd: "dhcp_do_request", ifname: ifname }, function(data) {
+      callback(data.status ? null : data);
+    });
+  };
+
+  util.runIpConfig = function (name, data, callback) {
+    if (!data) {
+      callback({ info: data });
+      return;
+    }
+
+    setProperty("net." + name + ".dns1", ipToString(data.dns1),
+                function(ok) {
+      if (!ok) {
+        return;
+      }
+      setProperty("net." + name + ".dns2", ipToString(data.dns2),
+                  function(ok) {
+        if (!ok) {
+          return;
+        }
+        setProperty("net." + name + ".gw", ipToString(data.gateway),
+                    function(ok) {
+          if (!ok) {
+            return;
+          }
+          callback({ info: data });
+        });
+      });
+    });
+  };
+
+  //--------------------------------------------------
+  // Helper functions.
+  //--------------------------------------------------
+
+  function stopProcess(service, process, callback) {
+    var count = 0;
+    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    function tick() {
+      let result = libcutils.property_get(service);
+      if (result === null) {
+        callback();
+        return;
+      }
+      if (result === "stopped" || ++count >= 5) {
+        // Either we succeeded or ran out of time.
+        timer = null;
+        callback();
+        return;
+      }
+
+      // Else it's still running, continue waiting.
+      timer.initWithCallback(tick, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
+    }
+
+    setProperty("ctl.stop", process, tick);
+  }
+
+  // Wrapper around libcutils.property_set that returns true if setting the
+  // value was successful.
+  // Note that the callback is not called asynchronously.
+  function setProperty(key, value, callback) {
+    let ok = true;
+    try {
+      libcutils.property_set(key, value);
+    } catch(e) {
+      ok = false;
+    }
+    callback(ok);
+  }
+
+  function ipToString(n) {
+    return String((n >>  0) & 0xFF) + "." +
+                 ((n >>  8) & 0xFF) + "." +
+                 ((n >> 16) & 0xFF) + "." +
+                 ((n >> 24) & 0xFF);
+  }
+
+  return util;
+};
--- a/dom/wifi/WifiWorker.js
+++ b/dom/wifi/WifiWorker.js
@@ -6,16 +6,18 @@
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/systemlibs.js");
+Cu.import("resource://gre/modules/WifiCommand.jsm");
+Cu.import("resource://gre/modules/WifiNetUtil.jsm");
 
 var DEBUG = false; // set to true to show debug messages.
 
 const WIFIWORKER_CONTRACTID = "@mozilla.org/wifi/worker;1";
 const WIFIWORKER_CID        = Components.ID("{a14e8977-d259-433a-a88d-58dd44657e5b}");
 
 const WIFIWORKER_WORKER     = "resource://gre/modules/wifi_worker.js";
 
@@ -130,16 +132,19 @@ var WifiManager = (function() {
   // Emulator build runs to here.
   // The debug() should only be used after WifiManager.
   if (!ifname) {
     manager.ifname = DEFAULT_WLAN_INTERFACE;
   }
   manager.schedScanRecovery = schedScanRecovery;
   manager.driverDelay = driverDelay ? parseInt(driverDelay, 10) : DRIVER_READY_WAIT;
 
+  var wifiCommand = WifiCommand(controlMessage);
+  var netUtil = WifiNetUtil(controlMessage);
+
   // Callbacks to invoke when a reply arrives from the wifi service.
   var controlCallbacks = Object.create(null);
   var idgen = 0;
 
   function controlMessage(obj, callback) {
     var id = idgen++;
     obj.id = id;
     if (callback)
@@ -160,146 +165,46 @@ var WifiManager = (function() {
   var recvErrors = 0;
 
   function waitForEvent() {
     wifiService.waitForEvent();
   }
 
   // Commands to the control worker.
 
-  function voidControlMessage(cmd, callback) {
-    controlMessage({ cmd: cmd }, function (data) {
-      callback(data.status);
-    });
-  }
-
   var driverLoaded = false;
 
   function loadDriver(callback) {
     if (driverLoaded) {
       callback(0);
       return;
     }
 
-    voidControlMessage("load_driver", function(status) {
+    wifiCommand.loadDriver(function (status) {
       driverLoaded = (status >= 0);
       callback(status)
     });
   }
 
   function unloadDriver(callback) {
     if (!unloadDriverEnabled) {
       // Unloading drivers is generally unnecessary and
       // can trigger bugs in some drivers.
       // On properly written drivers, bringing the interface
       // down powers down the interface.
       callback(0);
       return;
     }
 
-    voidControlMessage("unload_driver", function(status) {
+    wifiCommand.unloadDriver(function(status) {
       driverLoaded = (status < 0);
       callback(status);
     });
   }
 
-  function startSupplicant(callback) {
-    voidControlMessage("start_supplicant", callback);
-  }
-
-  function terminateSupplicant(callback) {
-    doBooleanCommand("TERMINATE", "OK", callback);
-  }
-
-  function stopSupplicant(callback) {
-    voidControlMessage("stop_supplicant", callback);
-  }
-
-  function connectToSupplicant(callback) {
-    voidControlMessage("connect_to_supplicant", callback);
-  }
-
-  function closeSupplicantConnection(callback) {
-    voidControlMessage("close_supplicant_connection", callback);
-  }
-
-  function doCommand(request, callback) {
-    controlMessage({ cmd: "command", request: request }, callback);
-  }
-
-  function doIntCommand(request, callback) {
-    doCommand(request, function(data) {
-      callback(data.status ? -1 : (data.reply|0));
-    });
-  }
-
-  function doBooleanCommand(request, expected, callback) {
-    doCommand(request, function(data) {
-      callback(data.status ? false : (data.reply == expected));
-    });
-  }
-
-  function doStringCommand(request, callback) {
-    doCommand(request, function(data) {
-      callback(data.status ? null : data.reply);
-    });
-  }
-
-  function listNetworksCommand(callback) {
-    doStringCommand("LIST_NETWORKS", callback);
-  }
-
-  function addNetworkCommand(callback) {
-    doIntCommand("ADD_NETWORK", callback);
-  }
-
-  function setNetworkVariableCommand(netId, name, value, callback) {
-    doBooleanCommand("SET_NETWORK " + netId + " " + name + " " + value, "OK", callback);
-  }
-
-  function getNetworkVariableCommand(netId, name, callback) {
-    doStringCommand("GET_NETWORK " + netId + " " + name, callback);
-  }
-
-  function removeNetworkCommand(netId, callback) {
-    doBooleanCommand("REMOVE_NETWORK " + netId, "OK", callback);
-  }
-
-  function enableNetworkCommand(netId, disableOthers, callback) {
-    doBooleanCommand((disableOthers ? "SELECT_NETWORK " : "ENABLE_NETWORK ") + netId, "OK", callback);
-  }
-
-  function disableNetworkCommand(netId, callback) {
-    doBooleanCommand("DISABLE_NETWORK " + netId, "OK", callback);
-  }
-
-  function statusCommand(callback) {
-    doStringCommand("STATUS", callback);
-  }
-
-  function pingCommand(callback) {
-    doBooleanCommand("PING", "PONG", callback);
-  }
-
-  function scanResultsCommand(callback) {
-    doStringCommand("SCAN_RESULTS", callback);
-  }
-
-  function disconnectCommand(callback) {
-    doBooleanCommand("DISCONNECT", "OK", callback);
-  }
-
-  function reconnectCommand(callback) {
-    doBooleanCommand("RECONNECT", "OK", callback);
-  }
-
-  function reassociateCommand(callback) {
-    doBooleanCommand("REASSOCIATE", "OK", callback);
-  }
-
   // A note about background scanning:
   // Normally, background scanning shouldn't be necessary as wpa_supplicant
   // has the capability to automatically schedule its own scans at appropriate
   // intervals. However, with some drivers, this appears to get stuck after
   // three scans, so we enable the driver's background scanning to work around
   // that when we're not connected to any network. This ensures that we'll
   // automatically reconnect to networks if one falls out of range.
   var reEnableBackgroundScan = false;
@@ -309,70 +214,55 @@ var WifiManager = (function() {
   function setBackgroundScan(enable, callback) {
     var doEnable = (enable === "ON");
     if (doEnable === manager.backgroundScanEnabled) {
       callback(false, true);
       return;
     }
 
     manager.backgroundScanEnabled = doEnable;
-    doBooleanCommand("SET pno " + (manager.backgroundScanEnabled ? "1" : "0"),
-                     "OK",
-                     function(ok) {
-                       callback(true, ok);
-                     });
+    wifiCommand.setBackgroundScan(manager.backgroundScanEnabled, callback);
   }
 
   var scanModeActive = false;
 
-  function doSetScanModeCommand(setActive, callback) {
-    doBooleanCommand(setActive ? "DRIVER SCAN-ACTIVE" : "DRIVER SCAN-PASSIVE", "OK", callback);
-  }
-
-  function scanCommand(forceActive, callback) {
+  function scan(forceActive, callback) {
     if (forceActive && !scanModeActive) {
       // Note: we ignore errors from doSetScanMode.
-      doSetScanModeCommand(true, function(ignore) {
+      wifiCommand.doSetScanMode(true, function(ignore) {
         setBackgroundScan("OFF", function(turned, ignore) {
           reEnableBackgroundScan = turned;
-          doBooleanCommand("SCAN", "OK", function(ok) {
-            doSetScanModeCommand(false, function(ignore) {
+          wifiCommand.scan(function(ok) {
+            wifiCommand.doSetScanMode(false, function(ignore) {
               // The result of scanCommand is the result of the actual SCAN
               // request.
               callback(ok);
             });
           });
         });
       });
       return;
     }
-    doBooleanCommand("SCAN", "OK", callback);
+    wifiCommand.scan(callback);
   }
 
   var debugEnabled = false;
-  function setLogLevel(level, callback) {
-    doBooleanCommand("LOG_LEVEL " + level, "OK", callback);
-  }
 
   function syncDebug() {
     if (debugEnabled !== DEBUG) {
       let wanted = DEBUG;
-      setLogLevel(wanted ? "DEBUG" : "INFO", function(ok) {
+      wifiCommand.setLogLevel(wanted ? "DEBUG" : "INFO", function(ok) {
         if (ok)
           debugEnabled = wanted;
       });
     }
   }
 
-  function getLogLevel(callback) {
-    doStringCommand("LOG_LEVEL", callback);
-  }
-
   function getDebugEnabled(callback) {
-    getLogLevel(function(level) {
+    wifiCommand.getLogLevel(function(level) {
       if (level === null) {
         debug("Unable to get wpa_supplicant's log level");
         callback(false);
         return;
       }
 
       var lines = level.split("\n");
       for (let i = 0; i < lines.length; ++i) {
@@ -384,266 +274,19 @@ var WifiManager = (function() {
         }
       }
 
       // If we're here, we didn't get the current level.
       callback(false);
     });
   }
 
-  function setScanModeCommand(setActive, callback) {
+  function setScanMode(setActive, callback) {
     scanModeActive = setActive;
-    doSetScanModeCommand(setActive, callback);
-  }
-
-  function wpsPbcCommand(callback) {
-    doBooleanCommand("WPS_PBC", "OK", callback);
-  }
-
-  function wpsPinCommand(detail, callback) {
-    doStringCommand("WPS_PIN " +
-                    (detail.bssid === undefined ? "any" : detail.bssid) +
-                    (detail.pin === undefined ? "" : (" " + detail.pin)),
-                    callback);
-  }
-
-  function wpsCancelCommand(callback) {
-    doBooleanCommand("WPS_CANCEL", "OK", callback);
-  }
-
-  function startDriverCommand(callback) {
-    doBooleanCommand("DRIVER START", "OK");
-  }
-
-  function stopDriverCommand(callback) {
-    doBooleanCommand("DRIVER STOP", "OK");
-  }
-
-  function startPacketFiltering(callback) {
-    doBooleanCommand("DRIVER RXFILTER-ADD 0", "OK", function(ok) {
-      ok && doBooleanCommand("DRIVER RXFILTER-ADD 1", "OK", function(ok) {
-        ok && doBooleanCommand("DRIVER RXFILTER-ADD 3", "OK", function(ok) {
-          ok && doBooleanCommand("DRIVER RXFILTER-START", "OK", callback)
-        });
-      });
-    });
-  }
-
-  function stopPacketFiltering(callback) {
-    doBooleanCommand("DRIVER RXFILTER-STOP", "OK", function(ok) {
-      ok && doBooleanCommand("DRIVER RXFILTER-REMOVE 3", "OK", function(ok) {
-        ok && doBooleanCommand("DRIVER RXFILTER-REMOVE 1", "OK", function(ok) {
-          ok && doBooleanCommand("DRIVER RXFILTER-REMOVE 0", "OK", callback)
-        });
-      });
-    });
-  }
-
-  function doGetRssiCommand(cmd, callback) {
-    doCommand(cmd, function(data) {
-      var rssi = -200;
-
-      if (!data.status) {
-        // If we are associating, the reply is "OK".
-        var reply = data.reply;
-        if (reply != "OK") {
-          // Format is: <SSID> rssi XX". SSID can contain spaces.
-          var offset = reply.lastIndexOf("rssi ");
-          if (offset !== -1)
-            rssi = reply.substr(offset + 5) | 0;
-        }
-      }
-      callback(rssi);
-    });
-  }
-
-  function getRssiCommand(callback) {
-    doGetRssiCommand("DRIVER RSSI", callback);
-  }
-
-  function getRssiApproxCommand(callback) {
-    doGetRssiCommand("DRIVER RSSI-APPROX", callback);
-  }
-
-  function getLinkSpeedCommand(callback) {
-    doStringCommand("DRIVER LINKSPEED", function(reply) {
-      if (reply)
-        reply = reply.split(" ")[1] | 0; // Format: LinkSpeed XX
-      callback(reply);
-    });
-  }
-
-  function getConnectionInfoGB(callback) {
-    var rval = {};
-    getRssiApproxCommand(function(rssi) {
-      rval.rssi = rssi;
-      getLinkSpeedCommand(function(linkspeed) {
-        rval.linkspeed = linkspeed;
-        callback(rval);
-      });
-    });
-  }
-
-  function getConnectionInfoICS(callback) {
-    doStringCommand("SIGNAL_POLL", function(reply) {
-      if (!reply) {
-        callback(null);
-        return;
-      }
-
-      let rval = {};
-      var lines = reply.split("\n");
-      for (let i = 0; i < lines.length; ++i) {
-        let [key, value] = lines[i].split("=");
-        switch (key.toUpperCase()) {
-          case "RSSI":
-            rval.rssi = value | 0;
-            break;
-          case "LINKSPEED":
-            rval.linkspeed = value | 0;
-            break;
-          default:
-            // Ignore.
-        }
-      }
-
-      callback(rval);
-    });
-  }
-
-  function getMacAddressCommand(callback) {
-    doStringCommand("DRIVER MACADDR", function(reply) {
-      if (reply)
-        reply = reply.split(" ")[2]; // Format: Macaddr = XX.XX.XX.XX.XX.XX
-      callback(reply);
-    });
-  }
-
-  function setPowerModeCommandICS(mode, callback) {
-    doBooleanCommand("DRIVER POWERMODE " + (mode === "AUTO" ? 0 : 1), "OK", callback);
-  }
-
-  function setPowerModeCommandJB(mode, callback) {
-    doBooleanCommand("SET ps " + (mode === "AUTO" ? 1 : 0), "OK", callback);
-  }
-
-  function getPowerModeCommand(callback) {
-    doStringCommand("DRIVER GETPOWER", function(reply) {
-      if (reply)
-        reply = (reply.split()[2]|0); // Format: powermode = XX
-      callback(reply);
-    });
-  }
-
-  function setNumAllowedChannelsCommand(numChannels, callback) {
-    doBooleanCommand("DRIVER SCAN-CHANNELS " + numChannels, "OK", callback);
-  }
-
-  function getNumAllowedChannelsCommand(callback) {
-    doStringCommand("DRIVER SCAN-CHANNELS", function(reply) {
-      if (reply)
-        reply = (reply.split()[2]|0); // Format: Scan-Channels = X
-      callback(reply);
-    });
-  }
-
-  function setBluetoothCoexistenceModeCommand(mode, callback) {
-    doBooleanCommand("DRIVER BTCOEXMODE " + mode, "OK", callback);
-  }
-
-  function setBluetoothCoexistenceScanModeCommand(mode, callback) {
-    doBooleanCommand("DRIVER BTCOEXSCAN-" + (mode ? "START" : "STOP"), "OK", callback);
-  }
-
-  function saveConfigCommand(callback) {
-    // Make sure we never write out a value for AP_SCAN other than 1
-    doBooleanCommand("AP_SCAN 1", "OK", function(ok) {
-      doBooleanCommand("SAVE_CONFIG", "OK", callback);
-    });
-  }
-
-  function reloadConfigCommand(callback) {
-    doBooleanCommand("RECONFIGURE", "OK", callback);
-  }
-
-  function setScanResultHandlingCommand(mode, callback) {
-    doBooleanCommand("AP_SCAN " + mode, "OK", callback);
-  }
-
-  function addToBlacklistCommand(bssid, callback) {
-    doBooleanCommand("BLACKLIST " + bssid, "OK", callback);
-  }
-
-  function clearBlacklistCommand(callback) {
-    doBooleanCommand("BLACKLIST clear", "OK", callback);
-  }
-
-  function setSuspendOptimizationsCommand(enabled, callback) {
-    doBooleanCommand("DRIVER SETSUSPENDOPT " + (enabled ? 0 : 1), "OK", callback);
-  }
-
-  // Wrapper around libcutils.property_set that returns true if setting the
-  // value was successful.
-  // Note that the callback is not called asynchronously.
-  function setProperty(key, value, callback) {
-    let ok = true;
-    try {
-      libcutils.property_set(key, value);
-    } catch(e) {
-      ok = false;
-    }
-    callback(ok);
-  }
-
-  function enableInterface(ifname, callback) {
-    controlMessage({ cmd: "ifc_enable", ifname: ifname }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function disableInterface(ifname, callback) {
-    controlMessage({ cmd: "ifc_disable", ifname: ifname }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function addHostRoute(ifname, route, callback) {
-    controlMessage({ cmd: "ifc_add_host_route", ifname: ifname, route: route }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function removeHostRoutes(ifname, callback) {
-    controlMessage({ cmd: "ifc_remove_host_routes", ifname: ifname }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function setDefaultRoute(ifname, route, callback) {
-    controlMessage({ cmd: "ifc_set_default_route", ifname: ifname, route: route }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function getDefaultRoute(ifname, callback) {
-    controlMessage({ cmd: "ifc_get_default_route", ifname: ifname }, function(data) {
-      callback(!data.route);
-    });
-  }
-
-  function removeDefaultRoute(ifname, callback) {
-    controlMessage({ cmd: "ifc_remove_default_route", ifname: ifname }, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function resetConnections(ifname, callback) {
-    controlMessage({ cmd: "ifc_reset_connections", ifname: ifname }, function(data) {
-      callback(!data.status);
-    });
+    wifiCommand.doSetScanMode(setActive, callback);
   }
 
   var httpProxyConfig = Object.create(null);
 
   /**
    * Given a network, configure http proxy when using wifi.
    * @param network A network object to update http proxy
    * @param info Info should have following field:
@@ -708,122 +351,53 @@ var WifiManager = (function() {
 
       // If the ssid of current connection is the same as configured ssid
       // It means we need update current connection to use static IP address.
       if (setNetworkKey == curNetworkKey) {
         // Use configureInterface directly doesn't work, the network iterface
         // and routing table is changed but still cannot connect to network
         // so the workaround here is disable interface the enable again to
         // trigger network reconnect with static ip.
-        disableInterface(manager.ifname, function (ok) {
-          enableInterface(manager.ifname, function (ok) {
+        netUtil.disableInterface(manager.ifname, function (ok) {
+          netUtil.enableInterface(manager.ifname, function (ok) {
           });
         });
       }
     });
   }
 
   var dhcpInfo = null;
-  function runDhcp(ifname) {
-    debug("Run Dhcp");
-    controlMessage({ cmd: "dhcp_do_request", ifname: ifname }, function(data) {
-      dhcpInfo = data.status ? null : data;
-      runIpConfig(ifname, dhcpInfo);
-    });
-  }
 
   function runStaticIp(ifname, key) {
     debug("Run static ip");
 
     // Read static ip information from settings.
     let staticIpInfo;
 
     if (!(key in staticIpConfig))
       return;
 
     staticIpInfo = staticIpConfig[key];
 
     // Stop dhcpd when use static IP
     if (dhcpInfo != null) {
-      stopDhcp(manager.ifname, function() {});
+      netUtil.stopDhcp(manager.ifname, function() {});
     }
 
     // Set ip, mask length, gateway, dns to network interface
-    configureInterface(ifname,
-                       staticIpInfo.ipaddr,
-                       staticIpInfo.maskLength,
-                       staticIpInfo.gateway,
-                       staticIpInfo.dns1,
-                       staticIpInfo.dns2, function (data) {
-      runIpConfig(ifname, staticIpInfo);
-    });
-  }
-
-  function stopProcess(service, process, callback) {
-    var count = 0;
-    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    function tick() {
-      let result = libcutils.property_get(service);
-      if (result === null) {
-        callback();
-        return;
-      }
-      if (result === "stopped" || ++count >= 5) {
-        // Either we succeeded or ran out of time.
-        timer = null;
-        callback();
-        return;
-      }
-
-      // Else it's still running, continue waiting.
-      timer.initWithCallback(tick, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
-    }
-
-    setProperty("ctl.stop", process, tick);
-  }
-
-  function stopDhcp(ifname, callback) {
-    // This function does exactly what dhcp_stop does. Unforunately, if we call
-    // this function twice before the previous callback is returned. We may block
-    // our self waiting for the callback. It slows down the wifi startup procedure.
-    // Therefore, we have to roll our own version here.
-    let dhcpService = DHCP_PROP + "_" + ifname;
-    let suffix = (ifname.substr(0, 3) === "p2p") ? "p2p" : ifname;
-    let processName = DHCP + "_" + suffix;
-    stopProcess(dhcpService, processName, callback);
-  }
-
-  function releaseDhcpLease(ifname, callback) {
-    controlMessage({ cmd: "dhcp_release_lease", ifname: ifname }, function(data) {
-      dhcpInfo = null;
-      notify("dhcplost");
-      callback(!data.status);
-    });
-  }
-
-  function getDhcpError(callback) {
-    controlMessage({ cmd: "dhcp_get_errmsg" }, function(data) {
-      callback(data.error);
-    });
-  }
-
-  function configureInterface(ifname, ipaddr, mask, gateway, dns1, dns2, callback) {
-    let message = { cmd: "ifc_configure", ifname: ifname,
-                     ipaddr: ipaddr, mask: mask, gateway: gateway,
-                     dns1: dns1, dns2: dns2};
-    controlMessage(message, function(data) {
-      callback(!data.status);
-    });
-  }
-
-  function runDhcpRenew(ifname, callback) {
-    controlMessage({ cmd: "dhcp_do_request", ifname: ifname }, function(data) {
-      if (!data.status)
-        dhcpInfo = data;
-      callback(data.status ? null : data);
+    netUtil.configureInterface( { ifname: ifname,
+                                  ipaddr: staticIpInfo.ipaddr,
+                                  mask: staticIpInfo.maskLength,
+                                  gateway: staticIpInfo.gateway,
+                                  dns1: staticIpInfo.dns1,
+                                  dns2: staticIpInfo.dns2 }, function (data) {
+      netUtil.runIpConfig(ifname, staticIpInfo, function(data) {
+        dhcpInfo = data.info;
+        notify("networkconnected", data);
+      });
     });
   }
 
   var suppressEvents = false;
   function notify(eventName, eventObject) {
     if (suppressEvents)
       return;
     var handler = manager["on" + eventName];
@@ -928,43 +502,48 @@ var WifiManager = (function() {
       return;
     }
     if (connectTries++ < 3) {
       // Try again in 5 seconds.
       if (!retryTimer)
         retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 
       retryTimer.initWithCallback(function(timer) {
-        connectToSupplicant(connectCallback);
+        wifiCommand.connectToSupplicant(connectCallback);
       }, 5000, Ci.nsITimer.TYPE_ONE_SHOT);
       return;
     }
 
     retryTimer = null;
     connectTries = 0;
     notify("supplicantlost", { success: false });
   }
 
   manager.connectionDropped = function(callback) {
     // Reset network interface when connection drop
-    configureInterface(manager.ifname, 0, 0, 0, 0, 0, function (data) {
+    netUtil.configureInterface( { ifname: manager.ifname,
+                                  ipaddr: 0,
+                                  mask: 0,
+                                  gateway: 0,
+                                  dns1: 0,
+                                  dns2: 0 }, function (data) {
     });
 
     // If we got disconnected, kill the DHCP client in preparation for
     // reconnection.
-    resetConnections(manager.ifname, function() {
-      stopDhcp(manager.ifname, function() {
+    netUtil.resetConnections(manager.ifname, function() {
+      netUtil.stopDhcp(manager.ifname, function() {
         callback();
       });
     });
   }
 
   manager.start = function() {
     debug("detected SDK version " + sdkVersion);
-    connectToSupplicant(connectCallback);
+    wifiCommand.connectToSupplicant(connectCallback);
   }
 
   function onconnected() {
     // For now we do our own DHCP. In the future, this should be handed
     // off to the Network Manager.
     let currentNetwork = Object.create(null);
     currentNetwork.netId = manager.connectionInfo.id;
 
@@ -972,47 +551,19 @@ var WifiManager = (function() {
       let key = getNetworkKey(currentNetwork);
       if (staticIpConfig  &&
           (key in staticIpConfig) &&
           staticIpConfig[key].enabled) {
           debug("Run static ip");
           runStaticIp(manager.ifname, key);
           return;
       }
-      runDhcp(manager.ifname);
-    });
-  }
-
-  function runIpConfig(name, data) {
-    if (!data) {
-      debug("IP config failed to run");
-      notify("networkconnected", { info: data });
-      return;
-    }
-
-    setProperty("net." + name + ".dns1", ipToString(data.dns1),
-                function(ok) {
-      if (!ok) {
-        debug("Unable to set net.<ifname>.dns1");
-        return;
-      }
-      setProperty("net." + name + ".dns2", ipToString(data.dns2),
-                  function(ok) {
-        if (!ok) {
-          debug("Unable to set net.<ifname>.dns2");
-          return;
-        }
-        setProperty("net." + name + ".gw", ipToString(data.gateway),
-                    function(ok) {
-          if (!ok) {
-            debug("Unable to set net.<ifname>.gw");
-            return;
-          }
-          notify("networkconnected", { info: data });
-        });
+      netUtil.runDhcp(manager.ifname, function(data) {
+        dhcpInfo = data.info;
+        notify("networkconnected", data);
       });
     });
   }
 
   var supplicantStatesMap = (sdkVersion >= 15) ?
     ["DISCONNECTED", "INTERFACE_DISABLED", "INACTIVE", "SCANNING",
      "AUTHENTICATING", "ASSOCIATING", "ASSOCIATED", "FOUR_WAY_HANDSHAKE",
      "GROUP_HANDSHAKE", "COMPLETED"]
@@ -1182,33 +733,24 @@ var WifiManager = (function() {
     if (eventData.indexOf("WPS-OVERLAP-DETECTED") === 0) {
       notifyStateChange({ state: "WPS_OVERLAP_DETECTED", BSSID: null, id: -1 });
       return true;
     }
     // Unknown event.
     return true;
   }
 
-  function killSupplicant(callback) {
-    // It is interesting to note that this function does exactly what
-    // wifi_stop_supplicant does. Unforunately, on the Galaxy S2, Samsung
-    // changed that function in a way that means that it doesn't recognize
-    // wpa_supplicant as already running. Therefore, we have to roll our own
-    // version here.
-    stopProcess(SUPP_PROP, WPA_SUPPLICANT, callback);
-  }
-
   function didConnectSupplicant(callback) {
     waitForEvent();
 
     // Load up the supplicant state.
     getDebugEnabled(function(ok) {
       syncDebug();
     });
-    statusCommand(function(status) {
+    wifiCommand.status(function(status) {
       parseStatus(status);
       notify("supplicantconnection");
       callback();
     });
   }
 
   function prepareForStartup(callback) {
     let status = libcutils.property_get(DHCP_PROP + "_" + manager.ifname);
@@ -1225,18 +767,18 @@ var WifiManager = (function() {
     // start, so we hand-roll it here.
     function tryStopSupplicant () {
       let status = libcutils.property_get(SUPP_PROP);
       if (status !== "running") {
         callback();
         return;
       }
       suppressEvents = true;
-      killSupplicant(function() {
-        disableInterface(manager.ifname, function (ok) {
+      wifiCommand.killSupplicant(function() {
+        netUtil.disableInterface(manager.ifname, function (ok) {
           suppressEvents = false;
           callback();
         });
       });
     }
   }
 
   // Initial state.
@@ -1300,27 +842,27 @@ var WifiManager = (function() {
             if (status) {
               callback(status);
               manager.state = "UNINITIALIZED";
               return;
             }
 
             function doStartSupplicant() {
               cancelWaitForDriverReadyTimer();
-              startSupplicant(function (status) {
+              wifiCommand.startSupplicant(function (status) {
                 if (status < 0) {
                   unloadDriver(function() {
                     callback(status);
                   });
                   manager.state = "UNINITIALIZED";
                   return;
                 }
 
                 manager.supplicantStarted = true;
-                enableInterface(manager.ifname, function (ok) {
+                netUtil.enableInterface(manager.ifname, function (ok) {
                   callback(ok ? 0 : -1);
                 });
               });
             }
             // Driver startup on certain platforms takes longer than it takes for us
             // to return from loadDriver, so wait 2 seconds before starting
             // the supplicant to give it a chance to start.
             if (manager.driverDelay > 0) {
@@ -1330,22 +872,22 @@ var WifiManager = (function() {
             }
           });
         });
       });
     } else {
       // Note these following calls ignore errors. If we fail to kill the
       // supplicant gracefully, then we need to continue telling it to die
       // until it does.
-      terminateSupplicant(function (ok) {
+      wifiCommand.terminateSupplicant(function (ok) {
         manager.connectionDropped(function () {
-          stopSupplicant(function (status) {
-            closeSupplicantConnection(function () {
+          wifiCommand.stopSupplicant(function (status) {
+            wifiCommand.closeSupplicantConnection(function () {
               manager.state = "UNINITIALIZED";
-              disableInterface(manager.ifname, function (ok) {
+              netUtil.disableInterface(manager.ifname, function (ok) {
                 unloadDriver(callback);
               });
             });
           });
         });
       });
     }
   }
@@ -1395,33 +937,33 @@ var WifiManager = (function() {
           }
           manager.tetheringState = "UNINITIALIZED";
           callback();
         });
       });
     }
   }
 
-  manager.disconnect = disconnectCommand;
-  manager.reconnect = reconnectCommand;
-  manager.reassociate = reassociateCommand;
+  manager.disconnect = wifiCommand.disconnect;
+  manager.reconnect = wifiCommand.reconnect;
+  manager.reassociate = wifiCommand.reassociate;
 
   var networkConfigurationFields = [
     "ssid", "bssid", "psk", "wep_key0", "wep_key1", "wep_key2", "wep_key3",
     "wep_tx_keyidx", "priority", "key_mgmt", "scan_ssid", "disabled",
     "identity", "password", "auth_alg", "phase1", "phase2", "eap", "pin",
     "pcsc"
   ];
 
   manager.getNetworkConfiguration = function(config, callback) {
     var netId = config.netId;
     var done = 0;
     for (var n = 0; n < networkConfigurationFields.length; ++n) {
       let fieldName = networkConfigurationFields[n];
-      getNetworkVariableCommand(netId, fieldName, function(value) {
+      wifiCommand.getNetworkVariable(netId, fieldName, function(value) {
         if (value !== null)
           config[fieldName] = value;
         if (++done == networkConfigurationFields.length)
           callback(config);
       });
     }
   }
   manager.setNetworkConfiguration = function(config, callback) {
@@ -1435,30 +977,30 @@ var WifiManager = (function() {
           // supplicant, and often we have a star in our config. In that case,
           // we need to avoid overwriting the correct password with a *.
           (fieldName === "password" ||
            fieldName === "wep_key0" ||
            fieldName === "psk") &&
           config[fieldName] === '*') {
         ++done;
       } else {
-        setNetworkVariableCommand(netId, fieldName, config[fieldName], function(ok) {
+        wifiCommand.setNetworkVariable(netId, fieldName, config[fieldName], function(ok) {
           if (!ok)
             ++errors;
           if (++done == networkConfigurationFields.length)
             callback(errors == 0);
         });
       }
     }
     // If config didn't contain any of the fields we want, don't lose the error callback.
     if (done == networkConfigurationFields.length)
       callback(false);
   }
   manager.getConfiguredNetworks = function(callback) {
-    listNetworksCommand(function (reply) {
+    wifiCommand.listNetworks(function (reply) {
       var networks = Object.create(null);
       var lines = reply.split("\n");
       if (lines.length === 1) {
         // We need to make sure we call the callback even if there are no
         // configured networks.
         callback(networks);
         return;
       }
@@ -1481,52 +1023,45 @@ var WifiManager = (function() {
           break;
         }
         manager.getNetworkConfiguration(config, function (ok) {
             if (!ok)
               ++errors;
             if (++done == lines.length - 1) {
               if (errors) {
                 // If an error occured, delete the new netId.
-                removeNetworkCommand(netId, function() {
+                wifiCommand.removeNetwork(netId, function() {
                   callback(null);
                 });
               } else {
                 callback(networks);
               }
             }
         });
       }
     });
   }
   manager.addNetwork = function(config, callback) {
-    addNetworkCommand(function (netId) {
+    wifiCommand.addNetwork(function (netId) {
       config.netId = netId;
       manager.setNetworkConfiguration(config, function (ok) {
         if (!ok) {
-          removeNetworkCommand(netId, function() { callback(false); });
+          wifiCommand.removeNetwork(netId, function() { callback(false); });
           return;
         }
 
         callback(ok);
       });
     });
   }
   manager.updateNetwork = function(config, callback) {
     manager.setNetworkConfiguration(config, callback);
   }
   manager.removeNetwork = function(netId, callback) {
-    removeNetworkCommand(netId, callback);
-  }
-
-  function ipToString(n) {
-    return String((n >>  0) & 0xFF) + "." +
-                 ((n >>  8) & 0xFF) + "." +
-                 ((n >> 16) & 0xFF) + "." +
-                 ((n >> 24) & 0xFF);
+    wifiCommand.removeNetwork(netId, callback);
   }
 
   function stringToIp(string) {
     let ip = 0;
     let start, end = -1;
     for (let i = 0; i < 4; i++) {
       start = end + 1;
       end = string.indexOf(".", start);
@@ -1557,48 +1092,48 @@ var WifiManager = (function() {
     let mask = 0;
     for (let i = 0; i < len; ++i) {
       mask |= (0x80000000 >> i);
     }
     return ntohl(mask);
   }
 
   manager.saveConfig = function(callback) {
-    saveConfigCommand(callback);
+    wifiCommand.saveConfig(callback);
   }
   manager.enableNetwork = function(netId, disableOthers, callback) {
-    enableNetworkCommand(netId, disableOthers, callback);
+    wifiCommand.enableNetwork(netId, disableOthers, callback);
   }
   manager.disableNetwork = function(netId, callback) {
-    disableNetworkCommand(netId, callback);
+    wifiCommand.disableNetwork(netId, callback);
   }
-  manager.getMacAddress = getMacAddressCommand;
-  manager.getScanResults = scanResultsCommand;
+  manager.getMacAddress = wifiCommand.getMacAddress;
+  manager.getScanResults = wifiCommand.scanResults;
   manager.setScanMode = function(mode, callback) {
-    setScanModeCommand(mode === "active", callback);
+    setScanMode(mode === "active", callback); // Use our own version.
   }
-  manager.setBackgroundScan = setBackgroundScan;
-  manager.scan = scanCommand;
-  manager.wpsPbc = wpsPbcCommand;
-  manager.wpsPin = wpsPinCommand;
-  manager.wpsCancel = wpsCancelCommand;
+  manager.setBackgroundScan = setBackgroundScan; // Use our own version.
+  manager.scan = scan; // Use our own version.
+  manager.wpsPbc = wifiCommand.wpsPbc;
+  manager.wpsPin = wifiCommand.wpsPin;
+  manager.wpsCancel = wifiCommand.wpsCancel;
   manager.setPowerMode = (sdkVersion >= 16)
-                         ? setPowerModeCommandJB
-                         : setPowerModeCommandICS;
+                         ? wifiCommand.setPowerModeJB
+                         : wifiCommand.setPowerModeICS;
   manager.getHttpProxyNetwork = getHttpProxyNetwork;
   manager.setHttpProxy = setHttpProxy;
   manager.configureHttpProxy = configureHttpProxy;
-  manager.setSuspendOptimizations = setSuspendOptimizationsCommand;
+  manager.setSuspendOptimizations = wifiCommand.setSuspendOptimizations;
   manager.setStaticIpMode = setStaticIpMode;
-  manager.getRssiApprox = getRssiApproxCommand;
-  manager.getLinkSpeed = getLinkSpeedCommand;
+  manager.getRssiApprox = wifiCommand.getRssiApprox;
+  manager.getLinkSpeed = wifiCommand.getLinkSpeed;
   manager.getDhcpInfo = function() { return dhcpInfo; }
   manager.getConnectionInfo = (sdkVersion >= 15)
-                              ? getConnectionInfoICS
-                              : getConnectionInfoGB;
+                              ? wifiCommand.getConnectionInfoICS
+                              : wifiCommand.getConnectionInfoGB;
 
   manager.isHandShakeState = function(state) {
     switch (state) {
       case "AUTHENTICATING":
       case "ASSOCIATING":
       case "ASSOCIATED":
       case "FOUR_WAY_HANDSHAKE":
       case "GROUP_HANDSHAKE":
--- a/dom/wifi/moz.build
+++ b/dom/wifi/moz.build
@@ -17,16 +17,21 @@ MODULE = 'dom'
 
 EXTRA_COMPONENTS += [
     'DOMWifiManager.js',
     'DOMWifiManager.manifest',
     'WifiWorker.js',
     'WifiWorker.manifest',
 ]
 
+EXTRA_JS_MODULES += [
+    'WifiCommand.jsm',
+    'WifiNetUtil.jsm',
+]
+
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     CPP_SOURCES = [
         'NetUtils.cpp',
         'WifiProxyService.cpp',
         'WifiUtils.cpp',
     ]
 
 LIBXUL_LIBRARY = True
--- a/mobile/android/base/tests/ContentContextMenuTest.java.in
+++ b/mobile/android/base/tests/ContentContextMenuTest.java.in
@@ -5,17 +5,17 @@ import @ANDROID_PACKAGE_NAME@.*;
 import android.content.ContentResolver;
 import android.util.DisplayMetrics;
 
 import java.lang.reflect.Method;
 
 /**
  * This class covers interactions with the context menu opened from web content
  */
-abstract class ContentContextMenuTest extends BaseTest {
+abstract class ContentContextMenuTest extends PixelTest {
     private static final int MAX_TEST_TIMEOUT = 10000;
 
     // This method opens the context menu of any web content. It assumes that the page is already loaded
     protected void openWebContentContextMenu(String waitText) {
         DisplayMetrics dm = new DisplayMetrics();
         getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
 
         // The web content we are trying to open the context menu for should be positioned at the top of the page, at least 60px heigh and aligned to the middle
--- a/mobile/android/base/tests/PixelTest.java.in
+++ b/mobile/android/base/tests/PixelTest.java.in
@@ -42,16 +42,32 @@ abstract class PixelTest extends BaseTes
         return p;
     }
 
     protected final void reloadAndPaint() {
         PaintedSurface painted = reloadAndGetPainted();
         painted.close();
     }
 
+    public void addTab(String url, String title, boolean isPrivate) {
+        Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+        if (isPrivate) {
+            selectMenuItem(StringHelper.NEW_PRIVATE_TAB_LABEL);
+        } else {
+            selectMenuItem(StringHelper.NEW_TAB_LABEL);
+        }
+        tabEventExpecter.blockForEvent();
+        contentEventExpecter.blockForEvent();
+        loadAndPaint(url);
+        tabEventExpecter.unregisterListener();
+        contentEventExpecter.unregisterListener();
+        mAsserter.ok(waitForText(title), "Checking that the page has loaded", "The page has loaded");
+    }
+
     protected final PaintedSurface waitForPaint(Actions.RepeatedEventExpecter expecter) {
         expecter.blockUntilClear(PAINT_CLEAR_DELAY);
         PaintedSurface p = mDriver.getPaintedSurface();
         if (p == null) {
             mAsserter.ok(p != null, "checking that painted surface loaded", 
                  "painted surface loaded");
         }
         return p;
--- a/mobile/android/base/tests/StringHelper.java.in
+++ b/mobile/android/base/tests/StringHelper.java.in
@@ -12,16 +12,44 @@ class StringHelper {
     };
     public static final String[] DEFAULT_BOOKMARKS_URLS = new String[] {
         "about:firefox",
         "http://support.mozilla.org/en-US/products/mobile",
         "https://addons.mozilla.org/en-US/android/"
     };
     public static final int DEFAULT_BOOKMARKS_COUNT = DEFAULT_BOOKMARKS_TITLES.length;
 
+    // About pages
+    public static final String ABOUT_BLANK_URL = "about:blank";
+    public static final String ABOUT_FIREFOX_URL = "about:firefox";
+    public static final String ABOUT_DOWNLOADS_URL = "about:downloads";
+    public static final String ABOUT_ADDONS_URL = "about:addons";
+    public static final String ABOUT_APPS_URL = "about:apps";
+
+    // Context Menu menu items
+    public static final String[] CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB = new String[] {
+        "Open Link in Private Tab", 
+        "Copy Link", 
+        "Share Link", 
+        "Bookmark Link"
+    };
+
+    public static final String[] CONTEXT_MENU_ITEMS_IN_NORMAL_TAB = new String[] {
+        "Open Link in New Tab",
+        "Open Link in Private Tab", 
+        "Copy Link", 
+        "Share Link", 
+        "Bookmark Link"
+    };
+
+    public static final String[] BOOKMARKS_OPTIONS_CONTEXTMENU_ITEMS = new String[] {
+        "Edit",
+        "Add to Home Screen"
+    };
+
     // Robocop page urls
     // Note: please use getAbsoluteUrl(String url) on each robocop url to get the correct url
     public static final String ROBOCOP_BIG_LINK_URL = "/robocop/robocop_big_link.html";
     public static final String ROBOCOP_BIG_MAILTO_URL = "/robocop/robocop_big_mailto.html";
     public static final String ROBOCOP_BLANK_PAGE_01_URL = "/robocop/robocop_blank_01.html";
     public static final String ROBOCOP_BLANK_PAGE_02_URL = "/robocop/robocop_blank_02.html";
     public static final String ROBOCOP_BLANK_PAGE_03_URL = "/robocop/robocop_blank_03.html";
     public static final String ROBOCOP_BOXES_URL = "/robocop/robocop_boxes.html";
@@ -124,15 +152,9 @@ class StringHelper {
     public static final String FORWARD_LABEL = "Forward";
     public static final String BOOKMARK_LABEL = "Bookmark";
 
     // Bookmark Toast Notification
     public static final String BOOKMARK_ADDED_LABEL = "Bookmark added";
     public static final String BOOKMARK_REMOVED_LABEL = "Bookmark removed";
     public static final String BOOKMARK_UPDATED_LABEL = "Bookmark updated";
     public static final String BOOKMARK_OPTIONS_LABEL = "Options";
-
-    // Bookmark Options Context Menu items
-    public static final String[] BOOKMARKS_OPTIONS_CONTEXTMENU_ITEMS = new String[] {
-    "Edit",
-    "Add to Home Screen"
-    };
 }
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -38,14 +38,15 @@
 [testJarReader]
 [testDistribution]
 [testFindInPage]
 [testInputUrlBar]
 [testAddSearchEngine]
 [testImportFromAndroid]
 [testMasterPassword]
 [testDeviceSearchEngine]
+[testPrivateBrowsing]
 
 # Used for Talos, please don't use in mochitest
 #[testPan]
 #[testCheck]
 #[testCheck2]
 #[testBrowserProviderPerf]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testPrivateBrowsing.java.in
@@ -0,0 +1,72 @@
+#filter substitution
+package @ANDROID_PACKAGE_NAME@.tests;
+
+import @ANDROID_PACKAGE_NAME@.*;
+
+import java.util.ArrayList;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * The test loads a new private tab and loads a page with a big link on it
+ * Opens the link in a new private tab and checks that it is private
+ * Adds a new normal tab and loads a 3rd URL
+ * Checks that the bigLinkUrl loaded in the normal tab is present in the browsing history but the 2 urls opened in private tabs are not
+ */
+public class testPrivateBrowsing extends ContentContextMenuTest {
+
+    @Override
+    protected int getTestType() {
+        return TEST_MOCHITEST;
+    }
+
+    public void testPrivateBrowsing() {
+        String bigLinkUrl = getAbsoluteUrl(StringHelper.ROBOCOP_BIG_LINK_URL);
+        String blank1Url = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+        String blank2Url = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+        blockForGeckoReady();
+
+        inputAndLoadUrl(StringHelper.ABOUT_BLANK_URL);
+
+        addTab(bigLinkUrl, StringHelper.ROBOCOP_BIG_LINK_TITLE, true);
+
+        verifyTabCount(1);
+
+        // Open the link context menu and verify the options
+        verifyContextMenuItems(StringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB);
+
+        // Check that "Open Link in New Tab" is not in the menu
+        mAsserter.ok(!mSolo.searchText(StringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]), "Checking that 'Open Link in New Tab' is not displayed in the context menu", "'Open Link in New Tab' is not displayed in the context menu");
+
+        // Open the link in a new private tab and check that it is private
+        Actions.EventExpecter privateTabEventExpector = mActions.expectGeckoEvent("Tab:Added");
+        mSolo.clickOnText(StringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB[0]);
+        String eventData = privateTabEventExpector.blockForEventData();
+        privateTabEventExpector.unregisterListener();
+
+        mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
+        verifyTabCount(2);
+
+        // Open a normal tab to check later that it was registered in the Firefox Browser History
+        addTab(blank2Url, StringHelper.ROBOCOP_BLANK_PAGE_02_TITLE, false);
+        verifyTabCount(2);
+
+        // Get the history list and check that the links open in private browsing are not saved
+        ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
+        mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history");
+        mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history");
+        mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history");
+    }
+
+    private boolean isTabPrivate(String eventData) {
+        try {
+            JSONObject data = new JSONObject(eventData);
+            return data.getBoolean("isPrivate");
+        } catch (JSONException e) {
+            mAsserter.ok(false, "Error parsing the event data", e.toString());
+            return false;
+        }
+    }
+}
--- a/modules/libjar/nsIZipReader.idl
+++ b/modules/libjar/nsIZipReader.idl
@@ -6,17 +6,17 @@
 
 #include "nsISupports.idl"
 
 interface nsIUTF8StringEnumerator;
 interface nsIInputStream;
 interface nsIFile;
 interface nsICertificatePrincipal;
 
-[scriptable, uuid(e1c028bc-c478-11da-95a8-00e08161165f)]
+[scriptable, uuid(fad6f72f-13d8-4e26-9173-53007a4afe71)]
 interface nsIZipEntry : nsISupports
 {
     /**
      * The type of compression used for the item.  The possible values and
      * their meanings are defined in the zip file specification at
      * http://www.pkware.com/business_and_developers/developer/appnote/
      */
     readonly attribute unsigned short   compression;
@@ -46,16 +46,20 @@ interface nsIZipEntry : nsISupports
      * entry represents a directory within the zip file which has no
      * corresponding entry within the zip file.  For example, the entry for the
      * directory foo/ in a zip containing exactly one entry for foo/bar.txt
      * is synthetic.  If the zip file contains an actual entry for a directory,
      * this attribute will be false for the nsIZipEntry for that directory.
      * It is impossible for a file to be synthetic.
      */
     readonly attribute boolean          isSynthetic;
+    /**
+     * The UNIX style file permissions of this item.
+     */
+    readonly attribute unsigned long    permissions;
 };
 
 [scriptable, uuid(38d6d07a-8a58-4fe7-be8b-ef6472fa83ff)]
 interface nsIZipReader : nsISupports
 {
     /**
      * Opens a zip file for reading.
      * It is allowed to open with another file, 
--- a/modules/libjar/nsJAR.cpp
+++ b/modules/libjar/nsJAR.cpp
@@ -906,16 +906,17 @@ nsJAREnumerator::GetNext(nsACString& aRe
 NS_IMPL_ISUPPORTS1(nsJARItem, nsIZipEntry)
 
 nsJARItem::nsJARItem(nsZipItem* aZipItem)
     : mSize(aZipItem->Size()),
       mRealsize(aZipItem->RealSize()),
       mCrc32(aZipItem->CRC32()),
       mLastModTime(aZipItem->LastModTime()),
       mCompression(aZipItem->Compression()),
+      mPermissions(aZipItem->Mode()),
       mIsDirectory(aZipItem->IsDirectory()),
       mIsSynthetic(aZipItem->isSynthetic)
 {
 }
 
 //------------------------------------------
 // nsJARItem::GetCompression
 //------------------------------------------
@@ -995,16 +996,28 @@ NS_IMETHODIMP
 nsJARItem::GetLastModifiedTime(PRTime* aLastModTime)
 {
     NS_ENSURE_ARG_POINTER(aLastModTime);
 
     *aLastModTime = mLastModTime;
     return NS_OK;
 }
 
+//------------------------------------------
+// nsJARItem::GetPermissions
+//------------------------------------------
+NS_IMETHODIMP
+nsJARItem::GetPermissions(uint32_t* aPermissions)
+{
+    NS_ENSURE_ARG_POINTER(aPermissions);
+
+    *aPermissions = mPermissions;
+    return NS_OK;
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // nsIZipReaderCache
 
 NS_IMPL_ISUPPORTS3(nsZipReaderCache, nsIZipReaderCache, nsIObserver, nsISupportsWeakReference)
 
 nsZipReaderCache::nsZipReaderCache()
   : mLock("nsZipReaderCache.mLock")
   , mZips(16)
--- a/modules/libjar/nsJAR.h
+++ b/modules/libjar/nsJAR.h
@@ -137,16 +137,17 @@ public:
     virtual ~nsJARItem() {}
 
 private:
     uint32_t     mSize;             /* size in original file */
     uint32_t     mRealsize;         /* inflated size */
     uint32_t     mCrc32;
     PRTime       mLastModTime;
     uint16_t     mCompression;
+    uint32_t     mPermissions;
     bool mIsDirectory; 
     bool mIsSynthetic;
 };
 
 /**
  * nsJAREnumerator
  *
  * Enumerates a list of files in a zip archive 
--- a/modules/libjar/zipwriter/src/nsZipHeader.cpp
+++ b/modules/libjar/zipwriter/src/nsZipHeader.cpp
@@ -122,16 +122,26 @@ NS_IMETHODIMP nsZipHeader::GetLastModifi
 NS_IMETHODIMP nsZipHeader::GetIsSynthetic(bool *aIsSynthetic)
 {
     NS_ASSERTION(mInited, "Not initalised");
 
     *aIsSynthetic = false;
     return NS_OK;
 }
 
+/* readonly attribute unsigned long permissions; */
+NS_IMETHODIMP nsZipHeader::GetPermissions(uint32_t *aPermissions)
+{
+    NS_ASSERTION(mInited, "Not initalised");
+
+    // Always give user read access at least, this matches nsIZipReader's behaviour
+    *aPermissions = ((mEAttr >> 16) & 0xfff | 0x100);
+    return NS_OK;
+}
+
 void nsZipHeader::Init(const nsACString & aPath, PRTime aDate, uint32_t aAttr,
                        uint32_t aOffset)
 {
     NS_ASSERTION(!mInited, "Already initalised");
 
     PRExplodedTime time;
     PR_ExplodeTime(aDate, PR_LocalTimeParameters, &time);
 
--- a/modules/libjar/zipwriter/test/unit/test_zippermissions.js
+++ b/modules/libjar/zipwriter/test/unit/test_zippermissions.js
@@ -5,17 +5,17 @@
 
 const DATA = "ZIP WRITER TEST DATA";
 
 var TESTS = [];
 
 function build_tests() {
   var id = 0;
 
-  // Minimum mode is 0400
+  // Minimum mode is 0o400
   for (let u = 4; u <= 7; u++) {
     for (let g = 0; g <= 7; g++) {
       for (let o = 0; o <= 7; o++) {
         TESTS[id] = {
           name: "test" + u + g + o,
           permission: (u << 6) + (g << 3) + o
         };
         id++;
@@ -48,26 +48,36 @@ function run_test() {
     // This reduces the coverage of the test but there isn't much we can do
     var perm = file.permissions & 0xfff;
     if (TESTS[i].permission != perm) {
       dump("File permissions for " + TESTS[i].name + " were " + perm.toString(8) + "\n");
       TESTS[i].permission = perm;
     }
 
     zipW.addEntryFile(TESTS[i].name, Ci.nsIZipWriter.COMPRESSION_NONE, file, false);
-    file.permissions = 0600;
+    do_check_eq(zipW.getEntry(TESTS[i].name).permissions, TESTS[i].permission | 0o400);
+    file.permissions = 0o600;
     file.remove(true);
   }
   zipW.close();
 
+  zipW.open(tmpFile, PR_RDWR);
+  for (let i = 0; i < TESTS.length; i++) {
+    dump("Testing zipwriter file permissions for " + TESTS[i].name + "\n");
+    do_check_eq(zipW.getEntry(TESTS[i].name).permissions, TESTS[i].permission | 0o400);
+  }
+  zipW.close();
+
   var zipR = new ZipReader(tmpFile);
   for (let i = 0; i < TESTS.length; i++) {
+    dump("Testing zipreader file permissions for " + TESTS[i].name + "\n");
+    do_check_eq(zipR.getEntry(TESTS[i].name).permissions, TESTS[i].permission | 0o400);
+    dump("Testing extracted file permissions for " + TESTS[i].name + "\n");
     zipR.extract(TESTS[i].name, file);
-    dump("Testing file permissions for " + TESTS[i].name + "\n");
     do_check_eq(file.permissions & 0xfff, TESTS[i].permission);
     do_check_false(file.isDirectory());
-    file.permissions = 0600;
+    file.permissions = 0o600;
     file.remove(true);
   }
   zipR.close();
 
   tmp.remove(true);
 }
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -55,16 +55,17 @@ user_pref("font.size.inflation.minTwips"
 user_pref("extensions.enabledScopes", 5);
 // Disable metadata caching for installed add-ons by default
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable intalling any distribution add-ons
 user_pref("extensions.installDistroAddons", false);
 
 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
 user_pref("geo.wifi.testing", true);
+user_pref("geo.wifi.logging.enabled", true);
 user_pref("geo.ignore.location_filter", true);
 
 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
 
 // Make url-classifier updates so rare that they won't affect tests
 user_pref("urlclassifier.updateinterval", 172800);
 // Point the url-classifier to the local testing server for fast failures
 user_pref("browser.safebrowsing.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -203,19 +203,26 @@ let Scheduler = {
           options.outExecutionDuration = durationMs;
         }
         return data.ok;
       },
       function onError(error) {
         // Decode any serialized error
         if (error instanceof PromiseWorker.WorkerError) {
           throw OS.File.Error.fromMsg(error.data);
-        } else {
-          throw error;
         }
+        // Extract something meaningful from WorkerErrorEvent
+        if (typeof error == "object" && error && error.constructor.name == "WorkerErrorEvent") {
+          let message = error.message;
+          if (message == "uncaught exception: [object StopIteration]") {
+            throw StopIteration;
+          }
+          throw new Error(message, error.filename, error.lineno);
+        }
+        throw error;
       }
     );
   }
 };
 
 const PREF_OSFILE_LOG = "toolkit.osfile.log";
 const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect";
 
@@ -960,24 +967,21 @@ DirectoryIterator.prototype = {
     if (this._isClosed) {
       return this._itmsg;
     }
     let self = this;
     let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
     promise = promise.then(
       DirectoryIterator.Entry.fromMsg,
       function onReject(reason) {
-        // If the exception is |StopIteration| (which we may determine only
-        // from its message...) we need to stop the iteration.
-        if (!(reason instanceof WorkerErrorEvent && reason.message == "uncaught exception: [object StopIteration]")) {
-          // Any exception other than StopIteration should be propagated as such
-          throw reason;
+        if (reason == StopIteration) {
+          self.close();
+          throw StopIteration;
         }
-        self.close();
-        throw StopIteration;
+        throw reason;
       });
     return promise;
   },
   /**
    * Close the iterator
    */
   close: function close() {
     if (this._isClosed) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_exception.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  do_test_pending();
+  run_next_test();
+}
+
+add_task(function test_typeerror() {
+  let exn;
+  try {
+    let fd = yield OS.File.open("/tmp", {no_such_key: 1});
+    do_print("Fd: " + fd);
+  } catch (ex) {
+    exn = ex;
+  }
+  do_print("Exception: " + exn);
+  do_check_true(typeof exn == "object");
+  do_check_true("name" in exn);
+  do_check_true(exn.message.indexOf("TypeError") != -1);
+});
+
+add_task(function() {
+  do_test_finished();
+});
--- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
@@ -3,10 +3,11 @@ head =
 tail =
 
 [test_osfile_closed.js]
 [test_path.js]
 [test_osfile_async.js]
 [test_profiledir.js]
 [test_logging.js]
 [test_creationDate.js]
+[test_exception.js]
 [test_path_constants.js]
 [test_removeDir.js]
--- a/toolkit/components/startup/nsAppStartup.cpp
+++ b/toolkit/components/startup/nsAppStartup.cpp
@@ -132,16 +132,17 @@ uint64_t ComputeAbsoluteTimestamp(PRTime
 //
 // nsAppStartup
 //
 
 nsAppStartup::nsAppStartup() :
   mConsiderQuitStopper(0),
   mRunning(false),
   mShuttingDown(false),
+  mStartingUp(true),
   mAttemptingQuit(false),
   mRestart(false),
   mInterrupted(false),
   mIsSafeModeNecessary(false),
   mStartupCrashTrackingEnded(false)
 { }
 
 
@@ -512,16 +513,33 @@ nsAppStartup::ExitLastWindowClosingSurvi
 NS_IMETHODIMP
 nsAppStartup::GetShuttingDown(bool *aResult)
 {
   *aResult = mShuttingDown;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsAppStartup::GetStartingUp(bool *aResult)
+{
+  *aResult = mStartingUp;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::DoneStartingUp()
+{
+  // This must be called once at most
+  MOZ_ASSERT(mStartingUp);
+
+  mStartingUp = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsAppStartup::GetRestarting(bool *aResult)
 {
   *aResult = mRestart;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAppStartup::GetWasRestarted(bool *aResult)
--- a/toolkit/components/startup/nsAppStartup.h
+++ b/toolkit/components/startup/nsAppStartup.h
@@ -51,16 +51,17 @@ private:
 
   friend class nsAppExitEvent;
 
   nsCOMPtr<nsIAppShell> mAppShell;
 
   int32_t      mConsiderQuitStopper; // if > 0, Quit(eConsiderQuit) fails
   bool mRunning;        // Have we started the main event loop?
   bool mShuttingDown;   // Quit method reentrancy check
+  bool mStartingUp;     // Have we passed final-ui-startup?
   bool mAttemptingQuit; // Quit(eAttemptQuit) still trying
   bool mRestart;        // Quit(eRestart)
   bool mInterrupted;    // Was startup interrupted by an interactive prompt?
   bool mIsSafeModeNecessary;       // Whether safe mode is necessary
   bool mStartupCrashTrackingEnded; // Whether startup crash tracking has already ended
 
 #if defined(XP_WIN)
   //Interaction with OS-provided profiling probes
--- a/toolkit/components/startup/public/nsIAppStartup.idl
+++ b/toolkit/components/startup/public/nsIAppStartup.idl
@@ -2,17 +2,17 @@
 /* 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/. */
 
 #include "nsISupports.idl"
 
 interface nsICmdLineService;
 
-[scriptable, uuid(744d6ec0-115f-11e3-9c94-68fd99890b3c)]
+[scriptable, uuid(9edef217-e664-4938-85a7-2fe84baa1755)]
 interface nsIAppStartup : nsISupports
 {
     /**
      * Create the hidden window.
      */
     void createHiddenWindow();
 
     /**
@@ -131,16 +131,30 @@ interface nsIAppStartup : nsISupports
     void quit(in uint32_t aMode);
 
     /**
      * True if the application is in the process of shutting down.
      */
     readonly attribute boolean shuttingDown;
 
     /**
+     * True if the application is in the process of starting up.
+     *
+     * Startup is complete once all observers of final-ui-startup have returned.
+     */
+    readonly attribute boolean startingUp;
+
+    /**
+     * Mark the startup as completed.
+     *
+     * Called at the end of startup by nsAppRunner.
+     */
+    [noscript] void doneStartingUp();
+
+    /**
      * True if the application is being restarted
      */
     readonly attribute boolean restarting;
 
     /**
      * True if this is the startup following restart, i.e. if the application
      * was restarted using quit(eRestart*).
      */
--- a/toolkit/components/telemetry/TelemetryPing.js
+++ b/toolkit/components/telemetry/TelemetryPing.js
@@ -57,16 +57,18 @@ function getLocale() {
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyServiceGetter(this, "idleService",
                                    "@mozilla.org/widget/idleservice;1",
                                    "nsIIdleService");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+                                  "resource://gre/modules/AddonManager.jsm");
 
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 /**
@@ -153,19 +155,17 @@ TelemetryPing.prototype = {
     // Look for app-specific timestamps
     var appTimestamps = {};
     try {
       let o = {};
       Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o);
       appTimestamps = o.TelemetryTimestamps.get();
     } catch (ex) {}
     try {
-      let o = {};
-      Cu.import("resource://gre/modules/AddonManager.jsm", o);
-      ret.addonManager = o.AddonManagerPrivate.getSimpleMeasures();
+      ret.addonManager = AddonManagerPrivate.getSimpleMeasures();
     } catch (ex) {}
 
     if (si.process) {
       for each (let field in Object.keys(si)) {
         if (field == "process")
           continue;
         ret[field] = si[field] - si.process
       }
@@ -540,16 +540,17 @@ TelemetryPing.prototype = {
     let payloadObj = {
       ver: PAYLOAD_VERSION,
       simpleMeasurements: simpleMeasurements,
       histograms: this.getHistograms(Telemetry.histogramSnapshots),
       slowSQL: Telemetry.slowSQL,
       chromeHangs: Telemetry.chromeHangs,
       lateWrites: Telemetry.lateWrites,
       addonHistograms: this.getAddonHistograms(),
+      addonDetails: AddonManagerPrivate.getTelemetryDetails(),
       info: info
     };
 
     if (Object.keys(this._slowSQLStartup.mainThread).length
       || Object.keys(this._slowSQLStartup.otherThreads).length) {
       payloadObj.slowSQLStartup = this._slowSQLStartup;
     }
 
@@ -684,17 +685,17 @@ TelemetryPing.prototype = {
     payloadStream.data = this.gzipCompressString(JSON.stringify(ping.payload));
     request.send(payloadStream);
   },
 
   gzipCompressString: function gzipCompressString(string) {
     let observer = {
       buffer: "",
       onStreamComplete: function(loader, context, status, length, result) {
-	this.buffer = String.fromCharCode.apply(this, result);
+        this.buffer = String.fromCharCode.apply(this, result);
       }
     };
 
     let scs = Cc["@mozilla.org/streamConverters;1"]
               .getService(Ci.nsIStreamConverterService);
     let listener = Cc["@mozilla.org/network/stream-loader;1"]
                   .createInstance(Ci.nsIStreamLoader);
     listener.init(observer);
--- a/toolkit/content/aboutTelemetry.js
+++ b/toolkit/content/aboutTelemetry.js
@@ -518,77 +518,130 @@ let Histogram = {
       // Add bucket label
       barDiv.appendChild(document.createTextNode(label));
 
       aDiv.appendChild(barDiv);
     }
   }
 };
 
-let KeyValueTable = {
+/*
+ * Helper function to render JS objects with white space between top level elements
+ * so that they look better in the browser
+ * @param   aObject JavaScript object or array to render
+ * @return  String
+ */
+function RenderObject(aObject) {
+  let output = "";
+  if (Array.isArray(aObject)) {
+    if (aObject.length == 0) {
+      return "[]";
+    }
+    output = "[" + JSON.stringify(aObject[0]);
+    for (let i = 1; i < aObject.length; i++) {
+      output += ", " + JSON.stringify(aObject[i]);
+    }
+    return output + "]";
+  }
+  let keys = Object.keys(aObject);
+  if (keys.length == 0) {
+    return "{}";
+  }
+  output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]);
+  for (let i = 1; i < keys.length; i++) {
+    output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]);
+  }
+  return output + "}";
+};
 
-  keysHeader: bundle.GetStringFromName("keysHeader"),
-
-  valuesHeader: bundle.GetStringFromName("valuesHeader"),
-
+let KeyValueTable = {
   /**
-   * Fill out a 2-column table with keys and values
+   * Returns a 2-column table with keys and values
+   * @param aMeasurements Each key in this JS object is rendered as a row in
+   *                      the table with its corresponding value
+   * @param aKeysLabel    Column header for the keys column
+   * @param aValuesLabel  Column header for the values column
    */
-  render: function KeyValueTable_render(aTableID, aMeasurements) {
-    let table = document.getElementById(aTableID);
-    this.renderHeader(table);
+  render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
+    let table = document.createElement("table");
+    this.renderHeader(table, aKeysLabel, aValuesLabel);
     this.renderBody(table, aMeasurements);
+    return table;
   },
 
   /**
    * Create the table header
    * Tabs & newlines added to cells to make it easier to copy-paste.
    *
    * @param aTable Table element
+   * @param aKeysLabel    Column header for the keys column
+   * @param aValuesLabel  Column header for the values column
    */
-  renderHeader: function KeyValueTable_renderHeader(aTable) {
+  renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
     let headerRow = document.createElement("tr");
     aTable.appendChild(headerRow);
 
     let keysColumn = document.createElement("th");
-    keysColumn.appendChild(document.createTextNode(this.keysHeader + "\t"));
+    keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
     let valuesColumn = document.createElement("th");
-    valuesColumn.appendChild(document.createTextNode(this.valuesHeader + "\n"));
+    valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
 
     headerRow.appendChild(keysColumn);
     headerRow.appendChild(valuesColumn);
   },
 
   /**
    * Create the table body
    * Tabs & newlines added to cells to make it easier to copy-paste.
    *
    * @param aTable Table element
    * @param aMeasurements Key/value map
    */
   renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
     for (let [key, value] of Iterator(aMeasurements)) {
       if (typeof value == "object") {
-        value = JSON.stringify(value);
+        value = RenderObject(value);
       }
 
       let newRow = document.createElement("tr");
       aTable.appendChild(newRow);
 
       let keyField = document.createElement("td");
       keyField.appendChild(document.createTextNode(key + "\t"));
       newRow.appendChild(keyField);
 
       let valueField = document.createElement("td");
       valueField.appendChild(document.createTextNode(value + "\n"));
       newRow.appendChild(valueField);
     }
   }
 };
 
+let AddonDetails = {
+  tableIDTitle: bundle.GetStringFromName("addonTableID"),
+  tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
+
+  /**
+   * Render the addon details section as a series of headers followed by key/value tables
+   * @param aSections Object containing the details sections to render
+   */
+  render: function AddonDetails_render(aSections) {
+    let addonSection = document.getElementById("addon-details");
+    for (let provider in aSections) {
+      let providerSection = document.createElement("h2");
+      let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
+      providerSection.appendChild(document.createTextNode(titleText));
+      addonSection.appendChild(providerSection);
+      addonSection.appendChild(
+        KeyValueTable.render(aSections[provider],
+                             this.tableIDTitle, this.tableDetailsTitle));
+    }
+  }
+};
+
 /**
  * Helper function for showing "No data collected" message for a section
  *
  * @param aSectionID ID of the section element that needs to be changed
  */
 function showEmptySectionMessage(aSectionID) {
   let sectionElement = document.getElementById(aSectionID);
 
@@ -808,27 +861,41 @@ function sortStartupMilestones(aSimpleMe
   }
 
   return result;
 }
 
 function displayPingData() {
   let ping = TelemetryPing.getPayload();
 
+  let keysHeader = bundle.GetStringFromName("keysHeader");
+  let valuesHeader = bundle.GetStringFromName("valuesHeader");
+
   // Show simple measurements
   let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements);
   if (Object.keys(simpleMeasurements).length) {
-    KeyValueTable.render("simple-measurements-table", simpleMeasurements);
+    let simpleSection = document.getElementById("simple-measurements");
+    simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
+                                                   keysHeader, valuesHeader));
   } else {
     showEmptySectionMessage("simple-measurements-section");
   }
 
   LateWritesSingleton.renderLateWrites(ping.lateWrites);
 
   // Show basic system info gathered
   if (Object.keys(ping.info).length) {
-    KeyValueTable.render("system-info-table", ping.info);
+    let infoSection = document.getElementById("system-info");
+    infoSection.appendChild(KeyValueTable.render(ping.info,
+                                                 keysHeader, valuesHeader));
   } else {
     showEmptySectionMessage("system-info-section");
   }
+
+  let addonDetails = ping.addonDetails;
+  if (Object.keys(addonDetails).length) {
+    AddonDetails.render(addonDetails);
+  } else {
+    showEmptySectionMessage("addon-details-section");
+  }
 }
 
 window.addEventListener("load", onLoad, false);
--- a/toolkit/content/aboutTelemetry.xhtml
+++ b/toolkit/content/aboutTelemetry.xhtml
@@ -70,18 +70,16 @@
     </section>
 
     <section id="simple-measurements-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.simpleMeasurementsSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
       <div id="simple-measurements" class="data hidden">
-        <table id="simple-measurements-table">
-        </table>
       </div>
     </section>
 
     <section id="late-writes-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.lateWritesSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
@@ -96,18 +94,25 @@
     </section>
 
     <section id="system-info-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.systemInfoSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
       <div id="system-info" class="data hidden">
-        <table id="system-info-table">
-        </table>
+      </div>
+    </section>
+
+    <section id="addon-details-section" class="data-section">
+      <h1 class="section-name">&aboutTelemetry.addonDetailsSection;</h1>
+      <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
+      <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
+      <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
+      <div id="addon-details" class="data hidden">
       </div>
     </section>
 
     <section id="addon-histograms-section" class="data-section">
       <h1 class="section-name">&aboutTelemetry.addonHistogramsSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggleOn;</span>
       <span class="toggle-caption hidden">&aboutTelemetry.toggleOff;</span>
       <span class="empty-caption hidden">&aboutTelemetry.emptySection;</span>
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -55,16 +55,17 @@ var BuiltinProvider = {
         "": "resource://gre/modules/commonjs/",
         "main": "resource:///modules/devtools/main.js",
         "devtools": "resource:///modules/devtools",
         "devtools/server": "resource://gre/modules/devtools/server",
         "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
         "devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js",
         "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
         "devtools/css-color": "resource://gre/modules/devtools/css-color",
+        "devtools/touch-events": "resource://gre/modules/devtools/touch-events",
         "devtools/client": "resource://gre/modules/devtools/client",
 
         "escodegen": "resource://gre/modules/devtools/escodegen",
         "estraverse": "resource://gre/modules/devtools/escodegen/estraverse",
 
         // Allow access to xpcshell test items from the loader.
         "xpcshell-test": "resource://test"
       },
@@ -97,33 +98,34 @@ var SrcdirProvider = {
     let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools");
     let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js"));
     let devtoolsURI = this.fileURI(devtoolsDir);
     let serverURI = this.fileURI(OS.Path.join(toolkitDir, "server"));
     let webconsoleURI = this.fileURI(OS.Path.join(toolkitDir, "webconsole"));
     let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js"));
     let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic"));
     let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color"));
+    let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events"));
     let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
     let escodegenURI = this.fileURI(OS.Path.join(toolkitDir, "escodegen"));
     let estraverseURI = this.fileURI(OS.Path.join(toolkitDir, "escodegen", "estraverse"));
     this.loader = new loader.Loader({
       modules: {
         "toolkit/loader": loader,
         "source-map": SourceMap,
       },
       paths: {
         "": "resource://gre/modules/commonjs/",
         "main": mainURI,
         "devtools": devtoolsURI,
         "devtools/server": serverURI,
         "devtools/toolkit/webconsole": webconsoleURI,
         "devtools/app-actor-front": appActorURI,
         "devtools/styleinspector/css-logic": cssLogicURI,
-        "devtools/css-color": cssColorURI,
+        "devtools/touch-events": touchEventsURI,
         "devtools/client": clientURI,
         "escodegen": escodegenURI,
         "estraverse": estraverseURI
       },
       globals: loaderGlobals
     });
 
     return this._writeManifest(devtoolsDir).then(null, Cu.reportError);
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -1320,21 +1320,23 @@ var WalkerActor = protocol.ActorClass({
   /**
    * Return the first node in the document that matches the given selector.
    * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
    *
    * @param NodeActor baseNode
    * @param string selector
    */
   querySelector: method(function(baseNode, selector) {
+    if (!baseNode) {
+      return {}
+    };
     let node = baseNode.rawNode.querySelector(selector);
 
     if (!node) {
-      return {
-      }
+      return {}
     };
 
     let node = this._ref(node);
     let newParents = this.ensurePathToRoot(node);
     return {
       node: node,
       newParents: [parent for (parent of newParents)]
     }
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -131,22 +131,21 @@ var PageStyleActor = protocol.ActorClass
    *       value: "property-value",
    *       priority: "!important" <optional>
    *       matched: <true if there are matched selectors for this value>
    *     },
    *     ...
    *   }
    */
   getComputed: method(function(node, options) {
-    let win = node.rawNode.ownerDocument.defaultView;
     let ret = Object.create(null);
 
     this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
     this.cssLogic.highlight(node.rawNode);
-    let computed = this.cssLogic._computedStyle;
+    let computed = this.cssLogic._computedStyle || [];
 
     Array.prototype.forEach.call(computed, name => {
       let matched = undefined;
       ret[name] = {
         value: computed.getPropertyValue(name),
         priority: computed.getPropertyPriority(name) || undefined
       };
     });
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -376,18 +376,29 @@ CssLogic.prototype = {
    *
    * @param {function} aCallback the function you want executed for each of the
    * CssSheet objects cached.
    * @param {object} aScope the scope you want for the callback function. aScope
    * will be the this object when aCallback executes.
    */
   forEachSheet: function CssLogic_forEachSheet(aCallback, aScope)
   {
-    for each (let sheet in this._sheets) {
-      sheet.forEach(aCallback, aScope);
+    for each (let sheets in this._sheets) {
+      for (let i = 0; i < sheets.length; i ++) {
+        // We take this as an opportunity to clean dead sheets
+        try {
+          let sheet = sheets[i];
+          sheet.domSheet; // If accessing domSheet raises an exception, then the
+          // style sheet is a dead object
+          aCallback.call(aScope, sheet, i, sheets);
+        } catch (e) {
+          sheets.splice(i, 1);
+          i --;
+        }
+      }
     }
   },
 
   /**
    * Process *some* cached stylesheets in the document using your callback. The
    * callback function should return true in order to halt processing.
    *
    * @param {function} aCallback the function you want executed for some of the
@@ -1003,18 +1014,18 @@ CssSheet.prototype = {
   /**
    * Retrieve the number of rules in this stylesheet.
    *
    * @return {number} the number of nsIDOMCSSRule objects in this stylesheet.
    */
   get ruleCount()
   {
     return this._ruleCount > -1 ?
-        this._ruleCount :
-        this.domSheet.cssRules.length;
+      this._ruleCount :
+      this.domSheet.cssRules.length;
   },
 
   /**
    * Retrieve a CssRule object for the given CSSStyleRule. The CssRule object is
    * cached, such that subsequent retrievals return the same CssRule object for
    * the same CSSStyleRule object.
    *
    * @param {CSSStyleRule} aDomRule the CSSStyleRule object for which you want a
@@ -1112,17 +1123,17 @@ CssSheet.prototype = {
       }
     }
     return Array.prototype.some.call(domRules, _iterator, this);
   },
 
   toString: function CssSheet_toString()
   {
     return "CssSheet[" + this.shortSource + "]";
-  },
+  }
 };
 
 /**
  * Information about a single CSSStyleRule.
  *
  * @param {CSSSheet|null} aCssSheet the CssSheet object of the stylesheet that
  * holds the CSSStyleRule. If the rule comes from element.style, set this
  * argument to null.
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/touch-events.js
@@ -0,0 +1,185 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let {CC, Cc, Ci, Cu, Cr} = require('chrome');
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+let handlerCount = 0;
+
+let orig_w3c_touch_events = Services.prefs.getIntPref('dom.w3c_touch_events.enabled');
+
+// =================== Touch ====================
+// Simulate touch events on desktop
+function TouchEventHandler (window) {
+  let contextMenuTimeout = 0;
+
+  // This guard is used to not re-enter the events processing loop for
+  // self dispatched events
+  let ignoreEvents = false;
+
+  let threshold = 25;
+  try {
+    threshold = Services.prefs.getIntPref('ui.dragThresholdX');
+  } catch(e) {}
+
+  let delay = 500;
+  try {
+    delay = Services.prefs.getIntPref('ui.click_hold_context_menus.delay');
+  } catch(e) {}
+
+  let TouchEventHandler = {
+    enabled: false,
+    events: ['mousedown', 'mousemove', 'mouseup', 'click'],
+    start: function teh_start() {
+      let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1;
+      handlerCount++;
+      Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1);
+      this.enabled = true;
+      this.events.forEach((function(evt) {
+        window.addEventListener(evt, this, true);
+      }).bind(this));
+      return isReloadNeeded;
+    },
+    stop: function teh_stop() {
+      handlerCount--;
+      if (handlerCount == 0)
+        Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events);
+      this.enabled = false;
+      this.events.forEach((function(evt) {
+        window.removeEventListener(evt, this, true);
+      }).bind(this));
+    },
+    handleEvent: function teh_handleEvent(evt) {
+      if (evt.button || ignoreEvents ||
+          evt.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN)
+        return;
+
+      // The gaia system window use an hybrid system even on the device which is
+      // a mix of mouse/touch events. So let's not cancel *all* mouse events
+      // if it is the current target.
+      let content = evt.target.ownerDocument.defaultView;
+      let isSystemWindow = content.location.toString().indexOf("system.gaiamobile.org") != -1;
+
+      let eventTarget = this.target;
+      let type = '';
+      switch (evt.type) {
+        case 'mousedown':
+          this.target = evt.target;
+
+          contextMenuTimeout =
+            this.sendContextMenu(evt.target, evt.pageX, evt.pageY, delay);
+
+          this.cancelClick = false;
+          this.startX = evt.pageX;
+          this.startY = evt.pageY;
+
+          // Capture events so if a different window show up the events
+          // won't be dispatched to something else.
+          evt.target.setCapture(false);
+
+          type = 'touchstart';
+          break;
+
+        case 'mousemove':
+          if (!eventTarget)
+            return;
+
+          if (!this.cancelClick) {
+            if (Math.abs(this.startX - evt.pageX) > threshold ||
+                Math.abs(this.startY - evt.pageY) > threshold) {
+              this.cancelClick = true;
+              content.clearTimeout(contextMenuTimeout);
+            }
+          }
+
+          type = 'touchmove';
+          break;
+
+        case 'mouseup':
+          if (!eventTarget)
+            return;
+          this.target = null;
+
+          content.clearTimeout(contextMenuTimeout);
+          type = 'touchend';
+          break;
+
+        case 'click':
+          // Mouse events has been cancelled so dispatch a sequence
+          // of events to where touchend has been fired
+          evt.preventDefault();
+          evt.stopImmediatePropagation();
+
+          if (this.cancelClick)
+            return;
+
+          ignoreEvents = true;
+          content.setTimeout(function dispatchMouseEvents(self) {
+            self.fireMouseEvent('mousedown', evt);
+            self.fireMouseEvent('mousemove', evt);
+            self.fireMouseEvent('mouseup', evt);
+            ignoreEvents = false;
+         }, 0, this);
+
+          return;
+      }
+
+      let target = eventTarget || this.target;
+      if (target && type) {
+        this.sendTouchEvent(evt, target, type);
+      }
+
+      if (!isSystemWindow) {
+        evt.preventDefault();
+        evt.stopImmediatePropagation();
+      }
+    },
+    fireMouseEvent: function teh_fireMouseEvent(type, evt)  {
+      let content = evt.target.ownerDocument.defaultView;
+      var utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+      utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true);
+    },
+    sendContextMenu: function teh_sendContextMenu(target, x, y, delay) {
+      let doc = target.ownerDocument;
+      let evt = doc.createEvent('MouseEvent');
+      evt.initMouseEvent('contextmenu', true, true, doc.defaultView,
+                         0, x, y, x, y, false, false, false, false,
+                         0, null);
+
+      let content = target.ownerDocument.defaultView;
+      let timeout = content.setTimeout((function contextMenu() {
+        target.dispatchEvent(evt);
+        this.cancelClick = true;
+      }).bind(this), delay);
+
+      return timeout;
+    },
+    sendTouchEvent: function teh_sendTouchEvent(evt, target, name) {
+      let document = target.ownerDocument;
+      let content = document.defaultView;
+
+      let touchEvent = document.createEvent('touchevent');
+      let point = document.createTouch(content, target, 0,
+                                       evt.pageX, evt.pageY,
+                                       evt.screenX, evt.screenY,
+                                       evt.clientX, evt.clientY,
+                                       1, 1, 0, 0);
+      let touches = document.createTouchList(point);
+      let targetTouches = touches;
+      let changedTouches = touches;
+      touchEvent.initTouchEvent(name, true, true, content, 0,
+                                false, false, false, false,
+                                touches, targetTouches, changedTouches);
+      target.dispatchEvent(touchEvent);
+      return touchEvent;
+    }
+  };
+
+  return TouchEventHandler;
+}
+
+exports.TouchEventHandler = TouchEventHandler;
--- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd
+++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd
@@ -23,16 +23,20 @@
 <!ENTITY aboutTelemetry.histogramsSection "
   Histograms
 ">
 
 <!ENTITY aboutTelemetry.simpleMeasurementsSection "
   Simple Measurements
 ">
 
+<!ENTITY aboutTelemetry.addonDetailsSection "
+  Add-on Details
+">
+
 <!ENTITY aboutTelemetry.lateWritesSection "
   Late Writes
 ">
 
 <!ENTITY aboutTelemetry.systemInfoSection "
   System Information
 ">
 
--- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties
@@ -40,8 +40,16 @@ histogramSum = sum
 
 disableTelemetry = Disable Telemetry
 
 enableTelemetry = Enable Telemetry
 
 keysHeader = Property
 
 valuesHeader = Value
+
+addonTableID = Add-on ID
+
+addonTableDetails = Details
+
+# Note to translators:
+# - The %1$S will be replaced with the name of an Add-on Provider (e.g. "XPI", "Plugin")
+addonProvider = %1$S Provider
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -11,16 +11,18 @@ this.EXPORTED_SYMBOLS = [
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/log4moz.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+                                  "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
                                   "resource://services-common/utils.js");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 
@@ -192,16 +194,17 @@ function OpenedConnection(connection, ba
     };
   }
 
   this._log = logProxy;
 
   this._log.info("Opened");
 
   this._connection = connection;
+  this._connectionIdentifier = basename + " Conn #" + number;
   this._open = true;
 
   this._cachedStatements = new Map();
   this._anonymousStatements = new Map();
   this._anonymousCounter = 0;
 
   // A map from statement index to mozIStoragePendingStatement, to allow for
   // canceling prior to finalizing the mozIStorageStatements.
@@ -277,16 +280,21 @@ OpenedConnection.prototype = Object.free
     if (!this._connection) {
       return Promise.resolve();
     }
 
     this._log.debug("Request to close connection.");
     this._clearIdleShrinkTimer();
     let deferred = Promise.defer();
 
+    AsyncShutdown.profileBeforeChange.addBlocker(
+      "Sqlite.jsm: " + this._connectionIdentifier,
+      deferred.promise
+    );
+
     // We need to take extra care with transactions during shutdown.
     //
     // If we don't have a transaction in progress, we can proceed with shutdown
     // immediately.
     if (!this._inProgressTransaction) {
       this._finalize(deferred);
       return deferred.promise;
     }
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -391,16 +391,19 @@ var gHotfixID = null;
 var AddonManagerInternal = {
   managerListeners: [],
   installListeners: [],
   addonListeners: [],
   typeListeners: [],
   providers: [],
   types: {},
   startupChanges: {},
+  // Store telemetry details per addon provider
+  telemetryDetails: {},
+
 
   // A read-only wrapper around the types dictionary
   typesProxy: Proxy.create({
     getOwnPropertyDescriptor: function typesProxy_getOwnPropertyDescriptor(aName) {
       if (!(aName in AddonManagerInternal.types))
         return undefined;
 
       return {
@@ -453,16 +456,20 @@ var AddonManagerInternal = {
    * them.
    */
   startup: function AMI_startup() {
     if (gStarted)
       return;
 
     this.recordTimestamp("AMI_startup_begin");
 
+    // clear this for xpcshell test restarts
+    for (let provider in this.telemetryDetails)
+      delete this.telemetryDetails[provider];
+
     let appChanged = undefined;
 
     let oldAppVersion = null;
     try {
       oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
       appChanged = Services.appinfo.version != oldAppVersion;
     }
     catch (e) { }
@@ -2187,22 +2194,30 @@ this.AddonManagerPrivate = {
   recordSimpleMeasure: function AMP_recordSimpleMeasure(name, value) {
     this._simpleMeasures[name] = value;
   },
 
   getSimpleMeasures: function AMP_getSimpleMeasures() {
     return this._simpleMeasures;
   },
 
+  getTelemetryDetails: function AMP_getTelemetryDetails() {
+    return AddonManagerInternal.telemetryDetails;
+  },
+
+  setTelemetryDetails: function AMP_setTelemetryDetails(aProvider, aDetails) {
+    AddonManagerInternal.telemetryDetails[aProvider] = aDetails;
+  },
+
   // Start a timer, record a simple measure of the time interval when
   // timer.done() is called
   simpleTimer: function(aName) {
     let startTime = Date.now();
     return {
-      done: () => AddonManagerPrivate.recordSimpleMeasure(aName, Date.now() - startTime)
+      done: () => this.recordSimpleMeasure(aName, Date.now() - startTime)
     };
   }
 };
 
 /**
  * This is the public API that UI and developers should be calling. All methods
  * just forward to AddonManagerInternal.
  */
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -1330,46 +1330,51 @@ function recursiveRemove(aFile) {
   }
   catch (e) {
     ERROR("Failed to remove empty directory " + aFile.path, e);
     throw e;
   }
 }
 
 /**
- * Returns the timestamp of the most recently modified file in a directory,
+ * Returns the timestamp and leaf file name of the most recently modified
+ * entry in a directory,
  * or simply the file's own timestamp if it is not a directory.
  *
  * @param  aFile
  *         A non-null nsIFile object
- * @return Epoch time, as described above. 0 for an empty directory.
+ * @return [File Name, Epoch time], as described above.
  */
 function recursiveLastModifiedTime(aFile) {
   try {
+    let modTime = aFile.lastModifiedTime;
+    let fileName = aFile.leafName;
     if (aFile.isFile())
-      return aFile.lastModifiedTime;
+      return [fileName, modTime];
 
     if (aFile.isDirectory()) {
       let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-      let entry, time;
-      let maxTime = aFile.lastModifiedTime;
+      let entry;
       while ((entry = entries.nextFile)) {
-        time = recursiveLastModifiedTime(entry);
-        maxTime = Math.max(time, maxTime);
+        let [subName, subTime] = recursiveLastModifiedTime(entry);
+        if (subTime > modTime) {
+          modTime = subTime;
+          fileName = subName;
+        }
       }
       entries.close();
-      return maxTime;
+      return [fileName, modTime];
     }
   }
   catch (e) {
     WARN("Problem getting last modified time for " + aFile.path, e);
   }
 
   // If the file is something else, just ignore it.
-  return 0;
+  return ["", 0];
 }
 
 /**
  * Gets a snapshot of directory entries.
  *
  * @param  aDir
  *         Directory to look at
  * @param  aSortEntries
@@ -1538,20 +1543,32 @@ var XPIProvider = {
 
   // True if all of the add-ons found during startup were installed in the
   // application install location
   allAppGlobal: true,
   // A string listing the enabled add-ons for annotating crash reports
   enabledAddons: null,
   // An array of add-on IDs of add-ons that were inactive during startup
   inactiveAddonIDs: [],
-  // Count of unpacked add-ons
-  unpackedAddons: 0,
   // Keep track of startup phases for telemetry
   runPhase: XPI_STARTING,
+  // Keep track of the newest file in each add-on, in case we want to
+  // report it to telemetry.
+  _mostRecentlyModifiedFile: {},
+  // Per-addon telemetry information
+  _telemetryDetails: {},
+
+  /*
+   * Set a value in the telemetry hash for a given ID
+   */
+  setTelemetry: function XPI_setTelemetry(aId, aName, aValue) {
+    if (!this._telemetryDetails[aId])
+      this._telemetryDetails[aId] = {};
+    this._telemetryDetails[aId][aName] = aValue;
+  },
 
   /**
    * Adds or updates a URI mapping for an Addon.id.
    *
    * Mappings should not be removed at any point. This is so that the mappings
    * will be still valid after an add-on gets disabled or uninstalled, as
    * consumers may still have URIs of (leaked) resources they want to map.
    */
@@ -1684,16 +1701,21 @@ var XPIProvider = {
   startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
     LOG("startup");
     this.runPhase = XPI_STARTING;
     this.installs = [];
     this.installLocations = [];
     this.installLocationsByName = {};
     // Hook for tests to detect when saving database at shutdown time fails
     this._shutdownError = null;
+    // Clear this at startup for xpcshell test restarts
+    this._telemetryDetails = {};
+    // Register our details structure with AddonManager
+    AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
+
 
     AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
 
     function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       }
       catch (e) {
@@ -2028,45 +2050,48 @@ var XPIProvider = {
    *         property which contains the add-ons dir/file descriptor and an
    *         mtime property which contains the add-on's last modified time as
    *         the number of milliseconds since the epoch.
    */
   getAddonStates: function XPI_getAddonStates(aLocation) {
     let addonStates = {};
     aLocation.addonLocations.forEach(function(file) {
       let id = aLocation.getIDForLocation(file);
+      let unpacked = 0;
+      let [modFile, modTime] = recursiveLastModifiedTime(file);
       addonStates[id] = {
         descriptor: file.persistentDescriptor,
-        mtime: recursiveLastModifiedTime(file)
+        mtime: modTime
       };
       try {
         // get the install.rdf update time, if any
         file.append(FILE_INSTALL_MANIFEST);
         let rdfTime = file.lastModifiedTime;
         addonStates[id].rdfTime = rdfTime;
-        this.unpackedAddons += 1;
+        unpacked = 1;
       }
       catch (e) { }
+      this._mostRecentlyModifiedFile[id] = modFile;
+      this.setTelemetry(id, "unpacked", unpacked);
     }, this);
 
     return addonStates;
   },
 
   /**
    * Gets an array of install location states which uniquely describes all
    * installed add-ons with the add-on's InstallLocation name and last modified
    * time. This function may be expensive because of the getAddonStates() call.
    *
    * @return an array of add-on states for each install location. Each state
    *         is an object with a name property holding the location's name and
    *         an addons property holding the add-on states for the location
    */
   getInstallLocationStates: function XPI_getInstallLocationStates() {
     let states = [];
-    this.unpackedAddons = 0;
     this.installLocations.forEach(function(aLocation) {
       let addons = aLocation.addonLocations;
       if (addons.length == 0)
         return;
 
       let locationState = {
         name: aLocation.name,
         addons: this.getAddonStates(aLocation)
@@ -3001,21 +3026,16 @@ var XPIProvider = {
       }
 
       return false;
     }
 
     let changed = false;
     let knownLocations = XPIDatabase.getInstallLocations();
 
-    // Gather stats for addon telemetry
-    let modifiedUnpacked = 0;
-    let modifiedExManifest = 0;
-    let modifiedXPI = 0;
-
     // The install locations are iterated in reverse order of priority so when
     // there are multiple add-ons installed with the same ID the one that
     // should be visible is the first one encountered.
     aState.reverse().forEach(function(aSt) {
 
       // We can't include the install location directly in the state as it has
       // to be cached as JSON.
       let installLocation = this.installLocationsByName[aSt.name];
@@ -3040,25 +3060,32 @@ var XPIProvider = {
           if (aOldAddon.id in addonStates) {
             let addonState = addonStates[aOldAddon.id];
             delete addonStates[aOldAddon.id];
 
             // Remember add-ons that were inactive during startup
             if (aOldAddon.visible && !aOldAddon.active)
               XPIProvider.inactiveAddonIDs.push(aOldAddon.id);
 
-            // Check if the add-on is unpacked, and has had other files changed
-            // on disk without the install.rdf manifest being changed
-            if ((addonState.rdfTime) && (aOldAddon.updateDate != addonState.mtime)) {
-              modifiedUnpacked += 1;
-              if (aOldAddon.updateDate >= addonState.rdfTime)
-                modifiedExManifest += 1;
-            }
-            else if (aOldAddon.updateDate != addonState.mtime) {
-              modifiedXPI += 1;
+            // Check if the add-on has been changed outside the XPI provider
+            if (aOldAddon.updateDate != addonState.mtime) {
+              // Is the add-on unpacked?
+              if (addonState.rdfTime) {
+                // Was the addon manifest "install.rdf" modified, or some other file?
+                if (addonState.rdfTime > aOldAddon.updateDate) {
+                  this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1);
+                }
+                else {
+                  this.setTelemetry(aOldAddon.id, "modifiedFile",
+                                    this._mostRecentlyModifiedFile[aOldAddon.id]);
+                }
+              }
+              else {
+                this.setTelemetry(aOldAddon.id, "modifiedXPI", 1);
+              }
             }
 
             // The add-on has changed if the modification time has changed, or
             // we have an updated manifest for it. Also reload the metadata for
             // add-ons in the application directory when the application version
             // has changed
             if (aOldAddon.id in aManifests[installLocation.name] ||
                 aOldAddon.updateDate != addonState.mtime ||
@@ -3102,22 +3129,16 @@ var XPIProvider = {
     // database.
     for (let location of knownLocations) {
       let addons = XPIDatabase.getAddonsInLocation(location);
       addons.forEach(function(aOldAddon) {
         changed = removeMetadata(aOldAddon) || changed;
       }, this);
     }
 
-    // Tell Telemetry what we found
-    AddonManagerPrivate.recordSimpleMeasure("modifiedUnpacked", modifiedUnpacked);
-    if (modifiedUnpacked > 0)
-      AddonManagerPrivate.recordSimpleMeasure("modifiedExceptInstallRDF", modifiedExManifest);
-    AddonManagerPrivate.recordSimpleMeasure("modifiedXPI", modifiedXPI);
-
     // Cache the new install location states
     let cache = JSON.stringify(this.getInstallLocationStates());
     Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
     this.persistBootstrappedAddons();
 
     // Clear out any cached migration data.
     XPIDatabase.migrateData = null;
 
@@ -3258,17 +3279,16 @@ var XPIProvider = {
                                                          aAppChanged,
                                                          aOldAppVersion,
                                                          aOldPlatformVersion);
         }
         catch (e) {
           ERROR("Failed to process extension changes at startup", e);
         }
       }
-      AddonManagerPrivate.recordSimpleMeasure("installedUnpacked", this.unpackedAddons);
 
       if (aAppChanged) {
         // When upgrading the app and using a custom skin make sure it is still
         // compatible otherwise switch back the default
         if (this.currentSkin != this.defaultSkin) {
           let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
           if (!oldSkin || isAddonDisabled(oldSkin))
             this.enableDefaultTheme();
@@ -3968,16 +3988,17 @@ var XPIProvider = {
    *         the params argument
    */
   callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile,
                                                         aMethod, aReason, aExtraParams) {
     // Never call any bootstrap methods in safe mode
     if (Services.appinfo.inSafeMode)
       return;
 
+    let timeStart = new Date();
     if (aMethod == "startup") {
       LOG("Registering manifest for " + aFile.path);
       Components.manager.addBootstrappedManifestLocation(aFile);
     }
 
     try {
       // Load the scope if it hasn't already been loaded
       if (!(aId in this.bootstrapScopes))
@@ -4014,16 +4035,17 @@ var XPIProvider = {
         WARN("Exception running bootstrap method " + aMethod + " on " + aId, e);
       }
     }
     finally {
       if (aMethod == "shutdown") {
         LOG("Removing manifest for " + aFile.path);
         Components.manager.removeBootstrappedManifestLocation(aFile);
       }
+      this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart);
     }
   },
 
   /**
    * Updates the disabled state for an add-on. Its appDisabled property will be
    * calculated and if the add-on is changed the database will be saved and
    * appropriate notifications will be sent out to the registered AddonListeners.
    *
@@ -5326,17 +5348,18 @@ AddonInstall.prototype = {
         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
                                                      existingAddonID);
         cleanStagingDir(stagedAddon.parent, []);
 
         // Update the metadata in the database
         this.addon._sourceBundle = file;
         this.addon._installLocation = this.installLocation;
-        this.addon.updateDate = recursiveLastModifiedTime(file); // XXX sync recursive scan
+        let [mFile, mTime] = recursiveLastModifiedTime(file);
+        this.addon.updateDate = mTime;
         this.addon.visible = true;
         if (isUpgrade) {
           this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
                                                         file.persistentDescriptor);
         }
         else {
           this.addon.installDate = this.addon.updateDate;
           this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon))
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -11,17 +11,18 @@ const IGNORE = ["escapeAddonURI", "shoul
                 "addManagerListener", "removeManagerListener",
                 "mapURIToAddonID"];
 
 const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
                         "AddonScreenshot", "AddonType", "startup", "shutdown",
                         "registerProvider", "unregisterProvider",
                         "addStartupChange", "removeStartupChange",
                         "recordTimestamp", "recordSimpleMeasure",
-                        "getSimpleMeasures", "simpleTimer"];
+                        "getSimpleMeasures", "simpleTimer",
+                        "setTelemetryDetails", "getTelemetryDetails"];
 
 function test_functions() {
   for (let prop in AddonManager) {
     if (typeof AddonManager[prop] != "function")
       continue;
     if (IGNORE.indexOf(prop) != -1)
       continue;
 
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -3846,16 +3846,17 @@ XREMain::XRE_mainRun()
     NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
 #endif
 
     nsCOMPtr<nsIObserverService> obsService =
       mozilla::services::GetObserverService();
     if (obsService)
       obsService->NotifyObservers(nullptr, "final-ui-startup", nullptr);
 
+    (void)appStartup->DoneStartingUp();
     appStartup->GetShuttingDown(&mShuttingDown);
   }
 
   if (!mShuttingDown) {
     rv = cmdLine->Run();
     NS_ENSURE_SUCCESS_LOG(rv, NS_ERROR_FAILURE);
 
     appStartup->GetShuttingDown(&mShuttingDown);
--- a/webapprt/Startup.jsm
+++ b/webapprt/Startup.jsm
@@ -20,16 +20,17 @@ Cu.import("resource://gre/modules/Permis
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
 // Initialize window-independent handling of webapps- notifications.
 Cu.import("resource://webapprt/modules/WebappsHandler.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
+Cu.import("resource://webapprt/modules/WebRTCHandler.jsm");
 
 const PROFILE_DIR = OS.Constants.Path.profileDir;
 
 function isFirstRunOrUpdate() {
   let savedBuildID = null;
   try {
     savedBuildID = Services.prefs.getCharPref("webapprt.buildID");
   } catch (e) {}
new file mode 100644
--- /dev/null
+++ b/webapprt/WebRTCHandler.jsm
@@ -0,0 +1,103 @@
+/* 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 = [];
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function handleRequest(aSubject, aTopic, aData) {
+  let { windowID, callID } = aSubject;
+  let constraints = aSubject.getConstraints();
+  let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+  contentWindow.navigator.mozGetUserMediaDevices(
+    constraints,
+    function (devices) {
+      prompt(contentWindow, callID, constraints.audio,
+             constraints.video || constraints.picture,
+             devices);
+    },
+    function (error) {
+      denyRequest(callID, error);
+    });
+}
+
+function prompt(aWindow, aCallID, aAudioRequested, aVideoRequested, aDevices) {
+  let audioDevices = [];
+  let videoDevices = [];
+  for (let device of aDevices) {
+    device = device.QueryInterface(Ci.nsIMediaDevice);
+    switch (device.type) {
+      case "audio":
+        if (aAudioRequested) {
+          audioDevices.push(device);
+        }
+        break;
+
+      case "video":
+        if (aVideoRequested) {
+          videoDevices.push(device);
+        }
+        break;
+    }
+  }
+
+  if (audioDevices.length == 0 && videoDevices.length == 0) {
+    denyRequest(aCallID);
+    return;
+  }
+
+  let params = {
+                 videoDevices: videoDevices,
+                 audioDevices: audioDevices,
+                 out: null
+               };
+  aWindow.openDialog("chrome://webapprt/content/getUserMediaDialog.xul", "",
+                     "chrome, dialog, modal", params).focus();
+
+  if (!params.out) {
+    denyRequest(aCallID);
+    return;
+  }
+
+  let allowedDevices = Cc["@mozilla.org/supports-array;1"].
+                       createInstance(Ci.nsISupportsArray);
+  let videoIndex = params.out.video;
+  let audioIndex = params.out.audio;
+
+  if (videoIndex != -1) {
+    allowedDevices.AppendElement(videoDevices[videoIndex]);
+  }
+
+  if (audioIndex != -1) {
+    allowedDevices.AppendElement(audioDevices[audioIndex]);
+  }
+
+  if (allowedDevices.Count()) {
+    Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow",
+                                 aCallID);
+  } else {
+    denyRequest(aCallID);
+  }
+}
+
+function denyRequest(aCallID, aError) {
+  let msg = null;
+  if (aError) {
+    msg = Cc["@mozilla.org/supports-string;1"].
+          createInstance(Ci.nsISupportsString);
+    msg.data = aError;
+  }
+
+  Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID);
+}
+
+Services.obs.addObserver(handleRequest, "getUserMedia:request", false);
new file mode 100644
--- /dev/null
+++ b/webapprt/content/getUserMediaDialog.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+function onOK() {
+  window.arguments[0].out = {
+    video: document.getElementById("video").selectedItem.value,
+    audio: document.getElementById("audio").selectedItem.value
+  };
+
+  return true;
+}
+
+function onLoad() {
+  let videoDevices = window.arguments[0].videoDevices;
+  if (videoDevices.length) {
+    let videoMenu = document.getElementById("video");
+    for (let i = 0; i < videoDevices.length; i++) {
+      videoMenu.appendItem(videoDevices[i].name, i);
+    }
+    videoMenu.selectedIndex = 1;
+  } else {
+    document.getElementById("videoGroup").hidden = true;
+  }
+
+  let audioDevices = window.arguments[0].audioDevices;
+  if (audioDevices.length) {
+    let audioMenu = document.getElementById("audio");
+    for (let i = 0; i < audioDevices.length; i++) {
+      audioMenu.appendItem(audioDevices[i].name, i);
+    }
+    audioMenu.selectedIndex = 1;
+  } else {
+    document.getElementById("audioGroup").hidden = true;
+  }
+
+  window.sizeToContent();
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/content/getUserMediaDialog.xul
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+
+<!-- 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/.  -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % gum-askDTD SYSTEM "chrome://webapprt/locale/getUserMediaDialog.dtd">
+%gum-askDTD;
+]>
+
+<dialog id="getUserMediaDialog" title="&getUserMediaDialog.title;"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        buttons="accept,cancel"
+        buttonlabelaccept="&getUserMediaDialog.buttonlabelaccept;"
+        buttonaccesskeyaccept="&getUserMediaDialog.buttonaccesskeyaccept;"
+        onload="onLoad()"
+        ondialogaccept="return onOK()"
+        buttonlabelcancel="&getUserMediaDialog.buttonlabelcancel;"
+        buttonaccesskeycancel="&getUserMediaDialog.buttonaccesskeycancel;">
+
+<script type="application/javascript"
+        src="chrome://webapprt/content/getUserMediaDialog.js"/>
+
+  <groupbox id="videoGroup" flex="1">
+    <caption label="&getUserMediaDialog.video.label;"/>
+    <menulist id="video">
+      <menupopup>
+        <menuitem label="&getUserMediaDialog.video.noVideo;" value="-1"/>
+      </menupopup>
+    </menulist>
+  </groupbox>
+
+  <groupbox id="audioGroup" flex="1">
+    <caption label="&getUserMediaDialog.audio.label;"/>
+    <menulist id="audio">
+      <menupopup>
+        <menuitem label="&getUserMediaDialog.audio.noAudio;" value="-1"/>
+      </menupopup>
+    </menulist>
+  </groupbox>
+
+</dialog>
--- a/webapprt/jar.mn
+++ b/webapprt/jar.mn
@@ -1,12 +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/.
 
 webapprt.jar:
 % content webapprt %content/
 * content/webapp.js                     (content/webapp.js)
 * content/webapp.xul                    (content/webapp.xul)
+  content/getUserMediaDialog.xul        (content/getUserMediaDialog.xul)
+  content/getUserMediaDialog.js         (content/getUserMediaDialog.js)
   content/mochitest-shared.js           (content/mochitest-shared.js)
   content/mochitest.js                  (content/mochitest.js)
   content/mochitest.xul                 (content/mochitest.xul)
   content/dbg-webapp-actors.js          (content/dbg-webapp-actors.js)
new file mode 100644
--- /dev/null
+++ b/webapprt/locales/en-US/webapprt/getUserMediaDialog.dtd
@@ -0,0 +1,17 @@
+<!-- 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/.  -->
+
+<!-- LOCALIZATION NOTE: These are localized strings for the getUserMedia dialog
+   - to ask permissions in the webapp runtime. -->
+
+<!ENTITY getUserMediaDialog.title                 "Media Sharing">
+<!ENTITY getUserMediaDialog.buttonlabelaccept     "Share">
+<!ENTITY getUserMediaDialog.buttonaccesskeyaccept "S">
+<!ENTITY getUserMediaDialog.buttonlabelcancel     "Cancel">
+<!ENTITY getUserMediaDialog.buttonaccesskeycancel "n">
+
+<!ENTITY getUserMediaDialog.video.label           "Select camera">
+<!ENTITY getUserMediaDialog.video.noVideo         "No video">
+<!ENTITY getUserMediaDialog.audio.label           "Select microphone">
+<!ENTITY getUserMediaDialog.audio.noAudio         "No audio">
--- a/webapprt/locales/jar.mn
+++ b/webapprt/locales/jar.mn
@@ -2,10 +2,11 @@
 # 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/.
 
 @AB_CD@.jar:
 % locale webapprt @AB_CD@ %locale/webapprt/
     locale/webapprt/webapp.dtd                     (%webapprt/webapp.dtd)
     locale/webapprt/webapp.properties              (%webapprt/webapp.properties)
+    locale/webapprt/getUserMediaDialog.dtd         (%webapprt/getUserMediaDialog.dtd)
 
 % locale branding @AB_CD@ resource://webappbranding/
--- a/webapprt/moz.build
+++ b/webapprt/moz.build
@@ -19,15 +19,16 @@ EXTRA_COMPONENTS += [
     'DirectoryProvider.js',
     'PaymentUIGlue.js',
     'components.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'RemoteDebugger.jsm',
     'Startup.jsm',
+    'WebRTCHandler.jsm',
     'WebappRT.jsm',
     'WebappsHandler.jsm',
 ]
 
 MOCHITEST_WEBAPPRT_CHROME_MANIFESTS += ['test/chrome/webapprt.ini']
 MOCHITEST_MANIFESTS += ['test/content/mochitest.ini']
 
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/browser_getUserMedia.js
@@ -0,0 +1,38 @@
+Cu.import("resource://gre/modules/Services.jsm");
+
+function test() {
+  waitForExplicitFinish();
+
+  let openedWindows = 0;
+
+  let winObserver = function(win, topic) {
+    if (topic == "domwindowopened") {
+      win.addEventListener("load", function onLoadWindow() {
+        win.removeEventListener("load", onLoadWindow, false);
+        openedWindows++;
+        if (openedWindows == 2) {
+          win.close();
+        }
+      }, false);
+    }
+  }
+
+  Services.ww.registerNotification(winObserver);
+
+  let mutObserver = null;
+
+  loadWebapp("getUserMedia.webapp", undefined, function onLoad() {
+    let msg = gAppBrowser.contentDocument.getElementById("msg");
+    mutObserver = new MutationObserver(function(mutations) {
+      is(msg.textContent, "PERMISSION_DENIED", "getUserMedia permission denied.");
+      is(openedWindows, 2, "Prompt shown.");
+      finish();
+    });
+    mutObserver.observe(msg, { childList: true });
+  });
+
+  registerCleanupFunction(function() {
+    Services.ww.unregisterNotification(winObserver);
+    mutObserver.disconnect();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/getUserMedia.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>getUserMedia Test App</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <span id="msg"></span>
+    <script>
+      navigator.mozGetUserMedia({ video: true, audio: true },
+        function(localMediaStream) {
+          document.getElementById("msg").textContent = window.URL.createObjectURL(localMediaStream);
+        },
+
+        function(err) {
+          document.getElementById("msg").textContent = err;
+        });
+    </script>
+    <h1>getUserMedia Test App</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/getUserMedia.webapp
@@ -0,0 +1,5 @@
+{
+  "name": "getUserMedia Test App",
+  "description": "an app for testing getUserMedia",
+  "launch_path": "/webapprtChrome/webapprt/test/chrome/getUserMedia.html"
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/test/chrome/getUserMedia.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/x-web-app-manifest+json
--- a/webapprt/test/chrome/webapprt.ini
+++ b/webapprt/test/chrome/webapprt.ini
@@ -21,18 +21,23 @@ support-files =
   geolocation-prompt-noperm.html
   debugger.webapp
   debugger.webapp^headers^
   debugger.html
   mozpay.webapp
   mozpay.webapp^headers^
   mozpay.html
   mozpay-success.html
+  getUserMedia.webapp
+  getUserMedia.webapp^headers^
+  getUserMedia.html
 
 
 [browser_sample.js]
 [browser_window-title.js]
 [browser_webperm.js]
 [browser_noperm.js]
 [browser_geolocation-prompt-perm.js]
 [browser_geolocation-prompt-noperm.js]
 [browser_debugger.js]
 [browser_mozpay.js]
+[browser_getUserMedia.js]
+skip-if = true